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");
}
}
我有一个 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");
}
}