本篇文章目的在於基本概念和原理的解釋,不會貼過多的使用代碼。html
Java NIO (New IO)是 Java 的另外一個 IO API (來自 java1.4) ,意味着能夠替代標準的 Java IO API和 Java Networking API。 提供了一種與標準 IO API 不一樣的 IO 工做方式。
注意:Java的NIO只是說IO API,阻塞非阻塞纔是IO的模型。java
也有人稱NIO爲No-Blocking IO,非阻塞IO,可是這麼說並不嚴謹。由於對於基礎的IO操做API(好比文件IO,FileChannel),仍是阻塞的模型。只有對Networking IO API纔可使用非阻塞的模型(configureBlocking(false)
)。redis
Java NIO中的Networking IO API,支持非阻塞IO模型,還實現了IO多路複用(IO Multiplexing)。對於服務端來講,能夠用更少的線程支持更多的併發,大幅度提高了性能。編程
阻塞與非阻塞是從線程的角度出發的,這裏指的是線程狀態。服務器
當進行IO讀寫時,線程是阻塞的狀態。此時會讓出cpu控制權,不會佔用cpu資源。網絡
什麼?不佔用CPU資源?那是否是表明阻塞模型更好呢?多線程
答案是並非,雖然阻塞狀態不會佔用CPU,可是會發生線程的切換,線程切換時會有上下文保存轉換的過程,須要CPU調度,是一個很昂貴的操做。併發
Java NIO中的基礎IO API(非Networking IO API)仍是阻塞的方式,只是使用方式從面向流(stream)編程面向塊(buffer)了,和BIO本質上並無什麼區別。異步
非阻塞是指在進行IO操做的時候,若是設備還未準備好(好比socket尚未收到數據),操做會直接返回結果,不會讓當前線程進入阻塞狀態。socket
這樣的優勢是,使用者能夠自行決定在數據未準備好時的操做。線程能夠在沒有數據期間去執行其餘操做。
Networking API能夠配置爲非阻塞模型Channel.configureBlocking(false)
,配合Selector來實現多路複用功能。簡單的說就是一個Selector監聽多個socket io(對於unix系統來講,socket也是一個fd,也屬於io),能夠在一個線程中支持多個鏈接。固然在實際服務器開發時,就算是NIO模型,有些程序也不會只使用一個線程;但相比傳統的Blocking IO方式來講,須要的線程數量也會大大減小了。(redis中就是使用了IO多路複用技術,而且只有一個線程監聽socket io)
AIO 是 Java 1.7 以後引入的包,是 NIO 的升級版本,新增了提異步非阻塞的 IO 操做方式,因此人們叫它 AIO(Asynchronous IO),異步 IO 是基於事件和回調機制實現的,也就是應用操做以後會直接返回,不會堵塞在那裏,當後臺處理完成,操做系統會執行回調通知相應的線程進行後續的操做。
在I/O編程過程當中,當須要同時處理多個客戶端請求時,能夠利用多線程或者I/O多路複用技術進行處理。I/O多路複用技術經過把多個I/O的阻塞複用到同一個Select的阻塞上,從而使得系統在單線程的狀況下能夠同時處理多個客戶端請求。與傳統的多線程/多進程模型相比,I/O多路複用的最大優點是系統開銷小,系統不須要建立新的額外進程或者線程,也不須要維護這些線程和進程的運行,下降了系統的維護工做量,節省了系統的資源,I/O多路複用的主要應用場景以下:
目前支持I/O多路複用的系統調用又select/pselect/poll/epoll。
select/epoll的介紹摘自https://zhuanlan.zhihu.com/p/...
select的實現思路很直接。假如程序同時監視以下圖的sock一、sock2和sock3三個socket,那麼在調用select以後,操做系統把進程A分別加入這三個socket的等待隊列中。
當任何一個socket收到數據後,中斷程序將喚起進程。下圖展現了sock2接收到了數據的處理流程。
所謂喚起進程,就是將進程從全部的等待隊列中移除,加入到工做隊列裏面。以下圖所示。
經由這些步驟,當進程A被喚醒後,它知道至少有一個socket接收了數據。程序只需遍歷一遍socket列表,就能夠獲得就緒的socket。
這種簡單方式行之有效,在幾乎全部操做系統都有對應的實現。
可是簡單的方法每每有缺點,主要是:
其一,每次調用select都須要將進程加入到全部監視socket的等待隊列,每次喚醒都須要從每一個隊列中移除。這裏涉及了兩次遍歷,並且每次都要將整個fds列表傳遞給內核,有必定的開銷。正是由於遍歷操做開銷大,出於效率的考量,纔會規定select的最大監視數量,默認只能監視1024個socket。
其二,進程被喚醒後,程序並不知道哪些socket收到數據,還須要遍歷一次。
那麼,有沒有減小遍歷的方法?有沒有保存就緒socket的方法?這兩個問題即是epoll技術要解決的。
補充說明: 本節只解釋了select的一種情形。當程序調用select時,內核會先遍歷一遍socket,若是有一個以上的socket接收緩衝區有數據,那麼select直接返回,不會阻塞。這也是爲何select的返回值有可能大於1的緣由之一。若是沒有socket有數據,進程纔會阻塞。
select低效的緣由之一是將「維護等待隊列」和「阻塞進程」兩個步驟合二爲一。以下圖所示,每次調用select都須要這兩步操做,然而大多數應用場景中,須要監視的socket相對固定,並不須要每次都修改。epoll將這兩個操做分開,先用epoll_ctl維護等待隊列,再調用epoll_wait阻塞進程。顯而易見的,效率就能獲得提高。
select低效的另外一個緣由在於程序不知道哪些socket收到數據,只能一個個遍歷。若是內核維護一個「就緒列表」,引用收到數據的socket,就能避免遍歷。以下圖所示,計算機共有三個socket,收到數據的sock2和sock3被rdlist(就緒列表)所引用。當進程被喚醒後,只要獲取rdlist的內容,就可以知道哪些socket收到數據。
epoll是在select出現N多年後才被髮明的,是select和poll的加強版本。epoll經過如下一些措施來改進效率。
原理:
以下圖所示,當某個進程調用epoll_create方法時,內核會建立一個eventpoll對象(也就是程序中epfd所表明的對象)。eventpoll對象也是文件系統中的一員,和socket同樣,它也會有等待隊列。
建立一個表明該epoll的eventpoll對象是必須的,由於內核要維護「就緒列表」等數據,「就緒列表」能夠做爲eventpoll的成員。
建立epoll對象後,能夠用epoll_ctl添加或刪除所要監聽的socket。以添加socket爲例,以下圖,若是經過epoll_ctl添加sock一、sock2和sock3的監視,內核會將eventpoll添加到這三個socket的等待隊列中。
當socket收到數據後,中斷程序會操做eventpoll對象,而不是直接操做進程。
當socket收到數據後,中斷程序會給eventpoll的「就緒列表」添加socket引用。以下圖展現的是sock2和sock3收到數據後,中斷程序讓rdlist引用這兩個socket。
eventpoll對象至關因而socket和進程之間的中介,socket的數據接收並不直接影響進程,而是經過改變eventpoll的就緒列表來改變進程狀態。
當程序執行到epoll_wait時,若是rdlist已經引用了socket,那麼epoll_wait直接返回,若是rdlist爲空,阻塞進程。
假設計算機中正在運行進程A和進程B,在某時刻進程A運行到了epoll_wait語句。以下圖所示,內核會將進程A放入eventpoll的等待隊列中,阻塞進程。
當socket接收到數據,中斷程序一方面修改rdlist,另外一方面喚醒eventpoll等待隊列中的進程,進程A再次進入運行狀態(以下圖)。也由於rdlist的存在,進程A能夠知道哪些socket發生了變化。