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

标签 java multithreading memory-leaks bufferedreader freeze

我有一个 Java 程序,它启动一个由 Process 类表示的单独的子进程,然后附加查看进程的 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() 调用永远挂起,我该怎么办?

相关问题:Program freezes on bufferedreader 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(),但不幸的是readLine()close()方法在BufferedReader 使用相同的同步锁对象,因此由于另一个线程已经拥有锁,此调用将阻塞直到另一个线程放弃它。但是如果 readLine() 调用永远不会返回,那么 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 跟踪器中已经报告了一个关于这件事的错误,但由于某种原因它被标记为“不会修复”:https://bugs.openjdk.java.net/browse/JDK-4859836

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

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

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

关于Java:BufferedReader 在 close() 上永远挂起,而 StreamDecoder 不考虑线程中断,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49825866/

相关文章:

ruby-on-rails - Rails Controller 是多线程的吗? Thread.exclusive 在 Controller 中

ios - NSData dataWithContentsOfURL 泄漏和 datawithContentsOfURL :options:error reports the URL is nil?

go - 为什么这个 golang 程序会造成内存泄漏?

java - 资源泄漏测试

java - 将屏幕和网络摄像头录制到视频文件

java - 使用 eclipse 在 java 中使用 windows 应用程序

java - 要求用户选择一个联系人,然后能够在以后查找该联系人的信息?

c# - 连续启动线程

java - SystemSecurityManager 默认做什么?

java - android jni返回多个变量