一、管道流的特点与作用:
PipedInputStream与PipedOutputStream分别为管道输入流和管道输出流。管道输入流通过连接到管道输出流实现了类似管道的功能,用于线程之间的通信。在使用时,通常由某个线程向管道输出流中写入数据。根据管道的特性,这些数据会自动发送到与管道输出流对应的管道输入流中。这时其他线程就可以从管道输入流中读取数据,这样就实现了线程之间的通信。
管道流与其他流一样,分为读入和写出流,但是管道流是用于从管道内读取内容和将内容写入到管道中。并且,在进行管道流对象的创建时,如果构建输入流对象则需要在输入流中传入输出流对象,构建输出流对象则需要在构造器中传入输入流对象。每次写入管道时写入的数据,都将会被存储在作为缓存区进行使用的字节数组中。由输出流写入管道,再由输入流从管道中进行读取。
二、输出流PipOutPutStream:
首先来看一下输出流的构造器:
public PipedOutputStream(PipedInputStream snk) throws IOException {
connect(snk);
}
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;
}
在进行输出流的构造时,需要需要传入一个输入流的对象,然后将输入流对象的连接状态设置为连接,并对其中的参数进行初始化。
之后,再来看write方法:
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);
}
write方法中,在经过了一系列的参数校验以后,调用了自身持有的输入流成员变量 sink 的receive方法。这个方法即是管道流的主要核心方法。
三、输入流 PipInPutStream:
刚才说到的是receive方法,这个方法中的逻辑看起来比较复杂,在看这段代码之前,需要先看一下这个类的成员变量有哪些:
字段 | 说明 |
---|---|
boolean closedByWriter = false; | 管道输出流是否关闭 |
volatile boolean closedByReader = false; | 管道输入流是否关闭 |
boolean connected = false; | 管道输入流是否被连接 |
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; | 缓冲区的位置,此管道输入流将从该位置读取下一个数据字节。 |
就可以了解到这段代码的功能。这段代码主要用于将一个字节数组 b 存储到缓存区 buffer 数组中。如果buffer缓存区已经满了,但是没有被读取,则会等待读取完毕后再进行继续存储。此处唯一要注意的点,为缓存区数组中的内容是不会进行清空的,只会进行循环使用。
synchronized void receive(byte b[], int off, int len) throws IOException {
//检查PipedInputStream状态,如果不正常,则抛出异常。
checkStateForReceive();
//获取将数据写入管道的线程
writeSide = Thread.currentThread();
//需要接收的数据量,初始值为len
int bytesToTransfer = len;
//如果需要接收的数据>0,即还有需要接收的数据
while (bytesToTransfer > 0) {
//如果写入管道的数据已经被读完,则等待
if (in == out)
awaitSpace();
//下次要传输的字节数,初始值为0
int nextTransferAmount = 0;
//如果缓冲区未满(满后,in会重置为0)
if (out < in) {
//计算下次要传输的字节数。值为缓冲区还剩余的空间(即in之前的位置都满了,只有in到末尾的位置可用)
nextTransferAmount = buffer.length - in;
} else if (in < out) {//?
if (in == -1) {//?
in = out = 0;
nextTransferAmount = buffer.length - in;
} else {//如果缓冲区已满(此时out之后和in之前的位置已满,只有in到out之间的位置可用)
nextTransferAmount = out - in;
}
}
//如果下次传输的字节数大于需要传输的字节数,这时只需要bytesToTransfer大小的空间就可以了
if (nextTransferAmount > bytesToTransfer)
nextTransferAmount = bytesToTransfer;
assert(nextTransferAmount > 0);
//将nextTransferAmount个字节从b中复制到缓冲区数组中
System.arraycopy(b, off, buffer, in, nextTransferAmount);
//重新计算需要传输的字节数,已经传输了nextTransferAmount字节,所以减去nextTransferAmount
bytesToTransfer -= nextTransferAmount;
//b中已经复制的部分以后不能再复制了,所以偏移量off后移nextTransferAmount个位置,防止重复复制
off += nextTransferAmount;
//将in后移nextTransferAmount,防止之前复制的数据被覆盖
in += nextTransferAmount;
//如果in已经超出缓冲区大小,将in置0,从头开始写
if (in >= buffer.length) {
in = 0;
}
}
}
之后是read(byte[] b, int off, int len)方法,该方法中,会从缓存区数组中读取内容,如果已经读取出了所有的内容,则会将in标记重新重置为-1。
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 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;
}
四、总结:
管道流的输入与输出必须存在于两个不同的线程中进行使用,在进行管道的输入流输入时,如果缓存区已经写满,却一直没有被读取,则会造成当前线程的死锁。如果在进行从缓存区读取的时候,如果先执行了读取操作,进行读取操作的线程会认为目前还没有进行写入,会在当前线程进行等待写入管道流,同样会造成死锁的情况发生。