Java IO - PipedOutputStream & PipedInputStream

本文介绍了Java中的PipedOutputStream和PipedInputStream,它们用于创建通信管道。通常由两个线程分别操作输入输出流,一个写入数据,另一个读取。PipedInputStream和PipedOutputStream之间只能单向通信,每次只能与一个管道流通信。文章详细探讨了它们的实例用法、限制以及源码解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本概念

  • 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();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oxf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值