基本概念
PipedOutputStream & PipedInputStream (管道字节输入流&管道输出字节流)是配套使用的。可以将管道输出流连接到管道输入流来创建通信管道。
通常的用法是让两个线程分别操作输入输出流,由输出流向管道写入数据,然后再由输出流从管道中读取数据。不建议对这两个对象尝试使用单个线程,因为这样可能会造成该线程死锁。
继承关系:
实例探究
1.生产者&消费者
定义两个线程代表生产者/消费者,然后将管道的输出/输入流进行连接,观察输出结果。
生产者(Producer ):即管道输出流(PipedOutputStream ),不断地向输入流输出数据。
消费者(Consumer ):即管道输入流(PipedInputStream ),不断地从输出流读取数据。
//生产者线程,负责向输入流输出数据
class Producer extends Thread {
private PipedOutputStream pos;
public Producer(PipedOutputStream pos, String name) {
super(name);
this.pos = pos;
}
public void run() {
int i = 0;
try {
while (true) {
Thread.sleep(500);
pos.write(i);
i++;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//消费者线程,负责从输出流读取数据
class Consumer extends Thread {
private PipedInputStream pis;
public Consumer(PipedInputStream pis, String name) {
super(name);
this.pis = pis;
}
public void run() {
try {
while (true) {
System.out.println(this.getName() + ":" + pis.read());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) throws IOException {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
//连接管道
pos.connect(pis);
Producer pro = new Producer(pos, "Producer");
Consumer con = new Consumer(pis, "Consumer");
//启动线程
pro.start();
con.start();
}
}
// 输出结果:
// Consumer:0
// Consumer:1
// Consumer:2
// Consumer:3
// Consumer:4
// Consumer:5
2.管道流限制性
管道流只能实现单向发送,如果要两个线程之间互通讯,则需要两个管道流。
观察输出结果,发现管道输出流每次只能与一个管道输入流通信,因此一个管道流只能用于两个线程间的通讯。不仅仅是管道流,其他 IO 方式都是一对一传输。
//采用上面的例子,省略相同代码...
public static void main(String[] args) throws IOException {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
pos.connect(pis);
Producer pro = new Producer(pos,"pro");
//定义了两个消费者线程(即两个管道输入流)
Consumer c1 = new Consumer(pis,"c1");
Consumer c2 = new Consumer(pis,"c2");
pro.start();
c1.start();
c2.start();
}
// 输出结果:
// c2:0
// c1:1
// c2:2
// c1:3
// c2:4
// c1:5
源码探究
2.PipedInputStream
类结构图
成员变量
boolean closedByWriter = false;
volatile boolean closedByReader = false;
boolean connected = false;
// 默认的管道缓冲数组大小
private static final int DEFAULT_PIPE_SIZE = 1024;
protected static final int PIPE_SIZE = DEFAULT_PIPE_SIZE;
// 缓冲区,即管道,在缓冲满之后会重新从头开始写入数据并覆盖掉原来的数据,也可以理解为置空操作
protected byte buffer[];
// 下一个写入字节的位置。in==out 代表管道没有可读数据
protected int in = -1;
// 下一个读取字节的位置。
protected int out = 0;
// 操作输入流线程
Thread readSide;
// 操作输出流线程
Thread writeSide;
该类定义了4个构造函数,可分为两种,未连接管道输出流(如:①②)与连接管道输出流(如:③④)。观察代码,构造函数主要做了两件事:
调用了 initPipe 方法来进行初始化,而该方法的主要作用是创建管道换区,如果未指定大小时,默认为 1024。
调用了 connect 方法来连接管道输出流,而该方法是在 PipedOutputStream 类中定义的,。它修改了输入流的成员变量值,令 in = -1; out = 0; connected = true;
// ①创建尚未连接的 PipedInputStream
public PipedInputStream() {
initPipe(DEFAULT_PIPE_SIZE);
}
// ②创建一个尚未连接的 PipedInputStream,并对管道缓冲区使用指定的管道大小
public PipedInputStream(int pipeSize) {
initPipe(pipeSize);
}
// ③创建 PipedInputStream,使其连接到管道输出流 src
public PipedInputStream(PipedOutputStream src) throws IOException {
this(src, DEFAULT_PIPE_SIZE);
}
// ④创建一个 PipedInputStream,使其连接到管道输出流 src,并对管道缓冲区使用指定的管道大小
public PipedInputStream(PipedOutputStream src, int pipeSize) throws IOException{
initPipe(pipeSize);
connect(src);
}
// 初始化管道,创建指定大小的缓冲数组
private void initPipe(int pipeSize) {
if (pipeSize <= 0) {
throw new IllegalArgumentException("Pipe Size <= 0");
}
buffer = new byte[pipeSize];
}
// 连接管道输出流(调用 PipedOutputStream 的连接方法)
public void connect(PipedOutputStream src) throws IOException {
src.connect(this);
}
下面来看 read 方法,这里定义了两种常见的读取数据的方式。需要注意的是,read 方法是线程安全的,即同一时间只能有一个线程操作该对象。假设现在定义了两个线程分别操作输出输入流,在输入流读取数据的时候,输出流则无法写入数据,反之亦然。
方法 ①,大致流程分为三步:
第一步,首先会判断管道是否存在可读取的数据,如果没有,唤醒其他处于阻塞状态的线程,本身线程进入阻塞等待状态;如果存在数据,则从管道中读取数据。
第二步,在从管道读取完数据就会判断是否到达的管道末尾,如果是的则修改索引(out=0),下次从管道初始位置读取(也可将管道理解为循环数组,因为一旦管道数据被写满后,就会再次从头开始写入数据并覆盖原来的数据,读取也遵循这个方式)。
第三步,判断管道中是否还有可读数据,如果没有(即 in = out),则将 in 置为 -1。
方法 ② :
第一步,先读取一个字节,用来判断管道中是否还有可读数据,没有的返回 -1;如果存在数据,则进入第二步。
第二步,通过比较 in 和 out 的大小,计算可读字节数量。在进行 read 操作时 in 会增加,write 操作时 out 会增加。 in > out,说明管道中还有可读数据;in < out,说明输入流已经将管道写满,所以会将 in 置为 0,再次从头开始写入数据(上面有提到过)。
第三步,将 available 与 len -1(减 1 因为一开始读取了一个字节)进行比较,如果不能一次读取完,则通过循环再次读取剩下内容。
// 读取此管道输入流中的下一个数据字节
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) {
return -1;
}
if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) {
throw new IOException("Pipe broken");
}
// 没有数据可读,则唤醒操作输出流的线程(因为是线程安全方法)
notifyAll();
try {
// 本身线程进入阻塞等待状态 1s
wait(1000);
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
//从管道中读取数据
int ret = buffer[out++] & 0xFF;
//判断是否读取到管道末尾,若是,再次从头开始读取
if (out >= buffer.length) {
out = 0;
}
//表示管道的内容已经全部读取完了
if (in == out) {
in = -1;
}
return ret;
}
// ②将最多 len 个数据字节从此管道输入流读入 byte 数组
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;
}
// 先读取一个字节判断是否到达流末尾
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 {
// 说明管道已经被写满,in 被置为0。所以剩余可读内容如下
available = buffer.length - out;
}
// 判断剩余待读取内容会不会超出数组的容量(上面先读取了一个字节,所以容量为 len -1)
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) {
in = -1;
}
}
return rlen;
}
下面来看看会在 receive 方法中调用到的两个方法
// 若“写入管道”的数据正好全部被读取完(例如,管道缓冲满),则执行 awaitSpace()操作;
// 它的目的是让“读取管道的线程”管道产生读取数据请求,从而才能继续的向“管道”中写入数据。
private void awaitSpace() throws IOException {
while (in == out) {
checkStateForReceive();
notifyAll();
try {
wait(1000);
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
}
// 检查状态
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");
}
}
重点来看 receive方法, 用于输入流接收信息,在输出流 write 操作时会被调用,我们也可以将其是为输出流的写入操作,只不过这里在输入流实现(因为是线程安全的方法,synchronized 生效的前提操作同一对象)。同样也有定义了两种常见的写入(接收)操作。
方法 ① :
第一步,检查状态,获取操作输出流的线程
第二步,判断管道的是否被全部读取完。若是,进入等待。假设有两个线程操作输出输入流,当前线程等待,则操作输入流的线程就会启动(前提是该线程还没销毁),启动后同样发现 in = out ,则将 in 置为 -1 结束。
第三步,判断 in < 0 ,是的话进行初始化(即将 in,out 置为 0),然后开始写入操作。
第四步,判断管道是否被写满,若已满则从头开始写入(之前的数据会被覆盖)。
方法 ② :
第一步,检查状态,计算要接收的字节数量
第二步,若还有要接收字节,判断管道的是否被全部读取完。然后进入第三步。
第三步,判断管道中是否还存在已写入,但未读取的数据。并计算等下要传输的字节数量
第四步,计算剩余要接收字节数量,不为 0 ,则循环进入第二步。
// ①从管道接收一个字节的数据
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;
}
}
// 最多从管道接收 len 的数量的字节数组
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;
}
//说明输入流已将管道写满,并将 in 置为 0
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,则终止程序
// 默认断言机制是没有开启的
assert (nextTransferAmount > 0);
System.arraycopy(b, off, buffer, in, nextTransferAmount);
//计算剩余还需要输入到管道的字节数量
bytesToTransfer -= nextTransferAmount;
off += nextTransferAmount;
in += nextTransferAmount;
if (in >= buffer.length) {
in = 0;
}
}
}
最后来看看剩余的方法
//输出流的 close 操作时会调用该方法
synchronized void receivedLast() {
closedByWriter = true;
notifyAll();
}
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{
// 说明管道已经被写满,并且已经开始重头再写入数据,但没有超过 out 的位置
//(因为 out 之后的数据没有被读取,不能覆盖,否则数据丢失)
return in + buffer.length - out;
}
}
public void close() throws IOException {
closedByReader = true;
synchronized (this) {
in = -1;
}
}
2.PipedOutputStream
类结构图
成员变量
// 与PipedOutputStream 通信的 PipedInputStream 对象
private PipedInputStream sink;
构造函数除了默认的无参构造函数外,还有一个带参的构造函数。在其内部调用了管道流连接的方法。
public PipedOutputStream() {
}
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 方法,定义了两种 write 方式,并且都通过输入流接收信息
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();
}
}