【问题标题】:Java: BufferedReader hangs forever on close() and StreamDecoder doesn't respect thread interruptJava:BufferedReader 在 close() 上永远挂起,并且 StreamDecoder 不考虑线程中断
【发布时间】:2018-09-24 08:05:24
【问题描述】:

我有一个 Java 程序,它启动一个由 Process 类表示的单独子进程,然后附加监听器来查看 Process 的 stdout/stderr。在某些情况下,进程会挂起并停止运行,此时 TimeLimiter 将抛出 TimeoutException,尝试中断实际上正在执行 readLine() 调用的底层线程,然后使用 @987654326 终止进程@ 并关闭来自 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();
    }
}

【问题讨论】:

  • 你是如何杀死进程的? Process#destroy() 和它的兄弟destroyForcibly 似乎也清理了流。在类似的情况下,人们有更多的success
  • 好吧,我试图在帖子中简化这一点,但如果您想了解更多细节,我们正在运行的进程实际上是 ffmpeg,而 ffmpeg 会衍生出各种子进程。当ffmpeg挂起时,我们最初只是按照你说的尝试了destroy()和destroyForcibly(),但事实证明这些方法并没有破坏子进程,所以我们不得不编写一个使用“ps”列出PID和子进程的子程序,然后遍历并在进程树中的每个进程和子进程上执行“kill -9”。

标签: java multithreading memory-leaks bufferedreader freeze


【解决方案1】:

如果你终止进程,stdout/stderr 应该很快就会变干(当操作系统管道变干时,EOF 和 readLine 中的 null 最终会变干)。

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

所以我认为调用 bufferedreader.close() 没有任何意义,你没有什么可松动的。 BufferedReader 只是将在 GC 上释放的内存。底层流已经关闭,即使这样,进程也会被终止。其中一个 kill 或 close 必须以 null 或某些异常弹出从属线程的 readLine。

更新:

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

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");
    }

}

【讨论】:

  • 是的,stdout 和 stderr 流已经关闭,但是 BufferedReader 永远不会收到 EOF,它只会永远挂起。而且由于它是在单独的线程中运行的,所以不会是内存泄漏吗?我假设线程不会被垃圾收集,直到 BufferedReader.readLine() 返回...
  • 怎么知道readline上的slave线程没有返回? while 循环以异常退出,但是当 readline 失败时从属线程代码在做什么?我们没有看到 SimpleTimeLimiter 的代码。我的意思是,如果没有所有代码,我们将无法为您提供太多帮助。
  • 因为如果我在 Future 上调用 get() 它永远不会返回。如果您查看我发布的答案 - 问题是 StreamDecoder 在 read() 期间等待输入时不支持线程中断。当我尝试关闭 BufferedReader 时我的应用程序挂起的事实是这种情况的一个症状,因为 read() 和 close() 操作尝试获取相同的同步锁。我是否可以关闭阅读器并不是我最关心的问题,因为我已经关闭了底层流,但是有一个线程无限期地卡在 read() 方法中的事实是内存泄漏。
  • 再次,你能提供完整的代码吗?你提到杀-9。 Process 类已经有一个销毁。不过,我可以理解确定性的必要性。我很久以前就停止尝试中断流,因为这只是代码必须检查的标志。众所周知,本机读/写不利于中断。但是关闭它们从来没有让我失望,如果没有完整的代码,我无法断言你声称的内容。我敢肯定,oracle 并没有未能实现一些显而易见的事情,比如在关闭流时踢出挂起的调用。也许你的 kill -9 不够优雅。无论如何,请出示代码。
  • 顺便说一句,如果您希望任何人尊重中断,那么您会经常感到失望而不是高兴。 3rd 方库在这方面尤其糟糕。而在 IO 中,情况更糟,因为 InterruptedIOException 扩展了 IOException,而不是 InterruptedEception。太容易被错误捕获并让重试错误地开始。
【解决方案2】:

这里的根本问题是多线程和同步锁。当您调用timeLimiter.callWithTimeout 时,它会在线程池中创建另一个线程来实际执行readLine()。当调用超时时,主线程尝试调用close(),可惜BufferedReader中的readLine()close()方法使用了同一个同步锁对象,所以由于另一个线程已经拥有锁,所以这个调用会阻塞直到另一个线程放弃它。但是如果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 将崩溃......

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-27
    • 2015-12-20
    • 1970-01-01
    • 2012-10-31
    • 1970-01-01
    • 2019-03-23
    • 2018-06-26
    • 2022-01-24
    相关资源
    最近更新 更多