JAVA高性能I/O設計模式

Java中的IO方式

主要分爲3種:BIO(同步阻塞)、NIO(同步非阻塞)和AIO(異步非阻塞)。linux

BIO

同步阻塞模式。在JDK1.4之前,使用Java創建網絡鏈接時,只能採用BIO方式,在服務器端啓動一個ServerSocket,而後使用accept等待客戶端請求,對於每個請求,使用一個線程來進行處理用戶請求。線程的大部分時間都在等待請求的到來和IO操做,利用率很低。並且線程的開銷比較大,數量有限,所以服務器同時能處理的鏈接數也很低。緩存

NIO

BIO模式中,是「一個Socket一個線程」;而在NIO中則是使用單個或少許的線程來輪詢Socket,當發現Socket上有請求時,才爲請求分配線程。所以是「一個請求一個線程」。服務器

具體實現就是把Socket經過Channel註冊到Selector,使用一個線程在Selector中輪詢,發現Channel有讀寫的事件,就能夠分配給其餘線程來處理(一般使用線程池)。網絡

AIO

從JDK7開始支持AIO模式。經過AsynchronousServerSocketChannel中註冊事件回調函數來處理業務邏輯。當IO操做完成之後,回調函數會被調用。若是傳入AsynchronousChannelGroup,能夠綁定線程池來處理事件。多線程

關於JDK的實現,Windows平臺基於IOCP實現AIO,Linux只有eppoll模擬實現了AIO。異步

 

用一句話來總結這三種IO的區別:socket

  • BIO是一個鏈接一個線程。
  • NIO是一個請求一個線程。
  • AIO是一個有效請求一個線程。

IO中的幾個概念

以銀行取款爲例: 函數

  • 同步 : 本身親自出馬持銀行卡到銀行取錢(使用同步IO時,Java本身處理IO讀寫);
  • 異步 : 委託一小弟拿銀行卡到銀行取錢,而後給你(使用異步IO時,Java將IO讀寫委託給OS處理,須要將數據緩衝區地址和大小傳給OS(銀行卡和密碼),OS須要支持異步IO操做API);
  • 阻塞 : ATM排隊取款,你只能等待(使用阻塞IO時,Java調用會一直阻塞到讀寫完成才返回);
  • 非阻塞 : 櫃檯取款,取個號,而後坐在椅子上作其它事,等號廣播會通知你辦理,沒到號你就不能去,你能夠不斷問大堂經理排到了沒有,大堂經理若是說還沒到你就不能去(使用非阻塞IO時,若是不能讀寫Java調用會立刻返回,當IO事件分發器會通知可讀寫時再繼續進行讀寫,不斷循環直到讀寫完成)

 系統I/O 可分爲阻塞型, 非阻塞同步型以及非阻塞異步型。性能

阻塞型I/O意味着控制權只到調用操做結束了纔會回到調用者手裏。 結果調用者被阻塞了, 這段時間了作不了任何其它事情。 更鬱悶的是,在等待IO結果的時間裏,調用者所在線程此時沒法騰出手來去響應其它的請求,這真是太浪費資源了。拿read()操做來講吧, 調用此函數的代碼會一直僵在此處直至它所讀的socket緩存中有數據到來。操作系統

相比之下,非阻塞同步是會當即返回控制權給調用者的。調用者不須要等等,它從調用的函數獲取兩種結果:要麼這次調用成功進行了;要麼系統返回錯誤標識告訴調用者當前資源不可用,你再等等或者再試度看吧。好比read()操做, 若是當前socket無數據可讀,則當即返回EWOULBLOCK/EAGAIN,告訴調用read()者」數據還沒準備好,你稍後再試」。

在非阻塞異步調用中,稍有不一樣。調用函數在當即返回時,還告訴調用者,此次請求已經開始了。系統會使用另外的資源或者線程來完成此次調用操做,並在完成的時候知會調用者(好比經過回調函數)。拿Windows的ReadFile()或者POSIX的aio_read()來講,調用它以後,函數當即返回,操做系統在後臺同時開始讀操做。

在以上三種IO形式中,非阻塞異步是性能最高、伸縮性最好的。

Reactor and Proactor

通常狀況下,I/O 複用機制須要事件分享器(event demultiplexor )。 事件分享器的做用,即將那些讀寫事件源分發給各讀寫事件的處理者,就像送快遞的在樓下喊: 誰的什麼東西送了, 快來拿吧。開發人員在開始的時候須要在分享器那裏註冊感興趣的事件,並提供相應的處理者(event handlers),或者是回調函數; 事件分享器在適當的時候會將請求的事件分發給這些handler或者回調函數。

涉及到事件分享器的兩種模式稱爲:Reactor and Proactor 。 Reactor模式是基於同步I/O的,而Proactor模式是和異步I/O相關的。 在Reactor模式中,事件分離者等待某個事件或者可應用或個操做的狀態發生(好比文件描述符可讀寫,或者是socket可讀寫),事件分離者就把這個事件傳給事先註冊的事件處理函數或者回調函數,由後者來作實際的讀寫操做。

Reactor模式:

Reator類圖如上所示,Reactor模式又叫反應器或反應堆,即實現註冊描述符(Handle)及事件的處理器(EventHandler),當有事件發生的時候,事件多路分發器(Event Demultiplexer)作出反應,調用事件具體的處理函數(ConcreteEventHandler::handle_event())。

Reator模式的典型啓動過程以下:

  1. 建立Reactor
  2. 註冊事件處理器(Reactor::register_handler())
  3. 調用事件多路分發器進入無限事件循環(Reacor:handle_events)
  4. 當操做系統通知某描述符狀態就緒時,事件多路分發器找出並調用此描述符註冊的事件處理器。

 Reactor模式已經被普遍使用,著名的開源事件庫libevent、libev、libuv都是使用Reactor模式。

Proactor模式

Proactor模式的類圖如上圖所示,Proactor模式又叫前攝器或主動器模式。它用於實現異步I/O模型,運行流程以下:

  1. Initiator主動調用Asynchronous Operation Processor發起異步I/O操做,
  2. 記錄異步操做的參數和函數地址放入完成事件隊列(Completion Event Queue)中
  3. Proactor循環檢測異步事件是否完成。若是完成則從完成事件隊列中取出回調函數完成回調。

Boost庫中的asio就使用了Proactor模式,其底層的異步I/O由操做系統提供,而異步事件的分發仍是由epoll/kequeue/select等實現。

在Proactor模式中,事件處理者(或者代由事件分離者發起)直接發起一個異步讀寫操做(至關於請求),而實際的工做是由操做系統來完成的。發起時,須要提供的參數包括用於存放讀到數據的緩存區,讀的數據大小,或者用於存放外發數據的緩存區,以及這個請求完後的回調函數等信息。事件分離者得知了這個請求,它默默等待這個請求的完成,而後轉發完成事件給相應的事件處理者或者回調。

二者區別

主要區別:

  • Reactor實現同步I/O多路分發,Proactor實現異步I/O分發。

若是隻是處理網絡I/O單線程的Reactor尚可處理,但若是涉及到文件I/O,單線程的Reactor可能被文件I/O阻塞而致使其餘事件沒法被分發。因此涉及到文件I/O最好仍是使用Proactor模式,或者用多線程模擬實現異步I/O的方式。

  • Reactor模式註冊的是文件描述符的就緒事件,而Proactor模式註冊的是完成事件。

即Reactor模式有事件發生的時候要判斷是讀事件仍是寫事件,而後用再調用系統調用(read/write等)將數據從內核中拷貝到用戶數據區繼續其餘業務處理。

而Proactor模式通常使用的是操做系統的異步I/O接口,發起異步調用(用戶提供數據緩衝區)以後操做系統將在內核態完成I/O並拷貝數據到用戶提供的緩衝區中,完成事件到達以後,用戶只須要實現本身後續的業務處理便可。

  • 主動和被動

Reactor模式是一種被動的處理,即有事件發生時被動處理。而Proator模式則是主動發起異步調用,而後循環檢測完成事件。

寫在最後

咱們知道linux系統提供的異步I/O,只支持O_DIRECT,不能帶緩存。所以出現了開源庫libeio,它和Linux的異步I/O同樣也是用多線程模擬,可是更高效。下圖是libeio的異步I/O實現,是否是很像Proactor模式啊。

相關文章
相關標籤/搜索