Java:BufferedReader 在 close() 上永远挂起并且 StreamDecoder 不考虑线程中断

Java: BufferedReader hangs forever on close() and StreamDecoder doesn't respect thread interrupt

我有一个 Java 程序,它启动一个由进程 class 表示的单独子进程,然后附加查看进程 stdout/stderr 的侦听器。在某些情况下,进程将挂起并停止取得进展,此时 TimeLimiter 将抛出 TimeoutException,尝试中断实际执行 readLine() 调用的底层线程,然后使用kill -9 并关闭 Process 对象的 stdout 和 stderr 流。它尝试做的最后一件事是关闭 BufferedReader,但此调用永远挂起。示例代码如下:

private static final TimeLimiter timeLimiter = new SimpleTimeLimiter(); // has its own thread pool

public void readStdout(Process process) {
    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    try {
        String line = null;
        while ((line = timeLimiter.callWithTimeout(reader::readLine, 5, TimeUnit.SECONDS, true)) != null) { // this will throw a TimeoutException when the process hangs
            System.out.println(line);
        }
    } finally {
        killProcess(process); // this does a "kill -9" on the process
        process.getInputStream().close(); // this works fine
        process.getErrorStream().close(); // this works fine
        reader.close(); // THIS HANGS FOREVER
    }
}

为什么 close() 呼叫永远挂起,我该怎么办?

相关问题:

更新:

如果不清楚,TimeLimiter 来自 Guava 库:https://github.com/google/guava/blob/master/guava/src/com/google/common/util/concurrent/SimpleTimeLimiter.java

另外,有人要求我提供 killProcess() 方法的代码,所以这里是(注意这只适用于 Linux/Unix 机器):

public void killProcess(Process process) {
    // get the process ID (pid)
    Field field = process.getClass().getDeclaredField("pid"); // assumes this is a java.lang.UNIXProcess
    field.setAccessible(true);
    int pid = (Integer)field.get(process);

    // populate the list of child processes
    List<Integer> processes = new ArrayList<>(Arrays.asList(pid));
    for (int i = 0; i < processes.size(); ++i) {
        Process findChildren = Runtime.getRuntime().exec(new String[] { "ps", "-o", "pid", "--no-headers", "--ppid", Integer.toString(processes.get(i)) });
        findChildren.waitFor(); // this will return a non-zero exit code when no child processes are found
        Scanner in = new Scanner(findChildren.getInputStream());
        while (in.hasNext()) {
            processes.add(in.nextInt());
        }
        in.close();
    }

    // kill all the processes, starting with the children, up to the main process
    for (int i = processes.size() - 1; i >= 0; --i) {
        Process killProcess = Runtime.getRuntime().exec(new String[] { "kill", "-9", Integer.toString(processes.get(i)) });
        killProcess.waitFor();
    }
}

这里的根本问题是多线程和同步锁。当您调用 timeLimiter.callWithTimeout 时,它会在线程池中创建另一个线程来实际执行 readLine()。当调用超时时,主线程试图调用close(),但不幸的是BufferedReader中的readLine()close()方法使用了同一个同步锁对象,所以由于另一个线程已经有了锁,这个调用将阻塞直到另一个线程放弃它。但是如果 readLine() 调用从不 returns,那么 close() 调用将永远挂起。这是 BufferedReader 源代码的片段:

String readLine(boolean ignoreLF) throws IOException {
    StringBuffer s = null;
    int startChar;

    synchronized (lock) {
        ensureOpen();
        boolean omitLF = ignoreLF || skipLF;
        ...
        ...
        ...
    }
}

public void close() throws IOException {
    synchronized (lock) {
        if (in == null)
            return;
        try {
            in.close();
        } finally {
            in = null;
            cb = null;
        }
    }
}

当然,TimeLimiter 会尝试中断正在执行 readLine() 的线程,以便该线程真正退出并让尝试调用 close() 的线程通过。这里真正的错误是 BufferedReader 没有遵守线程中断。事实上,在 JDK 跟踪器中已经报告了一个关于这件事的错误,但由于某种原因它被标记为 "won't fix":https://bugs.openjdk.java.net/browse/JDK-4859836

不过,公平地说,BufferedReader 并没有真正负责处理线程中断。 BufferedReader 只是一个缓冲区 class,所有对其 read()readLine() 方法的调用都只是从底层输入流中读取数据。在这种情况下,底层 class 是一个 InputStreamReader,如果您查看其源代码,它会在后台创建一个 StreamDecoder 来执行所有读取操作。确实,错误出在 StreamDecoder 上——它应该支持线程中断,但事实并非如此。

怎么办?不管好坏,都无法强制对象放弃其线程锁。由于 StreamDecoder 显然不是我们拥有或可以编辑的代码,因此我们无能为力。我目前的解决方案只是删除我们在 BufferedReader 上调用 close() 的部分,所以现在至少程序不会永远挂起。但它仍然是一个内存泄漏......在 TimeLimiter 的线程池中 运行ning readLine() 的线程实际上将永远 运行 。由于这是一个长期 运行ning 程序的一部分,该程序会随着时间的推移处理大量数据,最终该线程池将被垃圾线程填满并且 JVM 将崩溃...

如果有人对如何解决此问题有任何其他建议,请告诉我。

如果您终止该进程,stdout/stderr 应该会在不久之后干涸(最终当 OS 管道干涸时,来自 readLine 的 EOF 和 null)。

至少,关闭流应该会导致并发 readers/writers 失败。这适用于套接字,所以我希望它适用于文件和进程...

所以我认为调用 bufferedreader.close() 没有意义,您没有什么可松懈的。 BufferedReader 只是将在 GC 上释放的内存。底层流已经关闭,即便如此,进程也会被终止。这些 kill 或 close 之一必须弹出从属线程的 readLine 并返回 null 或某些异常。

更新:

下面的代码显示终止进程将按预期结束流:

package tests;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

public class TestProcessStreamsCloseEOF {

    static void p(Object msg, Throwable... ta) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        pw.println(Thread.currentThread().getName()+"] "+msg);

        if(ta!=null && ta.length>0)
            Stream.of(ta).forEach(t -> t.printStackTrace(pw));
        pw.flush();
        System.out.print(sw.toString());
    }

    public static void main(String[] args) throws Exception {
        /*
        slowecho.bat:
        -----------
        @echo off
        echo line 1
        pause
        echo line 2
        */
        Process p = new ProcessBuilder("slowecho.bat").start();
        new Thread(() -> {dump(p.getInputStream());}, "dumpstdout").start();
        new Thread(() -> {dump(p.getErrorStream());}, "dumpstderr").start();

        p("sleep 5s");
        Thread.sleep(5000);

        p("destroy...");
        //p.destroy();
        p.destroyForcibly();

        p("waitfor 5s");
        p.waitFor(5, TimeUnit.SECONDS);

        p("sleep 5s");
        Thread.sleep(5000);

        p("end.");
    }

    static void dump(InputStream is) {
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(is, "ISO-8859-1"));
            String line;
            while((line=br.readLine()) !=null) {
                p(line);
            }
        } catch(Throwable t) {
            p(""+t, t);
        }
        p("end");
    }

}