IO模型與NIO模型簡介
IO和NIO比較
同步與非同步
同步I/O 每個請求必須逐個地被處理,一個請求的處理會導致整個流程的暫時等待,這些事件無法併發地執行。使用者執行緒發起I/O請求後需要等待或者輪詢核心I/O操作完成後才能繼續執行。
非同步I/O 多個請求可以併發地執行,一個請求或者任務的執行不會導致整個流程的暫時等待。使用者執行緒發起I/O請求後仍然繼續執行,當核心I/O操作完成後會通知使用者執行緒,或者呼叫使用者執行緒註冊的回撥函式。
阻塞與非阻塞
阻塞 某個請求發出後,由於該請求操作需要的條件不滿足,請求操作一直阻塞,不會返回,直到條件滿足。Java IO的各種流都是阻塞的。這意味著一個執行緒一旦呼叫了read(),write()方法,那麼該執行緒就被阻塞住了,知道讀取到資料或者資料完整寫入了。在此期間執行緒不能做其他任何事情。
非阻塞 請求發出後,若該請求需要的條件不滿足,則立即返回一個標誌資訊告知條件不滿足,而不會一直等待。一般需要通過迴圈判斷請求條件是否滿足來獲取請求結果。Java NIO的非阻塞模式使得執行緒可以通過channel來讀資料,並且是返回當前已有的資料,或者什麼都不返回如果沒有資料可讀的話。這樣一來執行緒不會被阻塞住,它可以繼續向下執行。通常執行緒在呼叫非阻塞操作後,會通知處理其他channel上的IO操作。因此一個執行緒可以管理多個channel的輸入輸出。
向流和麵向緩衝區比較(Stream Oriented vs. Buffer Oriented)
第一個重大差異是Java IO是面向流的,而Java NIO是面向快取區的。
面向流 意思是我們每次從流當中讀取一個或多個位元組。怎麼處理讀取到的位元組是我們自己的事情。他們不會再任何地方快取。再有就是我們不能在流資料中向前後移動。如果需要向前後移動讀取位置,那麼我們需要首先為它建立一個快取區。
面向緩衝區 這有些細微差異。資料是被讀取到快取當中以便後續加工。我們可以在快取中向向後移動。這個特性給我們處理資料提供了更大的彈性空間。當然我們任然需要在使用資料前檢查快取中是否包含我們需要的所有資料。另外需要確保在往快取中寫入資料時避免覆蓋了已經寫入但是還未被處理的資料。
總結
面向流和麵向緩衝
ava IO是面向流的,每次從流(InputStream/OutputStream)中讀一個或多個位元組,直到讀取完所有位元組,它們沒有被快取在任何地方。另外,它不能前後移動流中的資料,如需前後移動處理,需要先將其快取至一個緩衝區。
Java NIO面向緩衝,資料會被讀取到一個緩衝區,需要時可以在緩衝區中前後移動處理,這增加了處理過程的靈活性。但與此同時在處理緩衝區前需要檢查該緩衝區中是否包含有所需要處理的資料,並需要確保更多資料讀入緩衝區時,不會覆蓋緩衝區內尚未處理的資料。
阻塞和非阻塞
Java IO的各種流是阻塞的。當某個執行緒呼叫read()或write()方法時,該執行緒被阻塞,直到有資料被讀取到或者資料完全寫入。阻塞期間該執行緒無法處理任何其它事情。
Java NIO為非阻塞模式。讀寫請求並不會阻塞當前執行緒,在資料可讀/寫前當前執行緒可以繼續做其它事情,所以一個單獨的執行緒可以管理多個輸入和輸出通道。
選擇器
Java NIO的選擇器允許一個單獨的執行緒同時監視多個通道,可以註冊多個通道到同一個選擇器上,然後使用一個單獨的執行緒來“選擇”已經就緒的通道。這種“選擇”機制為一個單獨的執行緒管理多個通道提供了可能。
零拷貝
Java NIO中提供的FileChannel擁有transferTo和transferFrom兩個方法,可直接把FileChannel中的資料拷貝到另外一個Channel,或者直接把另外一個Channel中的資料拷貝到FileChannel。該介面常被用於高效的網路/檔案的資料傳輸和大檔案拷貝。在作業系統支援的情況下,通過該方法傳輸資料並不需要將源資料從核心態拷貝到使用者態,再從使用者態拷貝到目標通道的核心態,同時也避免了兩次使用者態和核心態間的上下文切換,也即使用了“零拷貝”,所以其效能一般高於Java IO中提供的方法,使用FileChannel的零拷貝把本地檔案內容傳輸到網路。
NIO
NIO包含下面幾個核心的元件:
- Channels
- Buffers
- Selectors
Selectors
Selector是Java NIO中的一個元件,用於檢查一個或多個NIO Channel的狀態是否處於可讀、可寫。如此可以實現 單執行緒管理多個channels,也就是可以管理多個網路連結。
Java NIO的selector允許一個單一執行緒監聽多個channel輸入。我們可以註冊多個channel到selector上,然後然後用一個執行緒來挑出一個處於可讀或者可寫狀態的channel。selector機制使得單執行緒管理過個channel變得容易。
這有一幅示意圖,描述了單執行緒處理三個channel的情況:
建立一個Selector 可以通過Selector.open()方法:
Selector
selector = Selector.open();
|
註冊Channel到Selector上
為了同Selector掛了Channel,我們必須先把Channel註冊到Selector上,這個操作使用SelectableChannel.register():
channel.configureBlocking( false );
SelectionKey
key = channel.register(selector, SelectionKey.OP_READ);
|
Channel必須是非阻塞的。所以FileChannel不適用Selector,因為FileChannel不能切換為非阻塞模式。Socket c hannel可以正常使用。
注意register的第二個引數,這個引數是一個“關注集合”,代表我們關注的channel狀態,有四種基礎型別可供 監聽:
- Connect
- Accept
- Read
- Write
一個channel觸發了一個事件也可視作該事件處於就緒狀態。因此當channel與server連線成功後,那麼就是“連 接就緒”狀態。server channel接收請求連線時處於“可連線就緒”狀態。channel有資料可讀時處於“讀就 緒”狀態。channel可以進行資料寫入時處於“寫就緒”狀態。
上述的四種就緒狀態用SelectionKey中的常量表示如下:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果對多個事件感興趣可利用位的或運算結合多個常量,
int interestSet
= SelectionKey.OP_READ | SelectionKey.OP_WRITE;
|
SelectionKey's
register方法把Channel註冊到了Selectors上,這個方法的返回值是SelectionKeys,這個返回的物件包含了一些比較有價值的屬性:
- The interest set
這個“關注集合”實際上就是我們希望處理的事件的集合,它的值就是註冊時傳入的引數,我們可以用按位與運算把每個事件取出來
int
interestSet = selectionKey.interestOps();
boolean
isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean
isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean
isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean
isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
- The ready set
"就緒集合"中的值是當前channel處於就緒的值,一般來說在呼叫了select方法後都會需要用到就緒狀態。
int
readySet = selectionKey.readyOps();
或者:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
- The Channel
- The Selector
從SelectionKey操作Channel和Selector:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
-
An attached object (optional)
可以給一個SelectionKey附加一個Object,這樣做一方面可以方便我們識別某個特定的channel,同時也增加 了channel相關的附加資訊。例如,可以把用於channel的buffer附加到SelectionKey上:selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment();
附加物件的操作也可以在register的時候就執行
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
NIO Channel通道
特點
- 通道可以讀也可以寫,流一般來說是單向的(只能讀或者寫)。
- 通道可以非同步讀寫。
- 通道總是基於緩衝區Buffer來讀寫。
- 我們可以從通道中讀取資料,寫入到buffer;也可以中buffer內讀資料,寫入到通道中。
Channel實現
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel用於檔案的資料讀寫。 DatagramChannel用於UDP的資料讀寫。 SocketChannel用於TCP的資料讀寫。 ServerSocketChannel允許我們監聽TCP連結請求,每個請求會建立會一個SocketChannel.
NIO Buffer緩衝區
buffer本質上就是一塊記憶體區,可以用來寫入資料,並在稍後讀取出來。這塊記憶體被NIO Buffer包裹起來,對外提供一系列的讀寫方便開發的介面。
Buffer基本用法(Basic Buffer Usage)
利用Buffer讀寫資料,通常遵循四個步驟:
- 把資料寫入buffer;
- 呼叫flip;
- 從Buffer中讀取資料;
- 呼叫buffer.clear()或者buffer.compact()
Buffer的容量,位置,上限
一個Buffer有三個屬性是必須掌握的,分別是:
- capacity容量
- position位置
- limit限制
position和limit的具體含義取決於當前buffer的模式。capacity在兩種模式下都表示容量。
容量(Capacity)
作為一塊記憶體,buffer有一個固定的大小,叫做capacity容量。也就是最多隻能寫入容量值得位元組,整形等資料。一旦buffer寫滿了就需要清空已讀資料以便下次繼續寫入新的資料。
位置(Position)
當寫入資料到Buffer的時候需要中一個確定的位置開始,預設初始化時這個位置position為0,一旦寫入了資料比如一個位元組,整形資料,那麼position的值就會指向資料之後的一個單元,position最大可以到capacity-1.
當從Buffer讀取資料時,也需要從一個確定的位置開始。buffer從寫入模式變為讀取模式時,position會歸零,每次讀取後,position向後移動。
上限(Limit)
在寫模式,limit的含義是我們所能寫入的最大資料量。它等同於buffer的容量。
一旦切換到讀模式,limit則代表我們所能讀取的最大資料量,他的值等同於寫模式下position的位置。
資料讀取的上限時buffer中已有的資料,也就是limit的位置(原position所指的位置)。
Buffer Types
Java NIO有如下具體的Buffer型別:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
IO
Java的四種IO模型
- 阻塞 I/O
- 非阻塞 I/O
- I/O 多路複用(select和poll)
- 非同步 I/O(Posix.1的aio_系列函式)
阻塞IO 請求無法立即完成則保持阻塞,一般分為兩個階段:
- 階段1:等待資料就緒。網路 I/O 的情況就是等待遠端資料陸續抵達;磁碟I/O的情況就是等待磁碟資料從磁碟上讀取到核心態記憶體中。
- 階段2:資料拷貝。出於系統安全,使用者態的程式沒有許可權直接讀取核心態記憶體,因此核心負責把核心態記憶體中的資料拷貝一份到使用者態記憶體中。
非阻塞IO 有三個階段:
- socket設定為 NONBLOCK(非阻塞)就是告訴核心,當所請求的I/O操作無法完成時,不要將執行緒睡眠,而是返回一個錯誤碼(EWOULDBLOCK) ,這樣請求就不會阻塞。
- I/O操作函式將不斷的測試資料是否已經準備好,如果沒有準備好,繼續測試,直到資料準備好為止。整個I/O 請求的過程中,雖然使用者執行緒每次發起I/O請求後可以立即返回,但是為了等到資料,仍需要不斷地輪詢、重複請求,消耗了大量的 CPU 的資源。
- 資料準備好了,從核心拷貝到使用者空間。
IO多路複用 會用到select和poll函式,這兩個函式會使執行緒阻塞,但是和IO阻塞不同的是,這兩個函式可以同時阻塞讀個IO操作。還可以同時對多個讀操作多個寫操作的IO函式進行檢測,知道有資料可讀或者可寫時,才真正呼叫IO操作函式。
從流程上來看,使用select函式進行I/O請求和同步阻塞模型沒有太大的區別,甚至還多了新增監視Channel,以及呼叫select函式的額外操作,增加了額外工作。但是,使用 select以後最大的優勢是使用者可以在一個執行緒內同時處理多個Channel的I/O請求。使用者可以註冊多個Channel,然後不斷地呼叫select讀取被啟用的Channel,即可達到在同一個執行緒內同時處理多個I/O請求的目的。而在同步阻塞模型中,必須通過多執行緒的方式才能達到這個目的。
呼叫select/poll該方法由一個使用者態執行緒負責輪詢多個Channel,直到某個階段1的資料就緒,再通知實際的使用者執行緒執行階段2的拷貝。 通過一個專職的使用者態執行緒執行非阻塞I/O輪詢,模擬實現了階段一的非同步化。
非同步IO 呼叫aio_read 函式,告訴核心描述字,緩衝區指標,緩衝區大小,檔案偏移以及通知的方式,然後立即返回。當核心將資料拷貝到緩衝區後,再通知應用程式。所以非同步I/O模式下,階段1和階段2全部由核心完成,完成不需要使用者執行緒的參與。
相關
NIO和IO是如何影響程式設計的(How NIO and IO Influences Application Design)
開發中選擇NIO或者IO會在多方面影響程式設計:
- 使用NIO、IO的API呼叫類
- 資料處理
- 處理資料需要的執行緒數
API呼叫(The API Calls)
顯而易見使用NIO的API介面和使用IO時是不同的。不同於直接衝InputStream讀取位元組,我們的資料需要先寫入到buffer中,然後再從buffer中處理它們。
IO和NIO適用場景
如果你需要同時管理成千上萬的連結,這些連結只發送少量資料,例如聊天伺服器,用NIO來實現這個伺服器是有優勢的。類似的,如果你需要維持大量的連結,例如P2P網路,用單執行緒來管理這些 連結也是有優勢的。這種單執行緒多連線的設計可以用下圖描述:
Java NIO: A single thread managing multiple connections
如果連結數不是很多,但是每個連結的佔用較大頻寬,每次都要傳送大量資料,那麼使用傳統的IO設計伺服器可能是最好的選擇。下面是經典IO服務設計圖:
阻塞IO下的伺服器實現
單個執行緒逐個處理所有請求:使用阻塞I/O的伺服器,一般使用迴圈,逐個接受連線請求並讀取資料,然後處理下一個請求。
這個例子使用單個執行緒處理所有請求,同一時間只能處理一個請求,等待IO過程浪費大量的CPU資源,同時無法充分使用多CPU的優勢。
為每個請求建立一個執行緒:
為了防止連線請求過多,導致伺服器的執行緒數過多,造成多執行緒上下文切換開銷。可以通過執行緒池限制建立的執行緒數,
經典Reactor模式:
在Reactor模式中,有三個部分:
- Reactor 將I/O事件發派給對應的Handler
- Acceptor 處理客戶端連線請求
- Handlers 執行非阻塞讀/寫