0. Java 線程IO模型
Java當中的線程I/O模型如圖所示:
1. BIO
當一個線程進行I/O操作的時候,傳統的做法是阻塞等待,直到I/O操作完成再繼續後續的操作,這種IO方式就是BIO(Blocking I/O)。
BIO方式的缺點是:
- 大量併發線程的場景下效率過低;
- 空等待浪費資源;
2. NIO
JDK1.4引入了NIO(No Blocking I/O或者是New I/O)。NIO是一種同步非阻塞的I/O模型,相對於BIO,NIO允許一個線程在I/O操作的時候處理其他任務,但是需要定期輪詢檢查I/O操作是否完成。
NIO的缺點在於:
- 輪詢的時間間隔不好把握;
- 一個線程處理一個I/O操作,如果存在大量I/O,處理其他任務和輪詢操作反覆切換狀態,上下文切換開銷大;
3. I/O多路複用(主要)
3.1 概念
為了解決NIO的缺點,Linux引入了I/O多路複用的機制,即一個線程可以同時監聽多個I/O操作,當某個I/O操作完成後,會通知線程進行處理。
多路指的是多個SOCKET連接之間的I/O操作,複用指的是共用一個線程。
I/O多路複用的優點在於:
- 一個線程可以同時監聽多個I/O操作,減少了線程的數量,避免了線程切換的開銷;
需要注意的是,I/O多路複用只有和NIO配合使用才能發揮作用,因為NIO是非阻塞的,所以可以在一個線程中同時監聽多個I/O操作,而BIO是阻塞的,一個線程只能處理一個I/O操作,所以無法實現I/O多路複用。
3.2 實現
I/O多路複用的實現思路:
對於多個socket連接,程序提供一個文件描述符集合給系統,當某個接口的I/O操作完成後,會通知線程進行處理。
實現I/O多路複用的方式有三種:select、poll、epoll。
1. select
函數原型如下所示:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds:文件描述符的數量,即文件描述符集合中最大的文件描述符加1;
- readfds:讀文件描述符集合;
- writefds:寫文件描述符集合;
- exceptfds:異常文件描述符集合;
- timeout:超時時間;
從參數可以看出來select方式監聽讀、寫、異常事件。
select根據監聽的事件類型分別創建三個文件描述符數組,然後在timeout時間內阻塞線程進行監聽,直到有事件發生或者超時。之後檢查數組中是否有事件到達。
select的缺點在於:
- 文件描述符數組大小有限,為1024,因此對於高併發場景並不適用;
- 維持三個文件描述符數組,佔據大量的內存空間;
- 每次調用
select需要將數組從用户空間拷貝到內核空間,同時重新對數組進行遍歷查找,效率低;
2. poll
函數原型如下所示:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- fds:文件描述符數組;
- ndfs:文件描述符數組的大小;
- timeout:超時時間;
本質的工作過程和select類似,但是稍微做了改進,只需要構建一個數組,並且數組大小不受限制,而是能夠自由指定;
poll的缺點在於:
- 每次調用
poll之後都需要進行數組遍歷,這一點並沒有改進
3. epoll
為了解決select和poll的缺點,在高併發場景下,不同的操作系統引入了不同的解決方案,例如Linux引入了epoll、FreeBSD引入了kqueue、Solaris引入了/dev/poll。
由epoll實現I/O多路複用,步驟如下:
-
先創建epoll對象:
int epfd = epoll_create(10);其中,
int epoll_create(int size)會在內核空間開闢一塊指定大小的數據表,並由epfd指向這部分內存。 -
創建好epoll對象之後,使用
epoll_ctl將註冊需要監聽的事件:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epfd是創建數組之後的內存指針;-
op是操作類型,包括三種模式:EPOLL_CTL_ADD:添加需要監聽的事件;EPOLL_CTL_MOD:修改需要監聽的事件;EPOLL_CTL_DEL:刪除需要監聽的事件;
fd是需要監聽的文件描述符,需要支持NIO;-
event記錄了註冊事件的具體信息。數據結構如下所示:typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; -
使用
epoll_wait進行監聽:
epoll_wait函數原型如下所示:int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout); epfd是創建數組之後的內存指針;-
evlist是用於存放事件的數組,也是返回的結果數組,包含被觸發事件的對應文件描述符;- 這裏顯示了和
select、poll的區別,select、poll會返回所有文件描述符然後遍歷,而epoll只會返回被觸發事件的文件描述符;
- 這裏顯示了和
maxevents是監聽事件的最大容量;timeout是超時時間;
監聽步驟是block的,也就是阻塞的,只有超時才會返回;
epoll的優點在於:
- 只返回觸發事件的文件描述符,避免了整個數組的遍歷;
-
支持水平觸發(Level Trigger)和邊緣觸發(Edge Trigger)兩種模式;
-
對於水平觸發和邊緣觸發,具體解釋可參考這篇博客;
4. AIO
AIO(Asynchronous I/O),即異步非阻塞I/O模型,AIO的實現方式是基於事件和回調機制的,當一個I/O操作完成後,會通知線程進行處理,因此不需要輪詢操作。
-
AIO和NIO的區別在於:
- NIO:線程需要定時檢查I/O操作是否完成;
- AIO:安心去做其他事情,等到通知之後才會進行處理;
5. 技術對比
5.1 BIO、NIO、I/O多路複用、AIO對比
5.2 select、poll、epoll對比
6. 面試模擬
Q:IO多路複用是什麼意思?
A:IO多路複用指的是一個線程管理多個IO連接,監聽多個IO事件;Q:NIO的具體含義
A:NIO一般理解為Not Blocking IO,即非阻塞IO,和傳統的BIO(阻塞IO)相比,NIO模型中,一個線程在IO操作的時候可以處理其他任務,定期輪詢檢查IO操作是否完成Q:基於什麼實現的I/O多路複用?
A:傳統的實現方式包括select、poll,但是這兩類方法都需要遍歷數組,效率較低,為此不同的操作系統提出了不同的改進方案,例如solaris提出了/dev/poll,FreeBSD提出了kqueue,Linux提出了epoll,而epoll相比於select、poll的主要區別就是返回的事件列表只包括觸發事件的文件描述符,而不是全部監聽事件的文件描述符,改進了數組遍歷這一監聽方式。
參考資料
- 一文徹底理解Java IO模型(阻塞IO非阻塞IO/IO多路複用)
- IO多路複用機制詳解
- 講講BIO和NIO以及IO多路複用