在多线程编程中,线程间的数据交换是一个常见需求。Java IO包中的PipedInputStream和PipedOutputStream提供了一种高效的线程间通信机制,允许一批(多个)线程向PipedOutputStream写入数据,另一批(多个)线程从PipedInputStream读取数据。
但是,同一批(多个)线程相互之间会存在竞争,比如,同一批向PipedOutputStream写入数据的线程会存在竞争,同一批从PipedInputStream读取数据的线程也会存在竞争。因此PipedInputStream和PipedOutputStream中的线程安全需要通过synchronized关键字和wait()/notifyAll()机制实现。不建议在一个线程中同时使用PipedInputStream和PipedOutputStream,因为这样可能会导致这个线程陷入死锁状态。
PipedInputStream和PipedOutputStream之间的通信本质上是一个生产者-消费者模型,其中PipedOutputStream作为生产者,PipedInputStream作为消费者。两者通过一个循环缓冲区(byte[]数组)进行数据交换,PipedOutputStream将数据缓存在PipedInputStream的数组当中,等待PipedInputStream的读取。
PipedInputStream和PipedOutputStream的UML关系图,如下所示:
一、PipedOutputStream(生产者)源码------向PipedInputStream(消费者)中的缓冲区(byte[]数组)写入字节数据的输出Stream(生产者)
package java.io;
import java.io.*;
public
class PipedOutputStream extends OutputStream {
//与这个PipedOutputStream(生产者)相关联的 PipedInputStream (消费者)
private PipedInputStream sink;
//构造函数
public PipedOutputStream(PipedInputStream snk) throws IOException {
connect(snk);//调用connect()函数,来改变PipedInputStream (消费者)中一些变量的值
}
//构造函数
public PipedOutputStream() {
}
//线程同步函数:用来改变将要关联的PipedInputStream (消费者)中一些变量的值
public synchronized void connect(PipedInputStream snk) throws IOException {
if (snk == null) {
throw new NullPointerException();//如果将要关联的PipedInputStream (消费者)为null,抛出NullPointerException
} else if (sink != null || snk.connected) {
//如果与这个PipedOutputStream(生产者)相关联的 PipedInputStream (消费者)!=null或者将要关联的PipedInputStream (消费者)的boolean connected变量为true,则抛出IOException
throw new IOException("Already connected");
}
sink = snk;//将这个PipedOutputStream(生产者)与这个PipedInputStream (消费者)相关联
snk.in = -1;//改变PipedInputStream (消费者)中的变量int in=-1
snk.out = 0;//改变PipedInputStream (消费者)中的变量int out=0
snk.connected = true;//改变PipedInputStream (消费者)中的变量boolean connected=true
}
//向与这个PipedOutputStream(生产者)相关联的 PipedInputStream (消费者)的缓冲区(byte[]数组)写入1个字节
public void write(int b) throws IOException {
if (sink == null) {
//如果与这个PipedOutputStream(生产者)相关联的 PipedInputStream (消费者)== null,抛出IOException
throw new IOException("Pipe not connected");
}
sink.receive(b);//最终调用的是这个相关联的 PipedInputStream (消费者)的receive(int b)函数
}
//向与这个PipedOutputStream(生产者)相关联的 PipedInputStream (消费者)的缓冲区(byte[]数组)写入byte[]数组b的[off,off+len)(左闭右开,不包括off+len)索引位置的字节
public void write(byte b[], int off, int len) throws IOException {
if (sink == null) {
//如果与这个PipedOutputStream(生产者)相关联的 PipedInputStream (消费者)== null,抛出IOException
throw new IOException("Pipe not connected");
} else if (b == null) {
throw new NullPointerException();//如果byte[]数组b==null,抛出一个NullPointerException
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {//byte[]数组b的[off,off+len)(左闭右开)索引位置是否有越界的检查
throw new IndexOutOfBoundsException();//越界的话,抛出一个IndexOutOfBoundsException
} else if (len == 0) {
return;//如果len==0,结束本次函数调用
}
sink.receive(b, off, len);//最终调用的是这个相关联的 PipedInputStream (消费者)的receive(byte b[], int off, int len)函数
}
//线程同步函数:使用notifyAll()函数唤醒所有与这个PipedOutputStream(生产者)相关联的 PipedInputStream (消费者)线程(这个消费者可以绑定1~多个线程)
public synchronized void flush() throws IOException {
if (sink != null) {
synchronized (sink) {
sink.notifyAll();
}
}
}
//关闭这个PipedOutputStream(生产者),这个PipedOutputStream(生产者)不能再向与它相关联的PipedInputStream(消费者)中的缓冲区(byte[]数组)写入字节数据
public void close() throws IOException {
if (sink != null) {
sink.receivedLast();
}
}
}
二、PipedInputStream(消费者)源码------从自己的缓冲区(byte[]数组)读取字节数据的输入Stream(消费者)
package java.io;
public class PipedInputStream extends InputStream {
//标记符:true表示与这个 PipedInputStream (消费者)相关联的PipedOutputStream(生产者)已经关闭,反之,反之
boolean closedByWriter = false;
//标记符:true表示当前这个 PipedInputStream (消费者)已经关闭了,反之,反之
volatile boolean closedByReader = false;
//标记符:true表示与这个 PipedInputStream (消费者)相关联的PipedOutputStream(生产者)已经持有了这个PipedInputStream (消费者)对象(或者叫已经连接上了),反之,反之
boolean connected = false;
Thread readSide;//当前消费的线程
Thread writeSide;//当前生产者的线程
//默认的PipedInputStream (消费者)的缓冲区(byte[]数组)的长度
private static final int DEFAULT_PIPE_SIZE = 1024;
//PipedInputStream (消费者)的缓冲区(byte[]数组)
protected byte buffer[];
//缓冲区(byte[]数组)的写指针
protected int in = -1;
//缓冲区(byte[]数组)的读指针
protected int out = 0;
//构造函数
public PipedInputStream(PipedOutputStream src) throws IOException {
this(src, DEFAULT_PIPE_SIZE);//缓冲区(byte[]数组)的长度使用默认值1024
}
//构造函数
public PipedInputStream(PipedOutputStream src, int pipeSize)
throws IOException {
initPipe(pipeSize);//缓冲区(byte[]数组)的长度使用指定的长度
//最终还是调用PipedOutputStream(生产者)的connect()函数,并把自身对象this传递进去,然后在PipedOutputStream(生产者)的connect()函数中,改变自己的3个变量int in=-1、int out=0、boolean connected=true
connect(src);
}
//构造函数,缓冲区(byte[]数组)的长度使用默认值1024
public PipedInputStream() {
initPipe(DEFAULT_PIPE_SIZE);
}
//构造函数,缓冲区(byte[]数组)的长度使用指定的长度
public PipedInputStream(int pipeSize) {
initPipe(pipeSize);
}
//初始化缓冲区(byte[]数组)
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); //最终还是调用PipedOutputStream(生产者)的connect()函数,并把自身对象this传递进去,然后在PipedOutputStream(生产者)的connect()函数中,改变自己的3个变量int in=-1、int out=0、boolean connected=true
}
//线程同步函数:该函数只被PipedOutputStream(生产者)的write(int b)函数调用
protected synchronized void receive(int b) throws IOException {
checkStateForReceive();//检查PipedInputStream (消费者)的状态
writeSide = Thread.currentThread();//当前执行该函数的线程,就是生产者线程
if (in == out)
//如果缓冲区(byte[]数组)的读指针==缓冲区(byte[]数组)的写指针,唤醒所有消费者线程,自己这个生产者线程调用wait(1000)函数
awaitSpace();
if (in < 0) {//缓冲区(byte[]数组)的写指针<0时,设置缓冲区(byte[]数组)的写指针=0,缓冲区(byte[]数组)的读指针=0
in = 0;
out = 0;
}
buffer[in++] = (byte)(b & 0xFF);//向缓冲区的写指针位置写入1个字节
if (in >= buffer.length) {
in = 0;//如果缓冲区满了,设置缓冲区的写指针=0
}
}
//线程同步函数:该函数只被PipedOutputStream(生产者)的write(byte b[], int off, int len)函数调用
synchronized void receive(byte b[], int off, int len) throws IOException {
checkStateForReceive();//检查PipedInputStream (消费者)的状态
writeSide = Thread.currentThread();//当前执行该函数的线程,就是生产者线程
int bytesToTransfer = len;//生产者线程要写入到缓冲区(byte[]数组)中的字节总量
while (bytesToTransfer > 0) {
if (in == out)
//如果缓冲区(byte[]数组)的读指针==缓冲区(byte[]数组)的写指针,唤醒所有消费者线程,自己这个生产者线程调用wait(1000)函数
awaitSpace();
int nextTransferAmount = 0;//本次生产者线程要写入到缓冲区(byte[]数组)中的字节数量
if (out < in) {
//如果缓冲区的读指针<缓冲区的写指针,本次要写入到缓冲区(byte[]数组)中的字节数量=缓冲区的长度-缓冲区的写指针
nextTransferAmount = buffer.length - in;
} else if (in < out) {
if (in == -1) {
in = out = 0;
//如果缓冲区的读指针(out)> 缓冲区的写指针(in)并且缓冲区的写指针(in)=-1,先设置缓冲区的读(out)、写(in)指针=0,本次要写入到缓冲区(byte[]数组)中的字节数量=缓冲区的长度
nextTransferAmount = buffer.length - in;
} else {
//如果缓冲区的读指针(out)> 缓冲区的写指针(in)并且缓冲区的写指针(in)=-1,本次要写入到缓冲区(byte[]数组)中的字节数量=读指针(out)-写指针(in)
nextTransferAmount = out - in;
}
}
//本次生产者线程要写入到缓冲区(byte[]数组)中的字节数量最多为len,下次为len-本次写入到缓冲区(byte[]数组)中的字节数量,也就是每次写入的基于len个字节循环递减上一次写入的
if (nextTransferAmount > bytesToTransfer)
nextTransferAmount = bytesToTransfer;
assert(nextTransferAmount > 0);
System.arraycopy(b, off, buffer, in, nextTransferAmount);//向缓冲区(byte[]数组)的[in,in+nextTransferAmount)索引位置写入byte[]数组b中[off,off+nextTransferAmount)索引位置的字节,都是左闭右开。
bytesToTransfer -= nextTransferAmount;//每一次都基于len个字节循环递减本次写入到缓冲区(byte[]数组)中的字节数量nextTransferAmount
off += nextTransferAmount;//将下次要从byte[]数组b中取字节的起始索引的位置(偏移量)+本次写入到缓冲区(byte[]数组)中的字节数量nextTransferAmount
in += nextTransferAmount;//将缓冲区的写指针(in)+本次写入到缓冲区(byte[]数组)中的字节数量nextTransferAmount
if (in >= buffer.length) {
in = 0;//如果缓冲区的写指针(in)> 缓冲区(byte[]数组)的长度,设置缓冲区的写指针(in)=0
}
}
}
//检查PipedInputStream (消费者)的状态
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");
}
}
//如果缓冲区(byte[]数组)的读指针==缓冲区(byte[]数组)的写指针,唤醒所有消费者线程,自己这个生产者线程调用wait(1000)函数
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();
}
}
}
//关闭与这个 PipedInputStream (消费者)相关联的PipedOutputStream(生产者)
synchronized void receivedLast() {
closedByWriter = true;
notifyAll();//唤醒所有消费者线程
}
//线程同步函数:消费者线程每次从缓冲区(byte[]数组)中读取1个字节
public synchronized int read() throws IOException {
if (!connected) {//检查标记符connected,如果为false,抛出IOException
throw new IOException("Pipe not connected");
} else if (closedByReader) {//检查标记符closedByReader,如果为true,抛出IOException
throw new IOException("Pipe closed");
} else if (writeSide != null && !writeSide.isAlive()
&& !closedByWriter && (in < 0)) {
//检查当前这个PipedInputStream (消费者)对象中引用的生产者线程和生产者线程的状态,如果和标记符closedByWriter还有缓冲区(byte[]数组)的写指针(in)不能对应的话,抛出一个IOException
throw new IOException("Write end dead");
}
readSide = Thread.currentThread();//当前执行该函数的线程,就是消费者线程
int trials = 2;//这是一个多次检测的策略变量,防止生产者线程没有关闭了与这个 PipedInputStream (消费者)相关联的PipedOutputStream(生产者)时便抛出IOException
//in=-1的情况有种:
//①、生产者线程还没有向缓冲区(byte[]数组)中写任何字节
//②、消费者线程从缓冲区(byte[]数组)中读完字节(byte)数据以后读指针(out)=写指针(in),那么,当前消费者线程会设置写指针(in)=-1
//③、消费者线程执行PipedInputStream 的close()函数后,关闭了这个 PipedInputStream (消费者)
while (in < 0) {
if (closedByWriter) {
/* closed by writer, return EOF */
return -1;
}
if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) {
//多个消费者线程从缓冲区(byte[]数组)中读的时候,并且前一个消费者线程已经把缓冲区(byte[]数组)中写入的字节读完了,并且前一个线程设置了写指针(in)=-1,生产者线程也关闭了与这个 PipedInputStream (消费者)相关联的PipedOutputStream(生产者)时,抛出一个IOException
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;//获取缓冲区(byte[]数组)中读指针(out)索引位置的字节,并且将读指针(out)+1
if (out >= buffer.length) {
out = 0;//如果读指针(out)>=缓冲区(byte[]数组)的长度,设置读指针(out)=0
}
if (in == out) {
/* now empty */
in = -1;//如果消费者线程从缓冲区(byte[]数组)中读完字节(byte)数据以后读指针(out)=写指针(in),那么,当前消费者线程会设置写指针(in)=-1
}
return ret;
}
//线程同步函数:如果缓冲区(byte[]数组)中有足够多的字节的话(数量>len),消费者线程每次从缓冲区(byte[]数组)中读取len个字节放到byte[]数组b的[off, off+len)索引位置(左闭右开,不包括off+len)
//如果缓冲区(byte[]数组)中字节的数量<len个(比如有in(写指针)-out(读指针)个),消费者线程每次从缓冲区(byte[]数组)中读取(in-out)个字节放到byte[]数组b的[off, off+in-out)索引位置(左闭右开,不包括off+in-out)
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) {//byte[]数组b的[off,off+len)(左闭右开)索引位置是否有越界的检查
throw new IndexOutOfBoundsException();//越界的话,抛出一个IndexOutOfBoundsException
} else if (len == 0) {
return 0;//如果len==0,返回0
}
/* possibly wait on the first character */
int c = read();//先调用read()函数试探性从缓冲区(byte[]数组)中读1个字节
if (c < 0) {
return -1;//如果试探性的从缓冲区(byte[]数组)中都读不到1个字节,返回-1
}
b[off] = (byte) c;//把试探性从缓冲区(byte[]数组)中读到的第1个字节放到byte[]数组b的off索引位置
int rlen = 1;//累计从缓冲区(byte[]数组)中读到的所有字节数量
while ((in >= 0) && (len > 1)) {
int available;//本次执行System.arraycopy()函数可以从缓冲区(byte[]数组)中读到byte[]数组b中的字节数量
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)) {//减掉试探性从缓冲区(byte[]数组)中读到的第1个字节
available = len - 1;
}
System.arraycopy(buffer, out, b, off + rlen, available);
out += available;//读指针(out)+System.arraycopy()函数从缓冲区(byte[]数组)中读到byte[]数组b中的字节数量
rlen += available;//累计从缓冲区(byte[]数组)中读到的所有字节数量 + System.arraycopy()函数从缓冲区(byte[]数组)中读到byte[]数组b中的字节数量
len -= available;//len - System.arraycopy()函数从缓冲区(byte[]数组)中读到byte[]数组b中的字节数量
if (out >= buffer.length) {
out = 0;//如果读指针(out)>=缓冲区(byte[]数组)的长度,设置读指针(out)=0
}
if (in == out) {
/* now empty */
in = -1;//如果消费者线程从缓冲区(byte[]数组)中读完字节(byte)数据以后读指针(out)=写指针(in),那么,当前消费者线程会设置写指针(in)=-1
}
}
return rlen;//返回累计从缓冲区(byte[]数组)中读到的所有字节数量
}
//线程同步函数:返回缓冲区(byte[]数组)中可以被消费者线程读取的字节数量
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;
}
//关闭这个 PipedInputStream (消费者),其实就是设置标记符closedByReader=true, 设置写指针(in)=-1
public void close() throws IOException {
closedByReader = true;
synchronized (this) {
in = -1;
}
}
}
三、1个线程向PipedOutputStream(生产者)写字节数据,1个线程从PipedInputStream(消费者)读取字节数据的过程
3.1、非循环直接写和非循环直接读
package com.chelong.StreamAndReader;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class PipedTest {
public static void main(String[] args) throws IOException {
final PipedOutputStream output = new PipedOutputStream();
final PipedInputStream input = new PipedInputStream(output);
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
output.write("Hello world, pipe!".getBytes());//write()函数是阻塞的
} catch (IOException e) {
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
int data = -1;
while ((data = input.read()) != -1) {//read()函数是阻塞的
System.out.print((char) data);
}
} catch (IOException e) {
}
}
});
thread1.start();
thread2.start();
}
}
程序运行结果,如下所示:
main线程构造PipedOutputStream(生产者)和PipedInputStream(消费者)的过程如下:
向PipedOutputStream(生产者)写字节数据的生产者线程的执行过程如下:
从PipedInputStream(消费者)读取字节数据的消费者线程的执行过程如下:
3.1.1、非循环直接写和非循环直接读时1个生产者线程和1个消费者线程处理数据的过程
Java 语言定义了 6 种线程状态, 在任意一个时间点, 一个线程只能有且只有其中的一种状态, 这 6 种状态分别如下:
这 6 种线程状态的简单介绍,如下所示
JVM运行时内存结构主要包含了五个部分:程序计数器 (PC寄存器)、 JVM栈、Native方法栈、堆、 方法区。如下图所示:
图中红色部分是线程私有区域,进入这个区域的数据不会出现线程竞争的关系。而绿色区域中的数据则被所有线程共享,其中Java堆中存放的是大量对象,方法区中存放class信息、常量、静态变量等数据。
每个线程的线程栈中会存放函数(方法)的描述符,成员(本地)变量等,函数(方法)在线程栈中会通过压栈和弹栈来执行,除了8种(byte、short、int、long、float、double、boolean、char)基本的数据类型存储在线程栈中以外,其余的引用数据类型(对象)都存储在堆中,然后通过引用将堆中的对象和线程栈中的变量关联起来(也可以叫线程栈中的引用指向堆中的对象)。
那么,当使用者执行3.1中的代码时,1个生产者线程和1个消费者线程处理数据的过程如下:
①、main线程初始化一个缓冲区(byte[]数组),长度为1024(默认值),然后生产者线程通过不断的压栈来完成函数之间的调用,最终执行PipedInputStream.class::receive(byte b[], int off, int len)函数来对缓冲区(byte[]数组)进行填充,如下所示:
②、当生产者线程填充完缓冲区之后,写指针变量int in=17,读指针变量int out=0,Thread writeSide = 当前这个生产者线程(Thread)对象,生产者线程会把自己线程栈中修改的变量最终刷新到堆中PipedInputStream对象中,以确保其它消费者线程的线程栈从堆中读取这3个变量时,这3个变量已经为修改后的值,如下所示:
③、消费者线程读缓冲区(byte[]数组)的过程中会不断地执行out++(读指针)以读取缓冲区(byte[]数组)中的可用字节并返回,直到out(读指针)==in(写指针),修改in(写指针)=-1,并且每次同步执行PipedInputStream.class::read()函数时,都会更新Thread readSide = 当前这个消费者线程(Thread)对象,消费者线程也会把自己线程栈中修改的变量最终刷新到堆中PipedInputStream对象中,以确保其它消费者线程的线程栈从堆中读取这3个变量时,这3个变量已经为修改后的值,如下所示:
④、更新in(写指针)=-1后,消费者线程再次同步执行PipedInputStream.class::read()函数时,如果PipedInputStream::boolean closedByWriter变量为true,则会返回-1
3.2、加锁循环写和非加锁循环读到byte[]数组b中再处理
package com.chelong.pipe;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class PipeForTransferInThread {
public static void main(String[] args) throws IOException, InterruptedException {
final PipedOutputStream output = new PipedOutputStream();
final PipedInputStream input = new PipedInputStream(output);
//生产者线程
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
synchronized (input) {
try {
// input.wait();
output.write("Hello world, pipe!".getBytes());
input.wait();//释放锁并无限等待,直到消费者线程consumer 执行notifyAll()函数来唤醒当前阻塞
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
},"生产者线程");
//消费者线程
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] b = new byte[1024];//1KB
int readBytes = -1;
long lastTime = System.currentTimeMillis();
while ((readBytes = input.read(b, 0, b.length)) != -1) {
long curTime = System.currentTimeMillis();
System.out.print(Thread.currentThread().getName()+"本次读取花费时间:" + (curTime - lastTime) + "ms,读到的数据是:");
lastTime = curTime;
for (int i = 0; i < readBytes; i++) {
System.out.print((char) b[i]);//模拟处理字节数据
}
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
}
}
},"消费者线程");
producer.start();//生产者线程启动
consumer.start();//消费者线程启动
}
}
程序运行结果,如下所示:
main线程构造PipedOutputStream(生产者)和PipedInputStream(消费者)的过程可以参考3.1;
向PipedOutputStream(生产者)写字节数据的生产者线程的执行过程可以参考3.1;
从PipedInputStream(消费者)读取字节数据的消费者线程的执行过程如下:
3.2.1、加锁循环写和非加锁循环读到byte[]数组b中再处理时1个生产者线程和1个消费者线程处理数据的过程
标题3.2中的代码的整个执行过程如下:
①、main线程初始化一个缓冲区(byte[]数组),长度为1024(默认值),如下所示:
②、然后生产者线程通过不断的压栈来完成函数之间的调用,最终执行PipedInputStream.class::receive(byte b[], int off, int len)函数来对缓冲区(byte[]数组)进行填充,并且先在自己的线程栈中更新in(写指针)=17,out(读指针)=0,writeSide=当前这个生产者线程(Thread)对象 如下所示:
当生产者线程对缓冲区(byte[]数组)填充完成之后,再执行标题3.2中的代码
input.wait();
这行代码会释放锁并让生产者线程进入无限等待,直到消费者线程consumer执行notifyAll()函数来唤醒当前这个生产者线程。在这之前,生产者线程会将自己线程栈中的in(写指针)=17,out(读指针)=0,writeSide=当前这个生产者线程,这3个变量更新到主内存(也就是堆)中的PipedInputStream对象中。
③、消费者线程读缓冲区(byte[]数组)的过程也是通过不断的压栈来完成函数之间的调用,最终执行PipedInputStream::read()函数(试探性的读取1个字节)和PipedInputStream::read(byte b[], int off, int len)函数(读取剩余其它的字节)将步骤②中生产者线程写入到缓冲区(byte[]数组)中的17个字节读取出来
附言:最终消费者线程也会将自己线程栈中的in(写指针)= -1,out(读指针)= 17,writeSide=当前这个消费者线程,这3个变量更新到主内存(也就是堆)中的PipedInputStream对象中。
因此,本次消费者线程从缓冲区(byte[]数组)中读数据的过程中没有执行read()函数中的wait(1000)这一行代码,如下:
所以,本次消费者线程从缓冲区(byte[]数组)中读取数据到消费者线程中自己创建的byte[]数组中时,只花费了0ms:
接下来,当消费者线程将步骤②中生产者线程写入到缓冲区(byte[]数组)中的17个字节读取出来以后(通过System.arraycopy()函数复制到了消费者线程中自己创建的byte[]数组中),消费者线程会遍历从缓冲区读到的这个byte[]数组,来处理这些数据,如下所示(标题3.2中的代码片段):
//标题3.2中的代码片段
for (int i = 0; i < readBytes; i++) {
System.out.print((char) b[i]);//模拟处理字节数据
}
然后,当消费者线程再次执行
//标题3.2中的代码片段
input.read(b, 0, b.length)
从缓冲区(byte[]数组)中读数据到自己创建的byte[]数组中时,由于此时in(写指针)=-1,并且当下图中的其它5个条件都不成立时,唤醒执行了
input.wait()
的生产者线程,然后当前这个正在从缓冲区(byte数组)中读数据的消费者线程执行wait 1000ms ,如下:
④、当生产者线程被消费者线程执行的
notifyAll();
唤醒之后,会再次通过不断的压栈来完成函数之间的调用,再次执行PipedInputStream.class::receive(byte b[], int off, int len)函数来对缓冲区(byte[]数组)进行填充,并且先在自己的线程栈中先更新in(写指针)=17,out(读指针)=0,writeSide=当前这个生产者线程(Thread)对象 如下所示:
当生产者线程对缓冲区(byte[]数组)填充完成之后,再执行标题3.2中的代码
input.wait();
这行代码会释放锁并让生产者线程进入无限等待,直到消费者线程consumer执行notifyAll()函数来唤醒当前这个生产者线程。在这之前,生产者线程会将自己线程栈中的in(写指针)=17,out(读指针)=0,writeSide=当前这个生产者线程,这3个变量更新到主内存(也就是堆)中的PipedInputStream对象中。如下所示:
⑤、消费者线程在第③步执行了
wait(1000);
在等待了1000ms之后,消费者线程会自动唤醒继续执行,此时自己线程栈中的in(写指针)= -1,out(读指针)= 17已经被第④步中的生产者线程修改为in(写指针)=17,out(读指针)=0(生产者线程不会直接修改消费者线程栈中的变量,生产者线程会先将自己线程栈中in(写指针),out(读指针)变量的值修改到主内存中,然后消费者线程会自己将主内存中的这2个变量值刷新到消费者自己的线程栈中),如下所示:
然后执行PipedInputStream::read()函数(试探性的读取1个字节)和PipedInputStream::read(byte b[], int off, int len)函数(读取剩余其它的字节)将步骤④中生产者线程写入到缓冲区(byte[]数组)中的17个字节读取出来
附言:最终消费者线程也会将自己线程栈中的in(写指针)= -1,out(读指针)= 17,writeSide=当前这个消费者线程,这3个变量更新到主内存(也就是堆)中的PipedInputStream对象中。
由于,本次消费者线程从缓冲区(byte[]数组)中读数据的过程是从步骤③中自动唤醒继续执行的,所以,本次消费者线程从缓冲区(byte[]数组)中读取数据到消费者线程中自己创建的byte[]数组中时,花费了1015ms:
接下来,当消费者线程将步骤④中生产者线程写入到缓冲区(byte[]数组)中的17个字节读取出来以后(通过System.arraycopy()函数复制到了消费者线程中自己创建的byte[]数组中),消费者线程会遍历从缓冲区读到的这个byte[]数组,来处理这些数据,如下所示(标题3.2中的代码片段):
//标题3.2中的代码片段
for (int i = 0; i < readBytes; i++) {
System.out.print((char) b[i]);//模拟处理字节数据
}
然后,当消费者线程再次执行
//标题3.2中的代码片段
input.read(b, 0, b.length)
从缓冲区(byte[]数组)中读数据到自己创建的byte[]数组中时,由于此时in(写指针)=-1,并且当下图中的其它5个条件都不成立时,唤醒执行了
input.wait()
的生产者线程,然后当前这个正在从缓冲区(byte[]数组)中读数据的消费者线程执行wait 1000ms ,如下:
⑥、当生产者线程被消费者线程执行的
notifyAll();
唤醒之后,会再次通过不断的压栈来完成函数之间的调用,再次执行PipedInputStream.class::receive(byte b[], int off, int len)函数来对缓冲区(byte[]数组)进行填充,并且先在自己的线程栈中先更新in(写指针)=17,out(读指针)=0,writeSide=当前这个生产者线程(Thread)对象 如下所示:
当生产者线程对缓冲区(byte[]数组)填充完成之后,再执行标题3.2中的代码
input.wait();
这行代码会释放锁并让生产者线程进入无限等待,直到消费者线程consumer执行notifyAll()函数来唤醒当前这个生产者线程。在这之前,生产者线程会将自己线程栈中的in(写指针)=17,out(读指针)=0,writeSide=当前这个生产者线程,这3个变量更新到主内存(也就是堆)中的PipedInputStream对象中。如下所示:
⑦、消费者线程在第⑤步执行了
wait(1000);
在等待了1000ms之后,消费者线程会自动唤醒继续执行,此时自己线程栈中的in(写指针)= -1,out(读指针)= 17已经被第⑥步中的生产者线程修改为in(写指针)=17,out(读指针)=0(生产者线程不会直接修改消费者线程栈中的变量,生产者线程会先将自己线程栈中in(写指针),out(读指针)变量的值修改到主内存中,然后消费者线程会自己将主内存中的这2个变量值刷新到消费者自己的线程栈中),然后执行PipedInputStream::read()函数(试探性的读取1个字节)和PipedInputStream::read(byte b[], int off, int len)函数(读取剩余其它的字节)将步骤⑥中生产者线程写入到缓冲区(byte[]数组)中的17个字节读取出来
附言:最终消费者线程也会将自己线程栈中的in(写指针)= -1,out(读指针)= 17,writeSide=当前这个消费者线程,这3个变量更新到主内存(也就是堆)中的PipedInputStream对象中。
由于,本次消费者线程从缓冲区(byte[]数组)中读数据的过程是从步骤⑤中自动唤醒继续执行的,所以,本次消费者线程从缓冲区(byte[]数组)中读取数据到消费者线程中自己创建的byte[]数组中时,花费了1017ms:
接下来,当消费者线程将步骤⑥中生产者线程写入到缓冲区(byte[]数组)中的17个字节读取出来以后(通过System.arraycopy()函数复制到了消费者线程中自己创建的byte[]数组中),消费者线程会遍历从缓冲区读到的这个byte[]数组,来处理这些数据,如下所示(标题3.2中的代码片段):
//标题3.2中的代码片段
for (int i = 0; i < readBytes; i++) {
System.out.print((char) b[i]);//模拟处理字节数据
}
然后,当消费者线程再次执行
//标题3.2中的代码片段
input.read(b, 0, b.length)
从缓冲区(byte[]数组)中读数据到自己创建的byte[]数组中时,由于此时in(写指针)=-1,并且当下图中的其它5个条件都不成立时,唤醒执行了
input.wait()
的生产者线程,然后当前这个正在从缓冲区(byte[]数组)中读数据的消费者线程执行wait 1000ms ,如下:
⑧、当生产者线程被消费者线程执行的
notifyAll();
唤醒之后,会跳出for循环,结束生产者线程的生命周期,之后,该线程对象会被操作系统回收。
⑨、消费者线程在第⑦步执行了
wait(1000);
在等待了1000ms之后,消费者线程会自动唤醒继续执行,此时自己线程栈中的in(写指针)= -1,out(读指针)= 17,并且从
wait(1000);
的代码之后,继续执行,执行过程如下(从下图的紫色流程继续执行):
在执行了2个循环后,直到int trials = 0时,执行到判断(writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)这个条件时就会为true(下图的红色流程)
然后,抛出了一个IOException("Pipe broken"),因此,可以得出int trials变量的含义:这个变量是一个多次检测的策略变量,当生产者线程没有关闭了与这个 PipedInputStream (消费者)相关联的PipedOutputStream(生产者)时,并且writeSide变量指向的当前生产者线程已经被操作系统回收时(此时当前生产者线程对象的isAlive()函数会返回false),消费者线程会抛出1个IOException("Pipe broken"),并结束while循环,进而结束消费者线程的生命周期。之后,该线程对象也会被操作系统回收。如下图所示:
3.2.2、怎样防止3.2.1中第⑨步的生产者线程抛出IOException("Pipe broken")
回顾3.2.1中第⑨步中的消费者线程抛出IOException("Pipe broken")的产生过程:当执行到判断(writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)这个条件时就会为true(下图的红色流程)
那么,使用者就可以将上图中红色流程的前一步变成true即可,如下代码所示(只修改了生产者线程中的代码,消费者线程中的代码没有变化):
package com.chelong.pipe;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public static void main(String[] args) throws IOException, InterruptedException {
final PipedOutputStream output = new PipedOutputStream();
final PipedInputStream input = new PipedInputStream(output);
//生产者线程
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 3; i++) {
synchronized (input) {
// input.wait();
output.write("Hello world, pipe!".getBytes());
input.wait();//释放锁并无限等待,直到消费者线程thread2执行notifyAll()函数来唤醒当前阻塞
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (output != null) output.close();//调用close()函数关闭生产者对象
} catch (IOException e) {
e.printStackTrace();
}
}
}
}, "生产者线程");
//消费者线程
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] b = new byte[1024];//1KB
int readBytes = -1;
long lastTime = System.currentTimeMillis();
while ((readBytes = input.read(b, 0, b.length)) != -1) {
long curTime = System.currentTimeMillis();
System.out.print(Thread.currentThread().getName() + "本次读取花费时间:" + (curTime - lastTime) + "ms,读到的数据是:");
lastTime = curTime;
for (int i = 0; i < readBytes; i++) {
System.out.print((char) b[i]);//模拟处理字节数据
}
System.out.println();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}, "消费者线程");
producer.start();//生产者线程启动
consumer.start();//消费者线程启动
}
}
程序运行结果,如下所示:
通过PipedOutputStream.class::close()的源码可以看到这样修改后消费者线程不再抛出IOException("Pipe broken")原因:
PipedOutputStream.class(生产者类)的源码
package java.io;
import java.io.*;
public
class PipedOutputStream extends OutputStream {
...省略部分代码...
//关闭这个PipedOutputStream(生产者),这个PipedOutputStream(生产者)不能再向与它相关联的PipedInputStream(消费者)中的缓冲区(byte[]数组)写入字节数据
public void close() throws IOException {
if (sink != null) {
sink.receivedLast();//调用PipedInputStream.class::receivedLast()函数
}
}
}
PipedInputStream .class(消费者类)的源码
package java.io;
public class PipedInputStream extends InputStream {
//标记符:true表示与这个 PipedInputStream (消费者)相关联的PipedOutputStream(生产者)已经关闭,反之,反之
boolean closedByWriter = false;
...省略部分代码...
//关闭与这个 PipedInputStream (消费者)相关联的PipedOutputStream(生产者)
synchronized void receivedLast() {
closedByWriter = true;//关闭后消费者再从缓冲区(byte[])数组中读取字节数据时,会返回-1,不会抛出IOException了
notifyAll();//唤醒所有消费者线程
}
...省略部分代码...
四、多个线程向PipedOutputStream(生产者)写字节数据,多个线程从PipedInputStream(消费者)读取字节数据的过程
略(待补充)