Java nio都是非阻塞IO麼?並非如此 一段代碼引發的思考 Channels&Buffers Selector和非阻塞IO

java中的nio包,對於java程序員來說是個熟悉又陌生的東西。以前一直以爲nio=Non-blocking I/O,即非阻塞IO。後來又聽人說nio其實是new IO新一代IO的意思。兩種說法到底哪種是正確的?我去Oracle的java官網查看doc,很遺憾也沒直接解釋nio是什麼單詞的縮寫。但是經過一番實踐,確證new IO纔是nio正確的全稱。

一段代碼引發的思考

    ByteBuffer buf = ByteBuffer.allocate(48);
    buf.clear();

    channelA.read(buf);

    buf.flip();

    while(buf.hasRemaining()) {
        channelB.write(buf);
    }

這是在網上搜FileChannel看到的一段代碼。這個時候我以爲nio下的一切io操作都是非阻塞的,於是看到這段代碼我就非常費解。
爲什麼FileChannel在read的時候沒有加while判斷,在write的時候就要加while判斷了?難道說對FileChannel來說read是線程阻塞操作,write是非阻塞的?這就有點匪夷所思了。看到doc文檔也沒找到答案。

於是我自己試驗了一下,在while循環裏添加日誌,如果write是非阻塞的,那麼while應該打印很多次。最後的結果是while裏只打印一次日誌,代表read和write都是阻塞的!
這時候其實我更迷惑了,因爲我一直以爲nio下的io操作都是非阻塞的。
於是再次去百度FileChannel到底是阻塞還是非阻塞的。找了很久沒找到確切的答案,最後在StackOverflow上找到了一個比較滿意的回答:

UNIX does not support non-blocking I/O for files, see Non-blocking I/O with regular files. As Java should (at least try to) provide the same behaviour on all platforms, the FileChannel does not implement SelectableChannel.
UNIX不支持文件的非阻塞IO,參看這個網頁的說明。由於Java在全平臺上要保持行爲一致(或者努力這麼做),所以FileChannel沒有實現SelectableChannel。

However Java 7 will include a new AsynchronousFileChannel class that supports asynchronous file I/O, which is a different mechanism to non-blocking I/O.
但是Java7上引入了一個支持異步文件IO的AsynchronousFileChannel類,但是這是跟非阻塞IO不一樣的機制。

In general only sockets and pipes truly support non-blocking I/O via select() mechanism.
一般來說只有socktes和pipes通過select機制真正支持非阻塞IO。

從這段話來說,首先FileChannel肯定是阻塞IO的;其次實現了SelectableChannel接口才能實現非阻塞IO。從FileChannel上也能看出來,nio下不是所有io操作都是非阻塞的。因此nio的全稱絕不應該是non-blocking I/O,而應該是new IO。
當然這裏還說了異步和非阻塞是完全不一樣的機制,這個以後再來了解吧。

Channels&Buffers

nio下有很多類,對我們來說以下三個是最重要的:

  • Channels
  • Buffers
  • Selectors
    第一代io使用字節流和字符流來進行讀寫,而nio使用Channels和Buffers來實現讀寫。數據總是從Channel讀到buffer裏,再從buffer寫到Channel裏。這個是使用上很大的區別。
    Channel和字符字節流有點類似,但是Channel都是雙向的,既可以讀,也可以寫。
    以前的io流我們一般直接定義一個byte數組來作爲buffer,但是nio裏需要使用Buffer類。
    下面是網上找的一個使用FileChannel和Buffer的例子:
    RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
    FileChannel inChannel = aFile.getChannel();

    //創建48個字節長度的ByteBuffer
    ByteBuffer buf = ByteBuffer.allocate(48);

    int bytesRead = inChannel.read(buf); //從Channel中讀取數據到buffer裏
    while (bytesRead != -1) {

    buf.flip();  //切換buffer到讀取狀態

    while(buf.hasRemaining()){
        System.out.print((char) buf.get()); //每次讀取一個字節
    }

    buf.clear(); //清空buffer後才能繼續從Channel裏讀取數據
    bytesRead = inChannel.read(buf);
    
    aFile.close();

Buffer實例化的時候需要傳入長度。在buffer寫好數據,準備讀取到Channel裏的時候,一定要執行flip操作,buffer纔可以讀取。同樣的,再次寫入數據之前,buffer需要clear一下清除數據。

Selector和非阻塞IO

但是nio最吸引我們的肯定還是非阻塞IO,而java nio裏非阻塞IO的實現要靠Selector。Selector是一種IO多路複用機制的實現。簡單來說,就是用單個線程/進程來對多個IO Channel進行輪詢。內核不管IO有沒有完成都會立即返回給用戶,這樣單個IO的阻塞就不會阻塞用戶線程。單個線程無需一個個等待IO完成再工作,而是發現有完成的就處理,處理完繼續輪詢,直到下一個完成的IO出現。

這樣做的好處就是單個線程即可處理海量併發請求,當然前提是業務的數據處理不能太耗時,否則線程會卡在數據處理上。NodeJs就是單線程非阻塞IO模型,因此擅長處理高併發。同樣適合非阻塞IO的還有nginx、redis等高併發但是計算簡單的軟件。而Java數據庫IO連接的JDBC是阻塞io,因此Java服務器不太適合nio,而適合多線程來處理併發。

對Android程序員來說,Looper中的MessgeQueue在執行next方法獲取下一條handler消息的時候,如果沒有消息,主線程執行nativePollOnce方法阻塞住。這樣主線程在沒事的時候就不會消耗cpu資源。在有消息傳過來時,android framework除了把消息添加到MessageQueue,還會把主線程給wakeup。那麼怎麼樣block(阻塞)線程和wakeup(喚醒)線程呢?通過linux系統的epoll機制,而epoll機制就是selector相對的poll機制的加強版。

以上只是我的一點形而上的理解,有可能存在謬誤,推薦幾個解釋的好的博文來學習Selector以及IO多路複用的概念:

Java NIO 系列教程(翻譯的國外教程)

聊聊Linux 五種IO模型

StackOverflow上關於NativePollOnce方法的討論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章