java IO笔记(PipedInputStream/PipedOutputStream)
在上一篇笔记中讲述了java io 中的文件(file)以及如何用文件流来对文件进行读写操作,本篇则要讲述的是java IO中的管道流。
java IO中的管道流可以使得同一进程中的不同线程进行通信,如果不明白进程和线程的区别的话,可以去网上搜搜资料,可以看做提供同一jvm的通信能力。在java IO中管道的创建需要通过PipedInputStream和PipedOutputStream两个类,可以通过两者的构造方法进行互相关联也可以通过其中的connect方法进行关联。
下面将用一个最简单的例子来表明其功能。
package pipedIO;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Date;
public class PipedIO {
private PipedInputStream pis = new PipedInputStream();
private PipedOutputStream pos = new PipedOutputStream();
public static void main(String[] args) throws IOException {
PipedIO pipedIO = new PipedIO();
pipedIO.initThread();
}
private void initThread() throws IOException {
pos.connect(pis);
Thread input = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
String time = new Date().toString();
pos.write(time.getBytes());
System.out.println("输出流完成一次数据写出,数据为"+time);
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
});
Thread output = new Thread(new Runnable() {
@Override
public void run() {
byte[] temp = new byte[1024];
try {
int len;
while ((len = pis.read(temp)) != -1) {
System.out.println("输入流完成一次数据写入,数据为:"+new String(temp, 0, len));
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
});
input.start();
output.start();
}
}
执行上述代码,可以看到如下打印:
从控制台输出可以看出,两个线程之间完成了通信,看上去十分简单。但管道流真正的使用时,还需要注意一些事项。下面说说管道流的工作原理吧。
PipedInputStream.java
package java.io;
public class PipedInputStream extends InputStream {
boolean closedByWriter = false;
volatile boolean closedByReader = false;
boolean connected = false;
/* REMIND: identification of the read and write sides needs to be
more sophisticated. Either using thread groups (but what about
pipes within a thread?) or using finalization (but it may be a
long time until the next GC). */
Thread readSide;
Thread writeSide;
private static final int DEFAULT_PIPE_SIZE = 1024;
protected static final int PIPE_SIZE = DEFAULT_PIPE_SIZE;
protected byte buffer[];
protected int in = -1;
protected int out = 0;
public PipedInputStream(PipedOutputStream src) throws IOException {
this(src, DEFAULT_PIPE_SIZE);
}
public PipedInputStream(PipedOutputStream src, int pipeSize)
throws IOException {
initPipe(pipeSize);
connect(src);
}
public PipedInputStream() {
initPipe(DEFAULT_PIPE_SIZE);
}
public PipedInputStream(int pipeSize) {
initPipe(pipeSize);
}
private void initPipe(int pipeSize) {
if (pipeSize <= 0) {
throw new IllegalArgumentException("Pipe Size <= 0");
}
buffer = new byte[pipeSize];
}
public void connect(PipedOutputStream src) throws IOException {
src.connect(this);
}
protected synchronized void receive(int b) throws IOException {
checkStateForReceive();
writeSide = Thread.currentThread();
if (in == out)
awaitSpace();
if (in < 0) {
in = 0;
out = 0;
}
buffer[in++] = (byte)(b & 0xFF);
if (in >= buffer.length) {
in = 0;
}
}
synchronized void receive(byte b[], int off, int len) throws IOException {
checkStateForReceive();
writeSide = Thread.currentThread();
int bytesToTransfer = len;
while (bytesToTransfer > 0) {
if (in == out)
awaitSpace();
int nextTransferAmount = 0;
if (out < in) {
nextTransferAmount = buffer.length - in;
} else if (in < out) {
if (in == -1) {
in = out = 0;
nextTransferAmount = buffer.length - in;
} else {
nextTransferAmount = out - in;
}
}
if (nextTransferAmount > bytesToTransfer)
nextTransferAmount = bytesToTransfer;
assert(nextTransferAmount > 0);
System.arraycopy(b, off, buffer, in, nextTransferAmount);
bytesToTransfer -= nextTransferAmount;
off += nextTransferAmount;
in += nextTransferAmount;
if (in >= buffer.length) {
in = 0;
}
}
}
private void checkStateForReceive() throws IOException {
if (!connected) {
throw new IOException("Pipe not connected");
} else if (closedByWriter || closedByReader) {
throw new IOException("Pipe closed");
} else if (readSide != null && !readSide.isAlive()) {
throw new IOException("Read end dead");
}
}
private void awaitSpace() throws IOException {
while (in == out) {
checkStateForReceive();
/* full: kick any waiting readers */
notifyAll();
try {
wait(1000);
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
}
synchronized void receivedLast() {
closedByWriter = true;
notifyAll();
}
public synchronized int read() throws IOException {
if (!connected) {
throw new IOException("Pipe not connected");
} else if (closedByReader) {
throw new IOException("Pipe closed");
} else if (writeSide != null && !writeSide.isAlive()
&& !closedByWriter && (in < 0)) {
throw new IOException("Write end dead");
}
readSide = Thread.currentThread();
int trials = 2;
while (in < 0) {
if (closedByWriter) {
/* closed by writer, return EOF */
return -1;
}
if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) {
throw new IOException("Pipe broken");
}
/* might be a writer waiting */
notifyAll();
try {
wait(1000);
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
int ret = buffer[out++] & 0xFF;
if (out >= buffer.length) {
out = 0;
}
if (in == out) {
/* now empty */
in = -1;
}
return ret;
}
public synchronized int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
/* possibly wait on the first character */
int c = read();
if (c < 0) {
return -1;
}
b[off] = (byte) c;
int rlen = 1;
while ((in >= 0) && (len > 1)) {
int available;
if (in > out) {
available = Math.min((buffer.length - out), (in - out));
} else {
available = buffer.length - out;
}
// A byte is read beforehand outside the loop
if (available > (len - 1)) {
available = len - 1;
}
System.arraycopy(buffer, out, b, off + rlen, available);
out += available;
rlen += available;
len -= available;
if (out >= buffer.length) {
out = 0;
}
if (in == out) {
/* now empty */
in = -1;
}
}
return rlen;
}
public synchronized int available() throws IOException {
if(in < 0)
return 0;
else if(in == out)
return buffer.length;
else if (in > out)
return in - out;
else
return in + buffer.length - out;
}
public void close() throws IOException {
closedByReader = true;
synchronized (this) {
in = -1;
}
}
}
PipedOutputStream.java
package java.io;
import java.io.*;
public class PipedOutputStream extends OutputStream {
private PipedInputStream sink;
public PipedOutputStream(PipedInputStream snk) throws IOException {
connect(snk);
}
public PipedOutputStream() {
}
public synchronized void connect(PipedInputStream snk) throws IOException {
if (snk == null) {
throw new NullPointerException();
} else if (sink != null || snk.connected) {
throw new IOException("Already connected");
}
sink = snk;
snk.in = -1;
snk.out = 0;
snk.connected = true;
}
public void write(int b) throws IOException {
if (sink == null) {
throw new IOException("Pipe not connected");
}
sink.receive(b);
}
public void write(byte b[], int off, int len) throws IOException {
if (sink == null) {
throw new IOException("Pipe not connected");
} else if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
sink.receive(b, off, len);
}
public synchronized void flush() throws IOException {
if (sink != null) {
synchronized (sink) {
sink.notifyAll();
}
}
}
public void close() throws IOException {
if (sink != null) {
sink.receivedLast();
}
}
}
上面贴出了PipedInputStream和PipedOutputStream的源码,从源码中不难看出,在PipedOutputStream中封装了一个PipedInputStream,当调用两个类中的的connect方法时,最终都回到了PipedOutputStream中的connect方法,使得两个流进行连接关系。
我们可以看出无论是PipedInputStream的read方法,还是PipedOutputStream中的write方法,数据都是存放在PipedInputStream中的一个byte数组的缓存中的,该缓存默认大小为1024字节。那么这就有一个问题了,当PipedInputStream中的缓存区已经装满的时候,必须要等到读取PipedInputStream数据缓存并清除相对应的数据时,才能继续往缓存中写入,如果一直等不到,将再次产生类似死锁的情况。
因此向PipedOutputStream中写入数据的线程不应是负责从对应PipedInputStream中读取数据的唯一线程,下面举例说明:
package pipedIO;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class PipedIO1 {
private PipedInputStream pis = new PipedInputStream();
private PipedOutputStream pos = new PipedOutputStream();
public static void main(String[] args) throws IOException {
PipedIO1 pipedIO1 = new PipedIO1();
pipedIO1.initThread();
}
private void initThread() throws IOException {
pos.connect(pis);
Thread input = new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] inBytes = new byte[500];
byte[] outBytes = new byte[1000];
pos.write(inBytes);
System.out.println("输出流完成一次数据写出");
int len = pis.read(outBytes);
System.out.println("输入流完成一次数据读出");
while (len != -1) {
pos.write(outBytes);
System.out.println("输出流完成一次数据写出");
len = pis.read(inBytes);
System.out.println("输入流完成一次数据读出");
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
System.out.println("there are some mistakes");
}
}
});
input.start();
}
}
执行上述代码后,控制台可以看到如下打印:
上面的代码模拟了PipedInputStream和PipedOutputStream在同一线程中同时工作的情况,PipedOutputStream每次向buffer中写入1000字节的数据,PipedInputStream每次向buffer中度却500字节的数据,没执行一次读写操作,buffer中都会剩余500字节的数据未读取,因为没有调用PipedInputStream中的void initPipe(int pipeSize)方法,所以buffer默认的大小为1024字节,当执行完第二次读写操作时,缓冲区只剩余24字节的空间,并不足够再一次写入1000字节的数据了,所以此时PipedInputStream的write操作就会阻塞等待有人从buffer中读取并清空缓存区。然而因为读写在同一线程中,PipedInputStream的read操作又在等待PipedOutputStream的write操作完成后,再执行read,从而造成类似死锁的情况,从控制台可以看出,程序卡在了第三次读写时。要解决这个情况也很简单,只要读写操作不在一个线程中就可以了,可以参考第一个例子。
除了上面避免进入死锁的情况还要注意,在进行数据传输的时候,读写线程是否一直保存存活(isAlive),无论是任何一个线程不在活跃,此时进行读写操作就很有可能抛出IOEception。
当然你也可以自己写一个工具类,优化一下,比如使用ByteArrayOutputStream的自动扩充缓存的特点来避免管道流因为缓存区空间不够而造成的死锁情况。只有最适合自己需求的才是最棒的,不是吗。
除了管道之外,java中不同线程之间还有很多的通信方式,如果需要在线程之间传递字节数据,管道流就是要一个不错的选择,尽管大部分时候通信之间可能直接传递的是对象而不是简单的字节数据。