Java IO原始碼分析(三)——PipedOutputStream和PipedInputStream

lippon發表於2020-12-11

簡介

PipedOutputStream和PipedInputStream主要用於執行緒之間的通訊 。二者必須配合使用,也就是一段寫入,另一端接收。本質上也是一箇中間快取區,講資料快取在PipedInputStream的陣列當中,等待PipedOutputStream的讀取。
PipedInputStream的緩衝區中迴圈緩衝的思想很有意思。

PS:雖然這個也叫管道,但是這和程式之間的管道通訊沒有任何關係。這裡的管道流是基於Java使用者層的程式碼實現的,而經常通訊是基於核心態的程式的通訊。

原始碼分析

PipedOutputStream

public
class PipedOutputStream extends OutputStream {
	// 需要傳入的輸入流
    private PipedInputStream sink;
	// 輸入輸出流連線的構造
    public PipedOutputStream(PipedInputStream snk)  throws IOException {
        connect(snk);
    }
	// 預設建構函式
    public PipedOutputStream() {
    }
	// 連線輸入輸出流
    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;
    }

	// 將一個int型別資料寫入到輸出流,這裡就會將它傳給輸入流
    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();
        }
    }
}

PipedInputStream

public class PipedInputStream extends InputStream {
	// 輸出流是否被關閉
    boolean closedByWriter = false;
    // 輸入流是否被關閉,這裡修飾了volatile
    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;

	// 傳入輸出流的構造
    public PipedInputStream(PipedOutputStream src) throws IOException {
        this(src, DEFAULT_PIPE_SIZE);
    }

	// 傳入輸出流和管道快取大小的構造
    public PipedInputStream(PipedOutputStream src, int pipeSize)
            throws IOException {
         initPipe(pipeSize);
         connect(src);
    }

	// 預設構造
    public PipedInputStream() {
        initPipe(DEFAULT_PIPE_SIZE);
    }

	// 傳入管道大小的構造
    public PipedInputStream(int pipeSize) {
        initPipe(pipeSize);
    }
	
	// 初始化快取區陣列
    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);
    }

	// 接收一個位元組,同步的
    protected synchronized void receive(int b) throws IOException {
    	// 檢測管道的狀態
        checkStateForReceive();
        // 讀取當前寫入執行緒
        writeSide = Thread.currentThread();
        // 寫入指標等於讀取指標,說明緩衝區滿了,通知其他讀執行緒儘快來讀
        // 當前執行緒會進入等待狀態
        if (in == out)
            awaitSpace();
        // 輸出流的寫入位置小於0
        if (in < 0) {
            in = 0;
            out = 0;
        }
        // 寫入位元組,只取低八位
        buffer[in++] = (byte)(b & 0xFF);
        // 迴圈緩衝指標復位
        if (in >= buffer.length) {
            in = 0;
        }
    }

	// 寫入一堆
    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;
            } 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);
            System.arraycopy(b, off, buffer, in, nextTransferAmount);
            bytesToTransfer -= nextTransferAmount;
            off += nextTransferAmount;
            in += nextTransferAmount;
            if (in >= buffer.length) {
                in = 0;
            }
        }
    }

	// 判斷連線狀態
    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");
        }
    }
	
	// 讀完了資料,等待寫執行緒繼續寫資料
    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();
            }
        }
    }

	// 當輸出流被關閉的時候使用
    synchronized void receivedLast() {
        closedByWriter = true;
        notifyAll();
    }

	// 讀入一個位元組
    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");
            }
            // 等待寫入執行緒寫入
            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;
    }

	// 寫入到位元組陣列當中
    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;

			// 其實這裡就是一個迴圈緩衝的剩餘緩衝長度計算了,當寫指標超出了緩衝區的長度,就會回到-1,計算長度的方式就不同了
            if (in > out) {
            	// 寫指標沒超,那麼長度就應該就是寫指標位置減去讀指標,這裡取小是反之陣列越界
                available = Math.min((buffer.length - out), (in - out));
            } else {
            	// 寫指標超了,回到了-1,那麼剩餘長度就是陣列長度減去讀指標
                available = buffer.length - out;
            }

            // 防止陣列與越界
            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 available() throws IOException {
        if(in < 0)
        	// 當in == -1說明剛被讀完或者剛初始化,緩衝區沒有資料
            return 0;
        else if(in == out)
        	// 只有緩衝區被寫滿的時候,二者才會相等,說明緩衝區的資料滿了,如果是被讀完,in會被置-1
            return buffer.length;
        else if (in > out)
        	// 還有資料
            return in - out;
        else
        	// 迴圈緩衝,in在out後面,說明in已經跑完一圈了
            return in + buffer.length - out;
    }

	// 關閉管道
    public void close()  throws IOException {
        closedByReader = true;
        synchronized (this) {
            in = -1;
        }
    }
}

總結

PipedOutputStream特點

  • 本質就是呼叫PipedInputStream的介面,將資料寫進PipedInputStream的緩衝區當中。
  • 一個輸出只能一個輸入連線。
  • 和之前的ByteArrayInputStream 一樣,操作的資料都是位元組型別。

PipedInputStream特點

  • 內部主要由緩衝陣列、讀指標和寫指標構成。
  • 由於這兩個流是用於執行緒之間通訊,所以他們是需要保證執行緒安全的,他們對外的函式都是有同步鎖修飾的,同時只能有一個執行緒進行讀取獲取寫入,其實效率不高。
  • 當生產者寫入的時候發現緩衝區滿了,就會進入等待狀態,等待消費者消費資料,再將他們喚醒。
  • 當消費者讀取資料的時候發現緩衝區是空的,那麼就會進入等待,等待生產者寫入資料,再將他們喚醒。

緩衝區特點

  • 緩衝區其實採用的是一個迴圈緩衝的形式,在讀取資料的時候,讀取的是讀指標當前的位置,讀一個增加一個,但是當讀指標和寫指標相同的時候,in就會被置為-1,這是為了後面緩衝區資料滿的時候,讀指和寫指標的相同的情況進行區分,也就是說,讀指標和寫指標相等的時候,就是資料滿的時候;當讀指標超出了緩衝區陣列邊界,那麼就會被置為0,這樣往復就是迴圈緩衝的思想。
  • 當資料寫入的時候,資料就會寫入寫指標的位置,當寫指標超出了陣列邊界,就會被置為0;當寫指標等於讀指標,說明寫指標已經超圈了,那麼快取區的可用長度就是整個緩衝區的大小,不能再超過讀指標,不然會被理解為可用長度是大於的那一部分。

緩衝區有效資料長度的情況如下圖所示:

在這裡插入圖片描述

相關文章