初識NIO:
在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 類, 引入了一種基于通道和緩沖區的 I/O 方式,它可以使用 Native 函數庫直接配置設定堆外記憶體,然後通過一個存儲在 Java 堆的 DirectByteBuffer 對象作為這塊記憶體的引用進行操作,避免了在 Java 堆和 Native 堆中來回複制資料。
NIO 是一種同步非阻塞的 IO 模型。同步是指線程不斷輪詢 IO 事件是否就緒,非阻塞是指線程在等待 IO 的時候,可以同時做其他任務。同步的核心就是 Selector,Selector 代替了線程本身輪詢 IO 事件,避免了阻塞同時減少了不必要的線程消耗;非阻塞的核心就是通道和緩沖區,當 IO 事件就緒時,可以通過寫道緩沖區,保證 IO 的成功,而無需線程阻塞式地等待。
Buffer:
為什麼說NIO是基于緩沖區的IO方式呢?因為,當一個連結建立完成後,IO的資料未必會馬上到達,為了當資料到達時能夠正确完成IO操作,在BIO(阻塞IO)中,等待IO的線程必須被阻塞,以全天候地執行IO操作。為了解決這種IO方式低效的問題,引入了緩沖區的概念,當資料到達時,可以預先被寫入緩沖區,再由緩沖區交給線程,是以線程無需阻塞地等待IO。
通道:
當執行:SocketChannel.write(Buffer),便将一個 buffer 寫到了一個通道中。如果說緩沖區還好了解,通道相對來說就更加抽象。網上部落格難免有寫不嚴謹的地方,容易使初學者感到難以了解。
引用 Java NIO 中權威的說法:通道是 I/O 傳輸發生時通過的入口,而緩沖區是這些數 據傳輸的來源或目标。對于離開緩沖區的傳輸,您想傳遞出去的資料被置于一個緩沖區,被傳送到通道。對于傳回緩沖區的傳輸,一個通道将資料放置在您所提供的緩沖區中。
例如 有一個伺服器通道 ServerSocketChannel serverChannel,一個用戶端通道 SocketChannel clientChannel;伺服器緩沖區:serverBuffer,用戶端緩沖區:clientBuffer。
當伺服器想向用戶端發送資料時,需要調用:clientChannel.write(serverBuffer)。當用戶端要讀時,調用 clientChannel.read(clientBuffer)
當用戶端想向伺服器發送資料時,需要調用:serverChannel.write(clientBuffer)。當伺服器要讀時,調用 serverChannel.read(serverBuffer)
這樣,通道和緩沖區的關系似乎更好了解了。在實踐中,未必會出現這種雙向連接配接的蠢事(然而這确實存在的,後面的内容還會涉及),但是可以了解為在NIO中:如果想将Data發到目标端,則需要将存儲該Data的Buffer,寫入到目标端的Channel中,然後再從Channel中讀取資料到目标端的Buffer中。
Selector:
通道和緩沖區的機制,使得線程無需阻塞地等待IO事件的就緒,但是總是要有人來監管這些IO事件。這個工作就交給了selector來完成,這就是所謂的同步。
Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接配接(通道),但每個連接配接的流量都很低,使用Selector就會很友善。
要使用Selector,得向Selector注冊Channel,然後調用它的select()方法。這個方法會一直阻塞到某個注冊的通道有事件就緒,這就是所說的輪詢。一旦這個方法傳回,線程就可以處理這些事件。
Selector中注冊的感興趣事件有:
- OP_ACCEPT
- OP_CONNECT
- OP_READ
- OP_WRITE
優化:
一種優化方式是:将Selector進一步分解為Reactor,将不同的感興趣事件分開,每一個Reactor隻負責一種感興趣的事件。這樣做的好處是:1、分離阻塞級别,減少了輪詢的時間;2、線程無需周遊set以找到自己感興趣的事件,因為得到的set中僅包含自己感興趣的事件。
NIO和epoll:
epoll是Linux核心的IO模型。我想一定有人想問,AIO聽起來比NIO更加高大上,為什麼不使用AIO?AIO其實也有應用,但是有一個問題就是,Linux是不支援AIO的,是以基于AIO的程式運作在Linux上的效率相比NIO反而更低。而Linux是最主要的伺服器OS,是以相比AIO,目前NIO的應用更加廣泛。
說到這裡,可能你已經明白了,epoll一定和NIO有着很深的因緣。沒錯,如果仔細研究epoll的技術内幕,你會發現它确實和NIO非常相似,都是基于“通道”和緩沖區的,也有selector,隻是在epoll中,通道實際上是作業系統的“管道”。和NIO不同的是,NIO中,解放了線程,但是需要由selector阻塞式地輪詢IO事件的就緒;而epoll中,IO事件就緒後,會自動發送消息,通知selector:“我已經就緒了。”可以認為,Linux的epoll是一種效率更高的NIO。
NIO轶事:
一篇有意思的部落格,講的 Java selector.open() 的時候,會建立一個自己和自己的連結(windows上是tcp,linux上是通道)
這麼做的原因:可以從 Apache Mina 中窺探。在 Mina 中,有如下機制:
- Mina架構會建立一個Work對象的線程。
- Work對象的線程的run()方法會從一個隊列中拿出一堆Channel,然後使用Selector.select()方法來偵聽是否有資料可以讀/寫。
- 最關鍵的是,在select的時候,如果隊列有新的Channel加入,那麼,Selector.select()會被喚醒,然後重新select最新的Channel集合。
- 要喚醒select方法,隻需要調用Selector的wakeup()方法。
而一個阻塞在select上的線程有以下三種方式可以被喚醒:
- 有資料可讀/寫,或出現異常。
- 阻塞時間到,即time out。
- 收到一個non-block的信号。可由kill或pthread_kill發出。
首先 2 可以排除,而第三種方式,隻在linux中存在。是以,Java NIO為什麼要建立一個自己和自己的連結:就是如果想要喚醒select,隻需要朝着自己的這個loopback連接配接發點資料過去,于是,就可以喚醒阻塞在select上的線程了。