管道流是用来在多个线程之间进行信息传递的Java流,被号称是最难使用的流,被使用的频率也非常低。但事实上,管道流是非常有用的,它提供了多线程间信息传输的一种有效手段。
管道流包括两个类PipedOutputStream和PipedInputStream。其中PipedOutputStream是写入者/生产者/发送者;PipedInputStream是读取者/消费者/接收者。在使用管道流之前,需要注意以下要点:
第一,管道流仅用于多个线程之间传递信息,若用在同一个线程中可能会造成死锁;
第二,管道流的输入输出是成对的,一个输出流只能对应一个输入流,使用构造函数或者connect函数进行连接;
第三,一对管道流包含一个缓冲区,其默认值为1024个字节,若要改变缓冲区大小,可以使用带有参数的构造函数;
第四,管道的读写操作是互相阻塞的,当缓冲区为空时,读操作阻塞;当缓冲区满时,写操作阻塞;
第五,管道依附于线程,因此若线程结束,则虽然管道流对象还在,仍然会报错“read dead end”;
第六,管道流的读取方法与普通流不同,只有输出流正确close时,输出流才能读到-1值。
源代码一:在线程Sender中向管道流中写入一个字符串,写入后关闭该管道流;在线程Reciever中读取该字符串。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class PipedStreamExam1 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
//创建管道流
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
//创建线程对象
Sender sender = new Sender(pos);
Reciever reciever = new Reciever(pis);
//运行子线程
executorService.execute(sender);
executorService.execute(reciever);
} catch (IOException e) {
e.printStackTrace();
}
//等待子线程结束
executorService.shutdown();
try {
executorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class Sender extends Thread {
private PipedOutputStream pos;
public Sender(PipedOutputStream pos) {
super();
this.pos = pos;
}
@Override
public void run() {
try {
String s = "This is a good day. 今天是个好天气。";
System.out.println("Sender:" + s);
byte[] buf = s.getBytes();
pos.write(buf, 0, buf.length);
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class Reciever extends Thread {
private PipedInputStream pis;
public Reciever(PipedInputStream pis) {
super();
this.pis = pis;
}
@Override
public void run() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while ((len = pis.read(buf)) != -1) {
baos.write(buf, 0, len);
}
byte[] result = baos.toByteArray();
String s = new String(result, 0, result.length);
System.out.println("Reciever:" + s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意,若管道流没有关闭,则使用这种方法读取管道中的信息会报错:
while ((len = pis.read(buf)) != -1) {
baos.write(buf, 0, len);
}
错误代码为java.io.IOException: Write end dead;发生这个错误的原因在于,由于管道未关闭,所以read语句不会读到-1,因此PipedInputStream会持续从管道中读取数据,但是因为Sender线程已经结束,所以会抛出“Write end dead”异常。
管道流之所以难用,是因为很多情况下写入管道的数据难以区分“长度”,它的设计理念是“通过管道,将源数据源源不绝的发送到目的地”。因此,如果应用场景为“通过管道,将一段一段的数据一次次的发送到目的地”,就会发现很难使用。为此,使用Java多线程中的信号量来进行同步可以很好的满足此需求。
源代码二:在线程Sender中反复写入多个字符串,在Reciever中多次接收字符串;使用两个信号量Semaphore来控制写入和读取。
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class PipedStreamExam2 {
//控制读取的信号量,初始状态为0
public static Semaphore readSignal = new Semaphore(0,true);
//控制写入的信号量,初始状态为1,表示允许一次写入
public static Semaphore writeSignal = new Semaphore(1,true);
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
Sender sender = new Sender(pos);
Reciever reciever = new Reciever(pis);
executorService.execute(sender);
executorService.execute(reciever);
} catch (IOException e) {
e.printStackTrace();
}
executorService.shutdown();
}
static class Sender extends Thread {
private PipedOutputStream pos;
public Sender(PipedOutputStream pos) {
super();
this.pos = pos;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
Thread.sleep(new Random().nextInt(1000));
//获取写入信号量
writeSignal.acquire();
String content = "today is a good day. 今天是个好天气:"+i;
System.out.println("Sender:" + content);
pos.write(content.getBytes("utf-8"));
//释放读取信号量
readSignal.release();
}
pos.close();
readSignal.release();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Reciever extends Thread {
private PipedInputStream pis;
public Reciever(PipedInputStream pis) {
super();
this.pis = pis;
}
@Override
public void run() {
try {
byte[] buf = new byte[1024];
int len = 0;
String s;
while(true) {
//获取读取信号量
readSignal.acquire();
len = pis.read(buf);
if(len == -1)
break;
s = new String(buf, 0, len, "utf-8");
System.out.println("Reciever:" + s);
//释放写入信号量
writeSignal.release();
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
经过思考,我认为管道流应用的经典场景应该是将某个输入流从一个线程通过管道发送到另一个线程进行处理,从而提升程序效率。例如:线程A负责从网络上持续读取信息,线程B负责处理信息,那么线程A就会将读取的信息通过管道流发送至线程B,从而确保线程A的读取性能。
下面的例子中,Sender线程从文件中读取未知长度的字节流,然后交给Reciever线程,Reciever线程将此字节流存入另一个文件:
import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class PipedStreamExam3 {
public static void main(String[] args) {
try {
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream(pis);
Sender sender = new Sender(pos);
Reciever reciever = new Reciever(pis);
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(sender);
executorService.execute(reciever);
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class Sender extends Thread {
private PipedOutputStream pos = null;
public Sender(PipedOutputStream pos) {
this.pos = pos;
}
@Override
public void run() {
try {
FileInputStream fis = new FileInputStream("d:\\input.txt");
byte[] buf = new byte[1024];
int len = 0;
while ((len = fis.read(buf)) != -1) {
pos.write(buf, 0, len);
}
pos.flush();
pos.close();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class Reciever extends Thread {
private PipedInputStream pis = null;
public Reciever(PipedInputStream pis) {
this.pis = pis;
}
@Override
public void run() {
try {
FileOutputStream fos = new FileOutputStream("d:\\output.txt");
byte[] buf = new byte[1024];
int len = 0;
while ((len = pis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fos.flush();
fos.close();
pis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
比起文件流、字节数组流和对象流这样使用很普遍的流,管道流有很多不同之处。首先它必须依附线程对象,当线程对象已经失效而流未关闭时会出错;其次它往往读不到-1,因此在很多场景中需要程序员自己来保证同步;第三管道流能够保证良好的互斥,这往往是很有用的一点。多加练习,管道流大有用武之地。
本系列的其他文章
Java流编程实例之一–Java中的字节与字符
Java流编程实例之二–文件流
Java流编程实例之三–字节数组流和字符数组流