Java IO 之 管道流 原理分析

概述

管道流是用來在多個線程之間進行信息傳遞的Java流。
管道流分爲字節流管道流和字符管道流。
字節管道流:PipedOutputStream 和 PipedInputStream。
字符管道流:PipedWriter 和 PipedReader。
PipedOutputStream、PipedWriter 是寫入者/生產者/發送者;
PipedInputStream、PipedReader 是讀取者/消費者/接收者。

字節管道流

這裏我們只分析字節管道流,字符管道流原理跟字節管道流一樣,只不過底層一個是 byte 數組存儲 一個是 char 數組存儲的。

java的管道輸入與輸出實際上使用的是一個循環緩衝數來實現的。輸入流PipedInputStream從這個循環緩衝數組中讀數據,輸出流PipedOutputStream往這個循環緩衝數組中寫入數據。當這個緩衝數組已滿的時候,輸出流PipedOutputStream所在的線程將阻塞;當這個緩衝數組爲空的時候,輸入流PipedInputStream所在的線程將阻塞。

注意事項

在使用管道流之前,需要注意以下要點:
* 管道流僅用於多個線程之間傳遞信息,若用在同一個線程中可能會造成死鎖;
* 管道流的輸入輸出是成對的,一個輸出流只能對應一個輸入流,使用構造函數或者connect函數進行連接;
* 一對管道流包含一個緩衝區,其默認值爲1024個字節,若要改變緩衝區大小,可以使用帶有參數的構造函數;
* 管道的讀寫操作是互相阻塞的,當緩衝區爲空時,讀操作阻塞;當緩衝區滿時,寫操作阻塞;
* 管道依附於線程,因此若線程結束,則雖然管道流對象還在,仍然會報錯“read dead end”;
* 管道流的讀取方法與普通流不同,只有輸出流正確close時,輸出流才能讀到-1值。

示例

public class PipedStreamDemo {
    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 {
            //shutdown 後可能正在運行的線程很長時間都運行不完成,這裏設置超過1小時,強制執行 Interruptor 結束線程。
            executorService.awaitTermination(1, TimeUnit.HOURS);
        } 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 = "hello world, amazing java !";
                System.out.println("Sender:" + s);
                byte[] buf = s.getBytes();
                pos.write(buf, 0, buf.length);
                pos.close();
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception 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();
            }
        }
    }
}

輸出結果:

源碼分析

因爲數據是從 PipedOutputStream 寫入,然後通過 PipedInputStream 讀取的,所以下面我們先來分析下 生產者 PipedOutputStream 的源碼。

PipedOutputStream 源碼分析

初始化


1、定義了一個 PipedInputStream 成員變量 sink。用來保存需要寫入到的目標管道流中。
2、一個代參數的構造,一個無參的構造。
* 有參的構造調用 connect() 方法把兩個管道流連接在一起,
* 無參的構造函數更靈活,不必在創建一個 PipedOutputStream 的對象時指定 PipedInputStream 對象,可以在後面代碼,自己調用 connect() 自己指定。使用方式如下:

write 方法

write 方法就是調用 PipedInputStream的 receive 的方法,把要寫入的數據寫入進去。

PipedOutputStream 總結

通過源碼分析,發現該類沒有什麼特別的,通過構造或者 connect() 方法接收一個 PipedInputStream對象,然後把要輸出信息,交給 PipedInputStream.receive() 方法去接收。

PipedInputStream 源碼分析

打開該類後發現比 PipedInputStream 類複雜了好多。

類結構


PipedInputStream 中定義了很多成員變量

1、closedByWriter 是否關閉 PipedOutputStream 流。
2、closedByReader 是否關閉 PipedInputStream 流。
3、connected 輸入輸出管道流是否成功連接了。
4、readSide、writeSide 讀線程和寫線程
5、DEFAULT_PIPE_SIZE 默認讀寫的緩衝區大小爲 1024.
6、PIPE_SIZE 對外暴露管道流的讀寫緩衝區大小(當前包可見)
7、buffer 緩衝區大小
8、in 寫入緩衝區下標
9、out 寫出緩衝區下標

PipedInputStream 構造及初始化

  • PipedInputStream 支持有4種構造方法。
    1、public PipedInputStream(PipedOutputStream src)
    傳入一個 PipedOutputStream 參數,並調用 initPipe() 方法創建默認大小(1024)的 buffer。
    2、public PipedInputStream(PipedOutputStream src, int pipeSize)
    傳入一個 PipedOutputStream 參數和 pipeSize參數,調用 initPipe() 方法創建指定大小的 buffer
    3、public PipedInputStream()
    調用 initPipe() 方法,創建一個默認大小的buffer
    4、public PipedInputStream(int pipeSize)
    調用 initPipe() 方法,創建一個指定大小的buffer

  • initPipe 方法
    private void initPipe(int pipeSize)
    根據 pipeSize 創建 buffer 。

  • connect 方法
    public void connect(PipedOutputStream src)
    connect方法其實還是調用的 PipedOutputStream 類種的 connect 方法。
    所以下面這樣寫法,是等價的,都是調用 PipedOutputStream 類種的 connect 方法。

receive 方法


通過分析 PipedOutputStream 的源碼,我們知道,該方法是在 PipedOutputStream.write() 方法種調用的。
* 1、checkStateForReceive()檢查是否可以接受數據。(是否可向 buffer 種寫入數據);
* 2、獲取寫線程。PipedOutputStream.write() 中調用的,所以獲取的是PipedOutStream 所在的線程;
* 3、判斷 in==out。如果相等說明,已經緩衝區已經被填充滿數據了。這時調用 awaitSpace() 方法,喚醒讀線程(讀線程可能 wait 狀態),讓當前線程 wait ,如果沒有讀線程喚醒寫線程,那麼寫線程會在 awaitSpace() 方法種每隔1秒檢查一次是否可寫;

爲什麼 in == out 的時候就是寫滿緩衝區呢?
比如: buffer 長度爲10,現在寫了5個字節,又讀了5個字節,是不是 in 也等於 out?
其實不會的,爲什麼?
因爲讀的時候如果 in==out時,他把 in 的值置爲了 -1。詳見 read() 方法。
* 4、如果 in<0,就是第一次寫或者已經讀完 buffer 中已寫的數據,這是,把 in 和 out 置爲0;
* 5、向buffer 種寫入數據。
* 6、如果 in 達到 buffer 的最大長度,則把in 置爲 0, 下次開始從0 開始填充。(這裏,可以把 buffer 當成一個環形隊列)。

awaitSpace() 源碼

read() 方法

1、執行各種檢查,是否可讀。
2、獲取讀線程並賦值給 readSide 變量。
3、while 循環監聽判斷是否有寫線程寫數據,如果沒有則等待(每秒檢查一次),並喚醒寫線程(寫線程可能 wait )。
4、讀取 buffer 中的數據。 如果讀到 buffer 的最後一個元素,則把 out 置爲0,下次從下標0開始繼續讀(循環隊列表)。
5、如果 in == out,則把 in 置爲 -1 。置爲初始狀態。相當於清空了緩衝區,從緩衝區的下標 0 開始讀寫。

available() 方法

獲取當前可讀的字節數

1、如果 in<0; 說明當前沒有可讀的數據
2、如果 in == out; 說明數據已經填充滿了。
3、如果 in > out; 那麼in - out 就是 可寫的字節數。
4、否則,就是 in < out 的情況。因爲它是環形寫入的,可能出現 in < out 的情況,所以需要 in + buffer.length - out,才能獲取可讀字節長度。

PipedInputStream 總結

PipedInputStream 原理其實也很簡單,但代碼看起來有點懵,它就是通過 wait() 和 notifyAll() 來控制 buffer 是否可讀,或可寫的。

管道流,做開發這麼多年,現在都沒有遇到可用的場景。管道流能用到的場景,在併發包種,很多方式都可以實現或代替。比如 java.util.concurrent.Exchanger 類。
java.util.concurrent.Exc
hanger 的使用場景比管道流使用場景更廣泛些。


想了解更多精彩內容請關注我的公衆號

本人簡書blog地址:http://www.jianshu.com/u/1f0067e24ff8    
點擊這裏快速進入簡書

GIT地址:http://git.oschina.net/brucekankan/
點擊這裏快速進入GIT

發佈了132 篇原創文章 · 獲贊 79 · 訪問量 40萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章