【原创】从源码剖析IO流(四)管道流--转载请注明出处

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/qq_35427785/article/details/81838417

一、管道流的特点与作用:

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;
    }

四、总结:

管道流的输入与输出必须存在于两个不同的线程中进行使用,在进行管道的输入流输入时,如果缓存区已经写满,却一直没有被读取,则会造成当前线程的死锁。如果在进行从缓存区读取的时候,如果先执行了读取操作,进行读取操作的线程会认为目前还没有进行写入,会在当前线程进行等待写入管道流,同样会造成死锁的情况发生。

猜你喜欢

转载自blog.csdn.net/qq_35427785/article/details/81838417