Java Process Example Part 2

When reading and writing to a child process, it is necessary to execute the read and write blocks in separate threads to avoid deadlock.

1. Introduction

In the previous part of this Java Process Guide, we looked at how to read from and write to a child process. While executing the read and write blocks in a single thread works for simple cases, we should run these in separate threads to avoid deadlocks.

2. Read/Write Deadlock

For reference, here is the code that will deadlock when writing too much data (if the file sample.txt is too large).

Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cat", new String[0]);
try (OutputStream out = process.getOutputStream();
     InputStream fin = new FileInputStream("sample.txt");) {
    byte[] bytes = new byte[2048];
    int len;
    while ((len = fin.read(bytes)) != -1) {
	out.write(bytes, 0, len);
	System.err.print('.');
	System.err.flush();
    }
}

System.err.println("Done writing data ..");

try (InputStream in = process.getInputStream();) {
    byte[] bytes = new byte[2048];
    int len;
    while ((len = in.read(bytes)) != -1) {
	System.out.write(bytes, 0, len);
    }
}

In this case, the deadlock happens because the child process just echoes the input. However the parent process attempts to write the whole file before reading any data from the child. This will fill up the operating system buffers on the parent read-side and the parent is put to sleep. Subsequently the child is also idle because it is waiting for input from the parent.

The output of this code looks like the following. For each block of code written, the parent prints a dot (“.“).  Subsequently when the read buffer is full, the child process is blocked. The program has now deadlocked.

...................................................................

3. Separate Threads for Reader and Writer

So how does one avoid such a deadlock? By running the read block and write block in separate threads. That way, the read block can continue reading from the InputStream. And the write block can continue to write independently of the read stream. Here is how the code looks like:

Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cat", new String[0]);
ExecutorService esvc = Executors.newCachedThreadPool();
Runnable writer = () -> {
    try (OutputStream out = process.getOutputStream();
	 InputStream fin = new FileInputStream("sample.txt");) {
	byte[] bytes = new byte[2048];
	int len;
	while ((len = fin.read(bytes)) != -1) {
	    out.write(bytes, 0, len);
	    System.err.print('.');
	    System.err.flush();
	}
	System.err.println("Done writing data ..");
    } catch(java.io.IOException ex) {
	System.err.println(ex.getMessage());
    }
};
esvc.submit(writer);

Runnable reader = () -> {
    try (InputStream in = process.getInputStream();) {
	byte[] bytes = new byte[2048];
	int len;
	while ((len = in.read(bytes)) != -1) {
	    System.out.write(bytes, 0, len);
	}
    } catch(java.io.IOException ex) {
	System.err.println(ex.getMessage());
    }
};
esvc.submit(reader);
	
Runnable waiter = () -> {
    try {
	System.out.println("Process exited with: " + process.waitFor());
    } catch(InterruptedException ex) {}
};
esvc.submit(waiter).get();
esvc.shutdown();

Summary

When writing and reading data from and to the child process, it is necessary to avoid deadlock. This can be done by running the read and write blocks in separate threads.