Java框架解說之BIO NIO AIO不同IO模型演進(jìn)之路
引言
Netty
作為高性能的網(wǎng)絡(luò)通信框架,它是IO
模型演變過程中的產(chǎn)物。Netty
以Java NIO
為基礎(chǔ),是一種基于異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)通信應(yīng)用框架,Netty
用以快速開發(fā)高性能、高可靠的網(wǎng)絡(luò)服務(wù)器和客戶端程序,很多開源框架都選擇Netty
作為其網(wǎng)絡(luò)通信模塊。本文主要通過分析IO
模型的優(yōu)化演進(jìn)之路,比較不同IO
模型的異同,讓大家對(duì)于Java IO
模型有著更加深刻的理解,我想這也是Netty
如何實(shí)現(xiàn)高性能網(wǎng)絡(luò)通信理解的重要基礎(chǔ)。話不多說,我們趕緊發(fā)車了。
PS:文末有是彩蛋哦!
IO模型
1、什么是IO
在闡述BIO
、NIO
、AIO
之前,我們先來看下到底什么是IO
模型。我們都知道無(wú)論是程序還是平臺(tái),它們的功能高度抽象之后其實(shí)可以描述為這樣一個(gè)過程,即為通過外部條件以及數(shù)據(jù)的輸入,經(jīng)過程序或者平臺(tái)的處理產(chǎn)生了新的輸出,IO
模型實(shí)際上就是描述了計(jì)算機(jī)世界中的輸入和輸出過程的模式。
對(duì)于計(jì)算機(jī)來說,其鍵盤以及鼠標(biāo)等就是輸入設(shè)備,顯示器以及磁盤等就是輸出設(shè)備。舉個(gè)栗子,如果我們?cè)谟?jì)算機(jī)上寫一篇設(shè)計(jì)文檔并進(jìn)行保存,實(shí)際就是通過鍵盤對(duì)計(jì)算機(jī)進(jìn)行了數(shù)據(jù)輸入,完成設(shè)計(jì)文檔后將其保存輸出到了計(jì)算機(jī)的磁盤上。
上圖中的IO
描述,即為著名的計(jì)算機(jī)馮諾依曼體系,它大致描述了外部設(shè)備與計(jì)算機(jī)的IO
交互過程。
2、應(yīng)用程序IO交互
上文中我們介紹了計(jì)算機(jī)與外部設(shè)備交互的大致過程,那么我們的應(yīng)用程序是如何進(jìn)行IO
交互的呢?我們平時(shí)編寫的代碼不會(huì)獨(dú)立的存在,它總是被部署在linux
服務(wù)器或者各種容器中,應(yīng)用程序在服務(wù)器或者容器中啟動(dòng)后再對(duì)外提供服務(wù)。因此網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)首先需要和計(jì)算機(jī)進(jìn)行交互,才會(huì)被交由到對(duì)應(yīng)的程序去進(jìn)行后續(xù)的業(yè)務(wù)處理。
在Linux
的世界中,文件是用來描述Linux
世界的,目錄文件、套接字等都是文件。那文件又是什么鬼呢?文件實(shí)際就是二進(jìn)制流,二進(jìn)制流就是人類世界與計(jì)算機(jī)世界進(jìn)行交互的數(shù)據(jù)媒介。應(yīng)用從流中讀取數(shù)據(jù)即為read
操作,當(dāng)把流中的數(shù)據(jù)進(jìn)行寫入的時(shí)候就是write
操作。但是linux
系統(tǒng)又是如何區(qū)分不同類型的文件呢?實(shí)際是通過文件描述符(File Descriptor)
來進(jìn)行區(qū)分,文件描述符其實(shí)就是個(gè)整數(shù),這個(gè)整數(shù)實(shí)際是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。所以對(duì)這個(gè)整數(shù)的操作、就是對(duì)這個(gè)文件(流)的操作。
就拿網(wǎng)絡(luò)連接來說,我們創(chuàng)建一個(gè)網(wǎng)絡(luò)socket
,通過系統(tǒng)調(diào)用(socket調(diào)用)會(huì)返回一個(gè)文件描述符(某個(gè)整數(shù)),那么后續(xù)對(duì)socket
的操作就會(huì)轉(zhuǎn)化為對(duì)這個(gè)描述符的操作,主要涉及的操作包括accept
調(diào)用、read
調(diào)用以及 write
調(diào)用。這里所說的各種調(diào)用就是程序通過Linux內(nèi)核與計(jì)算機(jī)進(jìn)行交互。那么問題又來了,這個(gè)計(jì)算機(jī)內(nèi)核又是什么鬼。(PS:
關(guān)于內(nèi)核不是本文的重點(diǎn),這里就簡(jiǎn)單和大家說明下)
//socket函數(shù) socket(PF_INET6,SOCK_STREAM,IPPROTO_IP)
但是實(shí)際上應(yīng)用程序并不是直接從計(jì)算機(jī)中的網(wǎng)卡中獲取數(shù)據(jù),也就是說大家編寫的程序并不是直接操作計(jì)算機(jī)的底層硬件。
如上圖所示,在Linux
的結(jié)構(gòu)體系中,用戶的應(yīng)用程序都是通過Linux Kernel
內(nèi)核來操作計(jì)算機(jī)硬件。那么為什么應(yīng)用程序不能直接與底層硬件進(jìn)行交互還需要在中間再加一層內(nèi)核呢?主要有以下幾點(diǎn)考慮。
(1)計(jì)算機(jī)資源統(tǒng)一管理
Linux
內(nèi)核的作用就是進(jìn)程調(diào)度管理,同時(shí)對(duì)cpu
、內(nèi)存等系統(tǒng)資源進(jìn)行統(tǒng)一管理。因此內(nèi)核管理的都是系統(tǒng)極其敏感的資源,采用內(nèi)核制是為了實(shí)現(xiàn)系統(tǒng)的網(wǎng)絡(luò)通信,用戶管理,文件系統(tǒng)等安全穩(wěn)定的進(jìn)程管理,避免用戶應(yīng)用程序破壞系統(tǒng)數(shù)據(jù)。
(2)底層硬件調(diào)用統(tǒng)一封裝
試想一下,如果沒有內(nèi)核這層系統(tǒng)進(jìn)程,那么每個(gè)用戶應(yīng)用程序和硬件交互的時(shí)候都需要自己實(shí)現(xiàn)對(duì)應(yīng)的硬件驅(qū)動(dòng)。這樣的設(shè)計(jì)很難讓人接受,按照面向?qū)ο蟮脑O(shè)計(jì)思想,硬件的管理統(tǒng)一由Kernel
內(nèi)核負(fù)責(zé),Kernel
向下管理所有的硬件設(shè)備,向上提供給用戶進(jìn)程統(tǒng)一的系統(tǒng)調(diào)用,方便應(yīng)用程序可以像程序調(diào)用一樣進(jìn)行系統(tǒng)硬件交互。
3、5種IO模型
(1)阻塞型IO
當(dāng)用戶應(yīng)用進(jìn)程發(fā)起系統(tǒng)調(diào)用之后,在內(nèi)核數(shù)據(jù)沒有準(zhǔn)備好的情況下,調(diào)用一直處于阻塞狀態(tài),直到內(nèi)核準(zhǔn)備好數(shù)據(jù)后,將數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài),用戶應(yīng)用進(jìn)程獲取到數(shù)據(jù)后,本次調(diào)用才算完成。就好比你是外賣小哥,你到商家去取餐,商家的外賣還沒有準(zhǔn)備好,所以你只能在取餐的地方一直等待著,直到商家將做好的外賣準(zhǔn)備好,你才能拿了外賣去送餐。
(2)非阻塞型IO
非阻塞IO
式基于輪詢機(jī)制的IO
模型,應(yīng)用進(jìn)程不斷輪詢檢查內(nèi)核數(shù)據(jù)是否準(zhǔn)備好,如果沒有則返回EWOULDBLOCK
,進(jìn)程繼續(xù)發(fā)起recvfrom
調(diào)用,此時(shí)應(yīng)用可以去處理其他業(yè)務(wù)。當(dāng)內(nèi)核數(shù)據(jù)準(zhǔn)備好后,將內(nèi)核數(shù)據(jù)拷貝至用戶空間。這個(gè)過程就好比外賣小哥在等待取餐的時(shí)候不斷問商家外賣做好了沒(這個(gè)外賣小哥比較著急,送餐時(shí)間比較臨近了),每隔30s
問一次,直到外賣做好送到。
(3)多路復(fù)用IO
Linux
主要提供了select
、poll
以及epoll
等多路復(fù)用I/O
的實(shí)現(xiàn)方式,為什么會(huì)有三個(gè)實(shí)現(xiàn)呢?實(shí)際上他們的出現(xiàn)都是有時(shí)間順序的,后者的出現(xiàn)都是為了解決前者在使用中出現(xiàn)的問題。
在實(shí)際場(chǎng)景中,后端服務(wù)器接收大量的socket
連接,IO
多路復(fù)用是實(shí)際是使用了內(nèi)核提供的實(shí)現(xiàn)函數(shù),在實(shí)現(xiàn)函數(shù)中有一個(gè)參數(shù)是文件描述符集合,對(duì)這些文件描述符(FD
)進(jìn)行循環(huán)監(jiān)聽,當(dāng)某個(gè)文件描述符(FD
)就緒時(shí),就對(duì)這個(gè)文件描述符進(jìn)行處理。
下面我們分別看下select
、poll
以及epoll
這三個(gè)實(shí)現(xiàn)函數(shù)的實(shí)現(xiàn)原理:
select:
select
是操作系統(tǒng)的提供的內(nèi)核系統(tǒng)調(diào)用函數(shù),通過它可以將一組FD
傳給操作系統(tǒng),操作系統(tǒng)對(duì)這組FD
進(jìn)行遍歷,當(dāng)存在FD
處于數(shù)據(jù)就緒狀態(tài)后,將其全部返回給調(diào)用方,這樣應(yīng)用程序就可以對(duì)已經(jīng)就緒的IO
流進(jìn)行處理了。
select
在使用過程中存在一些問題:
(1)select
最多只能監(jiān)聽1024
個(gè)連接,支持的連接數(shù)較少;
(2)select
并不會(huì)只返回就緒的FD
,而是需要用戶進(jìn)程自己一個(gè)一個(gè)進(jìn)行遍歷找到就緒的FD
;
(3)用戶進(jìn)程在調(diào)用select
時(shí),都需要將FD
集合從用戶態(tài)拷貝到內(nèi)核態(tài),當(dāng)FD
較多時(shí)資源開銷相對(duì)較大。
poll:
poll
機(jī)制實(shí)際與select
機(jī)制區(qū)別不大,只是poll
機(jī)制去除掉了監(jiān)聽連接數(shù)1024的限制。
epoll:
epoll
解決了select
以及poll
機(jī)制的大部分問題,主要體現(xiàn)在以下幾個(gè)方面:
(1)FD
發(fā)現(xiàn)的變化:內(nèi)核不再通過輪詢遍歷的方式找到就緒的FD
,而是通過異步IO
事件喚醒的方式,當(dāng)socket
有事件發(fā)生時(shí),通過回調(diào)函數(shù)將就緒的FD
加入到就緒事件鏈表中,從而避免了輪詢掃描FD
集合;
(2)FD
返回的變化:內(nèi)核將已經(jīng)就緒的FD
返回給用戶,用戶應(yīng)用程序不需要自己再遍歷找到就緒的FD
;
(3)FD
拷貝的變化:epoll和內(nèi)核共享同一塊內(nèi)存,這塊內(nèi)存中保存的就是那些已經(jīng)可讀或者可寫的的文件描述符集合,這樣就減少了內(nèi)核和程序的內(nèi)存拷貝開銷。
(該圖片來自于網(wǎng)絡(luò))
(4)信號(hào)驅(qū)動(dòng)IO
系統(tǒng)存在一個(gè)信號(hào)捕捉函數(shù),該信號(hào)捕捉函數(shù)與socket
存在關(guān)聯(lián)關(guān)系,在用戶進(jìn)程發(fā)起sigaction調(diào)用之后,用戶進(jìn)程可以去處理其他的業(yè)務(wù)流程。當(dāng)內(nèi)核將數(shù)據(jù)準(zhǔn)備好之后,用戶進(jìn)程會(huì)接收到一個(gè)SIGIO
信號(hào),然后用戶進(jìn)程中斷當(dāng)前的任務(wù)發(fā)起recvfrom
調(diào)用從內(nèi)核讀取數(shù)據(jù)到用戶空間再進(jìn)行數(shù)據(jù)處理。
(5)異步IO
所謂異步IO
模型,就是用戶進(jìn)程發(fā)起系統(tǒng)調(diào)用之后,不管內(nèi)核對(duì)應(yīng)的請(qǐng)求數(shù)據(jù)是否準(zhǔn)備好,都不會(huì)阻塞當(dāng)前進(jìn)程,立即返回后進(jìn)程可以繼續(xù)處理其他的業(yè)務(wù)。當(dāng)內(nèi)核準(zhǔn)備好數(shù)據(jù)之后,系統(tǒng)會(huì)從內(nèi)核復(fù)制數(shù)據(jù)到用戶空間,然后通過信號(hào)通知用戶進(jìn)程進(jìn)行數(shù)據(jù)讀取處理。
Java中的IO模型
上文中我們闡述了Linux
本身存在的幾種IO
模型,那么對(duì)應(yīng)到Java
程序世界中,Java
也有對(duì)應(yīng)的IO
模型,分別是BIO
、NIO
以及AIO
三種IO
模型。它們都提供了和IO
有關(guān)的API
,這些API
實(shí)際也是依賴系統(tǒng)層面的IO
完成數(shù)據(jù)處理的,因此Java
的IO
模型,實(shí)際就是對(duì)系統(tǒng)層面IO
模型的封裝。接下來我們來一起看下Java
的這幾種IO
模型。
BIO
BIO
即為Blocking IO
,顧名思義就是阻塞型IO
模型,當(dāng)用戶進(jìn)程向服務(wù)端發(fā)起請(qǐng)求后,一定等到服務(wù)端處理完成有數(shù)據(jù)返回給用戶,用戶進(jìn)程才完成一次IO
操作,否則就會(huì)阻塞住,像個(gè)癡心漢傻傻的一直等待數(shù)據(jù)返回,當(dāng)數(shù)據(jù)完成返回后用戶線程才會(huì)解除block
狀態(tài),因此在整個(gè)數(shù)據(jù)讀取過程中會(huì)發(fā)生阻塞。
另外從下圖我們可以看出來,每一個(gè)客戶端連接,服務(wù)端都有對(duì)應(yīng)的處理線程來處理對(duì)應(yīng)的請(qǐng)求。還是以餐廳吃飯的例子,你到餐廳去吃飯,假如每來一個(gè)消費(fèi)者,餐廳都用一個(gè)服務(wù)員來接待直到消費(fèi)者吃飽喝足走出餐廳,那么這個(gè)餐廳得配置多少個(gè)服務(wù)員才合適?這么多服務(wù)員,餐廳的老板估計(jì)得賠的內(nèi)褲都沒了。
因此在網(wǎng)絡(luò)連接不多的情況下,BIO還能發(fā)回作用。但是當(dāng)連接數(shù)上來后,比如幾十萬(wàn)甚至上百萬(wàn)連接,BIO
模型的IO
交互就顯得心有余而力不足了。當(dāng)連接數(shù)不斷攀高時(shí),BIO
模型的IO
交互方式存在以下幾種弊端。
(1)頻繁創(chuàng)建和銷毀大量的線程會(huì)消耗系統(tǒng)資源給服務(wù)器造成巨大的壓力;
(2)另外大量的處理線程會(huì)占用過多的JVM
內(nèi)存,你的程序不要干其他事情了,都被大量連接線程給占滿了;
(3)實(shí)際上線程的上下文切換成本也是很高的。
基于BIO
模型在處理大量連接時(shí)存在上述的問題,因此我們需要一種更加高效的線程模型來應(yīng)對(duì)幾十萬(wàn)甚至上百萬(wàn)的客戶端連接。
NIO
通過上文的分析,由于在BIO
模型下,Java
中在進(jìn)行IO
操作時(shí)候是沒辦法知道什么時(shí)候可以讀數(shù)據(jù)或者什么時(shí)候可以寫數(shù)據(jù),BIO
又是一個(gè)實(shí)在孩子因此沒有什么好的辦法只能在哪里傻等著。由于socket
的讀寫操作不能進(jìn)行中斷,因此當(dāng)有新的連接到來時(shí),只能不斷創(chuàng)建新的線程來處理,從而導(dǎo)致存在性能問題。
那么如何解決這個(gè)問題呢?我們都知道問題的根源就是BIO
模型中我們不知道數(shù)據(jù)的讀取與寫入的時(shí)機(jī),才導(dǎo)致的阻塞等待,那么如果我們能夠知道數(shù)據(jù)讀寫的時(shí)機(jī),是不是就不用傻傻的等著響應(yīng),也不用再創(chuàng)建新的線程來處理連接了。
為了提升IO
交互效率,避免阻塞傻等的情況發(fā)生。Java 1.4
中引入了NIO
,對(duì)于NIO
來說,有人稱之為Non-blocking IO
,但是我更愿意稱之為New IO
。因?yàn)樗且环N基于IO
多路復(fù)用的IO
模型,而不是簡(jiǎn)單的同步非阻塞的IO
模型。所謂IO
多路復(fù)用指的就是用同一個(gè)線程處理大量連接,多路指的就是大量連接,復(fù)用指的就是使用一個(gè)線程來進(jìn)行處理。
那我們先來看看同步非阻塞模型有什么問題,NIO
的讀寫以及接受方法在等待數(shù)據(jù)就緒階段都是非阻塞的。如上文中的描述,同步非阻塞模式下應(yīng)用進(jìn)程不斷向內(nèi)核發(fā)起調(diào)用,詢問內(nèi)核數(shù)據(jù)完成準(zhǔn)備。相對(duì)于同步阻塞模型有了一定的優(yōu)化,通過不斷輪詢數(shù)據(jù)是否準(zhǔn)備好,避免了調(diào)用阻塞。但是由于應(yīng)用不斷進(jìn)行系統(tǒng)IO
調(diào)用,在此過程中十分消耗CPU
,因此還有進(jìn)一步優(yōu)化的空間。此時(shí)就該IO多路復(fù)用模型上場(chǎng)一展拳腳了,而Java
的NIO
正是借助于此實(shí)現(xiàn)了IO
性能的提升。(這里以epoll
機(jī)制來進(jìn)行說明)
Java NIO
基于通道和緩沖區(qū)的形式來處理流數(shù)據(jù),借助于Linux操作系統(tǒng)的epoll
機(jī)制,多路復(fù)用器selector就會(huì)不斷進(jìn)行輪詢,當(dāng)某個(gè)channel
的事件(讀事件,寫事件,連接事件等等)準(zhǔn)備就緒的時(shí)候,就是會(huì)找到這個(gè)channel
對(duì)應(yīng)的SelectionKey
,去做相應(yīng)的操作,進(jìn)行數(shù)據(jù)的讀寫操作。
AIO
所謂AIO(Asynchronous IO)
就是NIO
第二代,它是在Java 7
中引入的,是一種異步IO
模型。異步IO
模型是基于事件和回調(diào)機(jī)制實(shí)現(xiàn)的,當(dāng)應(yīng)用發(fā)起調(diào)用請(qǐng)求之后會(huì)直接返回不會(huì)阻塞在那里,當(dāng)后臺(tái)進(jìn)行數(shù)據(jù)處理完成后,操作系統(tǒng)便會(huì)通知對(duì)應(yīng)的線程來進(jìn)行后續(xù)的數(shù)據(jù)處理。
從效率上來看,AIO
無(wú)疑是最高的,然而,美中不足的是目前作為廣大服務(wù)器使用的系統(tǒng) linux
對(duì) AIO
的支持還不完善,導(dǎo)致我們還不能愉快的使用 AIO
這項(xiàng)技術(shù),Netty
實(shí)際也是使用過AIO
技術(shù),但是實(shí)際并沒有帶來很大的性能提升,目前還是基于Java NIO
實(shí)現(xiàn)的。
總結(jié)
本文主要從計(jì)算機(jī)IO
交互出發(fā),分別給大家介紹了什么是IO
模型以及常見的五種IO
模型,介紹了這幾種IO
模型的優(yōu)缺點(diǎn),從系統(tǒng)優(yōu)化演進(jìn)的角度分析了Java BIO
、NIO
以及AIO
演化之路。從設(shè)計(jì)者的角度分析Java BIO
存在的不足。我們?cè)賮砘仡櫹抡麄€(gè)演進(jìn)過程的脈絡(luò)。
在后續(xù)的文章中,筆者將繼續(xù)帶大家深入研究的Netty
作為高性能網(wǎng)絡(luò)通信框架的奇妙之處,敬請(qǐng)期待哦。
真正的大師永遠(yuǎn)懷著一顆學(xué)徒的心
到此這篇關(guān)于Java框架解說之BIO NIO AIO不同IO模型演進(jìn)之路的文章就介紹到這了,更多相關(guān)Java IO模型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- JavaIO模型中的BIO,NIO和AIO詳解
- Java中BIO、NIO和AIO的區(qū)別、原理與用法
- java中BIO、NIO、AIO都有啥區(qū)別
- Java BIO,NIO,AIO總結(jié)
- 淺談Java中BIO、NIO和AIO的區(qū)別和應(yīng)用場(chǎng)景
- 詳解Java 網(wǎng)絡(luò)IO編程總結(jié)(BIO、NIO、AIO均含完整實(shí)例代碼)
- Java中BIO、NIO、AIO的理解
- Java中網(wǎng)絡(luò)IO的實(shí)現(xiàn)方式(BIO、NIO、AIO)介紹
- Java中AIO、BIO、NIO應(yīng)用場(chǎng)景及區(qū)別
相關(guān)文章
Java9新特性Java.util.Optional優(yōu)化與增強(qiáng)解析
這篇文章主要為大家介紹了Java9新特性Java.util.Optional優(yōu)化與增強(qiáng)使用說明解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03java連接MySQL數(shù)據(jù)庫(kù)實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了java連接MySQL數(shù)據(jù)庫(kù)實(shí)現(xiàn)代碼,感興趣的小伙伴們可以參考一下2016-06-06詳解RabbitMQ中死信隊(duì)列和延遲隊(duì)列的使用詳解
這篇文章主要為大家介紹了RabbitMQ中死信隊(duì)列和延遲隊(duì)列的原理與使用,這也是Java后端面試中常見的問題,感興趣的小伙伴可以了解一下2022-05-05JAVA獲取當(dāng)前項(xiàng)目和文件所在路徑的實(shí)例代碼
這篇文章主要介紹了JAVA獲取當(dāng)前項(xiàng)目和文件所在路徑的實(shí)例代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Spring boot redis cache的key的使用方法
這篇文章主要介紹了Spring boot redis cache的key的使用方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05