Java 中的 BIO歪架、NIO和 AIO 理解為是 Java 語言對操作系統(tǒng)的各種 IO 模型的封裝。程序員在使用這些 API 的時候挺尾,不需要關(guān)心操作系統(tǒng)層面的知識变勇,也不需要根據(jù)不同操作系統(tǒng)編寫不同的代碼。只需要使用Java的API就可以了既荚。
在講 BIO,NIO,AIO 之前先來回顧一下這樣幾個概念:同步與異步稚失,阻塞與非阻塞。
關(guān)于同步和異步的概念解讀困擾著很多程序員恰聘,大部分的解讀都會帶有自己的一點偏見句各。參考了 Stackoverflow相關(guān)問題后對原有答案進(jìn)行了進(jìn)一步完善:
When you execute something synchronously, you wait for it to finish before moving on to another task. When you execute something asynchronously, you can move on to another task before it finishes.
當(dāng)你同步執(zhí)行某項任務(wù)時,你需要等待其完成才能繼續(xù)執(zhí)行其他任務(wù)晴叨。當(dāng)你異步執(zhí)行某些操作時凿宾,你可以在完成另一個任務(wù)之前繼續(xù)進(jìn)行。
-
同步 :兩個同步任務(wù)相互依賴兼蕊,并且一個任務(wù)必須以依賴于另一任務(wù)的某種方式執(zhí)行初厚。 比如在
A->B
事件模型中,你需要先完成 A 才能執(zhí)行B遍略。 再換句話說惧所,同步調(diào)用種被調(diào)用者未處理完請求之前骤坐,調(diào)用不返回,調(diào)用者會一直等待結(jié)果的返回下愈。 - 異步: 兩個異步的任務(wù)完全獨立的纽绍,一方的執(zhí)行不需要等待另外一方的執(zhí)行。再換句話說势似,異步調(diào)用種一調(diào)用就返回結(jié)果不需要等待結(jié)果返回拌夏,當(dāng)結(jié)果返回的時候通過回調(diào)函數(shù)或者其他方式拿著結(jié)果再做相關(guān)事情,
阻塞和非阻塞
- 阻塞: 阻塞就是發(fā)起一個請求履因,調(diào)用者一直等待請求結(jié)果返回障簿,也就是當(dāng)前線程會被掛起,無法從事其他任務(wù)栅迄,只有當(dāng)條件就緒才能繼續(xù)站故。
- 非阻塞: 非阻塞就是發(fā)起一個請求,調(diào)用者不用一直等著結(jié)果返回毅舆,可以先去干其他事情西篓。
如何區(qū)分 “同步/異步 ”和 “阻塞/非阻塞” 呢?
同步/異步是從行為角度描述事物的憋活,而阻塞和非阻塞描述的當(dāng)前事物的狀態(tài)(等待調(diào)用結(jié)果時的狀態(tài))岂津。
1. BIO (Blocking I/O)
同步阻塞I/O模式,數(shù)據(jù)的讀取寫入必須阻塞在一個線程內(nèi)等待其完成悦即。
1.1 傳統(tǒng) BIO
BIO通信(一請求一應(yīng)答)模型圖如下(圖源網(wǎng)絡(luò)吮成,原出處不明):
[圖片上傳失敗...(image-6a125e-1611913037326)]
采用 BIO 通信模型 的服務(wù)端,通常由一個獨立的 Acceptor 線程負(fù)責(zé)監(jiān)聽客戶端的連接辜梳。我們一般通過在while(true)
循環(huán)中服務(wù)端會調(diào)用 accept()
方法等待接收客戶端的連接的方式監(jiān)聽請求粱甫,請求一旦接收到一個連接請求,就可以建立通信套接字在這個通信套接字上進(jìn)行讀寫操作冗美,此時不能再接收其他客戶端連接請求魔种,只能等待同當(dāng)前連接的客戶端的操作執(zhí)行完成, 不過可以通過多線程來支持多個客戶端的連接粉洼,如上圖所示节预。
如果要讓 BIO 通信模型 能夠同時處理多個客戶端請求,就必須使用多線程(主要原因是socket.accept()
属韧、socket.read()
游添、socket.write()
涉及的三個主要函數(shù)都是同步阻塞的)扼劈,也就是說它在接收到客戶端連接請求之后為每個客戶端創(chuàng)建一個新的線程進(jìn)行鏈路處理硼砰,處理完成之后碎罚,通過輸出流返回應(yīng)答給客戶端,線程銷毀。這就是典型的 一請求一應(yīng)答通信模型 拙泽。我們可以設(shè)想一下如果這個連接不做任何事情的話就會造成不必要的線程開銷淌山,不過可以通過 線程池機(jī)制 改善,線程池還可以讓線程的創(chuàng)建和回收成本相對較低顾瞻。使用FixedThreadPool
可以有效的控制了線程的最大數(shù)量泼疑,保證了系統(tǒng)有限的資源的控制,實現(xiàn)了N(客戶端請求數(shù)量):M(處理客戶端請求的線程數(shù)量)的偽異步I/O模型(N 可以遠(yuǎn)遠(yuǎn)大于 M)荷荤,下面一節(jié)"偽異步 BIO"中會詳細(xì)介紹到退渗。
我們再設(shè)想一下當(dāng)客戶端并發(fā)訪問量增加后這種模型會出現(xiàn)什么問題?
在 Java 虛擬機(jī)中蕴纳,線程是寶貴的資源会油,線程的創(chuàng)建和銷毀成本很高,除此之外古毛,線程的切換成本也是很高的翻翩。尤其在 Linux 這樣的操作系統(tǒng)中,線程本質(zhì)上就是一個進(jìn)程喇潘,創(chuàng)建和銷毀線程都是重量級的系統(tǒng)函數(shù)体斩。如果并發(fā)訪問量增加會導(dǎo)致線程數(shù)急劇膨脹可能會導(dǎo)致線程堆棧溢出、創(chuàng)建新線程失敗等問題颖低,最終導(dǎo)致進(jìn)程宕機(jī)或者僵死,不能對外提供服務(wù)弧烤。
1.2 偽異步 IO
為了解決同步阻塞I/O面臨的一個鏈路需要一個線程處理的問題忱屑,后來有人對它的線程模型進(jìn)行了優(yōu)化一一一后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數(shù)M:線程池最大線程數(shù)N的比例關(guān)系暇昂,其中M可以遠(yuǎn)遠(yuǎn)大于N.通過線程池可以靈活地調(diào)配線程資源莺戒,設(shè)置線程的最大值,防止由于海量并發(fā)接入導(dǎo)致線程耗盡急波。
偽異步IO模型圖(圖源網(wǎng)絡(luò)从铲,原出處不明):
[圖片上傳失敗...(image-d752e8-1611913037325)]
采用線程池和任務(wù)隊列可以實現(xiàn)一種叫做偽異步的 I/O 通信框架,它的模型圖如上圖所示澄暮。當(dāng)有新的客戶端接入時名段,將客戶端的 Socket 封裝成一個Task(該任務(wù)實現(xiàn)java.lang.Runnable接口)投遞到后端的線程池中進(jìn)行處理,JDK 的線程池維護(hù)一個消息隊列和 N 個活躍線程泣懊,對消息隊列中的任務(wù)進(jìn)行處理伸辟。由于線程池可以設(shè)置消息隊列的大小和最大線程數(shù),因此馍刮,它的資源占用是可控的信夫,無論多少個客戶端并發(fā)訪問,都不會導(dǎo)致資源的耗盡和宕機(jī)。
偽異步I/O通信框架采用了線程池實現(xiàn)静稻,因此避免了為每個請求都創(chuàng)建一個獨立線程造成的線程資源耗盡問題警没。不過因為它的底層仍然是同步阻塞的BIO模型,因此無法從根本上解決問題振湾。
1.3 代碼示例
下面代碼中演示了BIO通信(一請求一應(yīng)答)模型惠奸。我們會在客戶端創(chuàng)建多個線程依次連接服務(wù)端并向其發(fā)送"當(dāng)前時間+:hello world",服務(wù)端會為每個客戶端線程創(chuàng)建一個線程來處理恰梢。代碼示例出自閃電俠的博客佛南,原地址如下:
http://www.reibang.com/p/a4e03835921a
客戶端
/**
*
* @author 閃電俠
* @date 2018年10月14日
* @Description:客戶端
*/
public class IOClient {
public static void main(String[] args) {
// TODO 創(chuàng)建多個線程,模擬多個客戶端連接服務(wù)端
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 3333);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
}
}
服務(wù)端
/**
* @author 閃電俠
* @date 2018年10月14日
* @Description: 服務(wù)端
*/
public class IOServer {
public static void main(String[] args) throws IOException {
// TODO 服務(wù)端處理客戶端連接請求
ServerSocket serverSocket = new ServerSocket(3333);
// 接收到客戶端連接請求之后為每個客戶端創(chuàng)建一個新的線程進(jìn)行鏈路處理
new Thread(() -> {
while (true) {
try {
// 阻塞方法獲取新的連接
Socket socket = serverSocket.accept();
// 每一個新的連接都創(chuàng)建一個線程嵌言,負(fù)責(zé)讀取數(shù)據(jù)
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字節(jié)流方式讀取數(shù)據(jù)
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
}
}).start();
} catch (IOException e) {
}
}
}).start();
}
}
1.4 總結(jié)
在活動連接數(shù)不是特別高(小于單機(jī)1000)的情況下嗅回,這種模型是比較不錯的,可以讓每一個連接專注于自己的 I/O 并且編程模型簡單摧茴,也不用過多考慮系統(tǒng)的過載绵载、限流等問題。線程池本身就是一個天然的漏斗苛白,可以緩沖一些系統(tǒng)處理不了的連接或請求娃豹。但是,當(dāng)面對十萬甚至百萬級連接的時候购裙,傳統(tǒng)的 BIO 模型是無能為力的懂版。因此,我們需要一種更高效的 I/O 處理模型來應(yīng)對更高的并發(fā)量躏率。
2. NIO (New I/O)
2.1 NIO 簡介
NIO是一種同步非阻塞的I/O模型躯畴,在Java 1.4 中引入了 NIO 框架,對應(yīng) java.nio 包薇芝,提供了 Channel , Selector蓬抄,Buffer等抽象。
NIO中的N可以理解為Non-blocking夯到,不單純是New嚷缭。它支持面向緩沖的,基于通道的I/O操作方法耍贾。 NIO提供了與傳統(tǒng)BIO模型中的 Socket
和 ServerSocket
相對應(yīng)的 SocketChannel
和 ServerSocketChannel
兩種不同的套接字通道實現(xiàn),兩種通道都支持阻塞和非阻塞兩種模式阅爽。阻塞模式使用就像傳統(tǒng)中的支持一樣,比較簡單逼争,但是性能和可靠性都不好优床;非阻塞模式正好與之相反。對于低負(fù)載誓焦、低并發(fā)的應(yīng)用程序胆敞,可以使用同步阻塞I/O來提升開發(fā)速率和更好的維護(hù)性着帽;對于高負(fù)載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用移层,應(yīng)使用 NIO 的非阻塞模式來開發(fā)仍翰。
2.2 NIO的特性/NIO與IO區(qū)別
如果是在面試中回答這個問題,我覺得首先肯定要從 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 說起观话。然后予借,可以從 NIO 的3個核心組件/特性為 NIO 帶來的一些改進(jìn)來分析。如果频蛔,你把這些都回答上了我覺得你對于 NIO 就有了更為深入一點的認(rèn)識灵迫,面試官問到你這個問題,你也能很輕松的回答上來了晦溪。
1)Non-blocking IO(非阻塞IO)
IO流是阻塞的瀑粥,NIO流是不阻塞的。
Java NIO使我們可以進(jìn)行非阻塞IO操作三圆。比如說狞换,單線程中從通道讀取數(shù)據(jù)到buffer,同時可以繼續(xù)做別的事情舟肉,當(dāng)數(shù)據(jù)讀取到buffer中后修噪,線程再繼續(xù)處理數(shù)據(jù)。寫數(shù)據(jù)也是一樣的路媚。另外黄琼,非阻塞寫也是如此。一個線程請求寫入一些數(shù)據(jù)到某通道磷籍,但不需要等待它完全寫入适荣,這個線程同時可以去做別的事情。
Java IO的各種流是阻塞的院领。這意味著,當(dāng)一個線程調(diào)用 read()
或 write()
時够吩,該線程被阻塞比然,直到有一些數(shù)據(jù)被讀取,或數(shù)據(jù)完全寫入周循。該線程在此期間不能再干任何事情了
2)Buffer(緩沖區(qū))
IO 面向流(Stream oriented)强法,而 NIO 面向緩沖區(qū)(Buffer oriented)。
Buffer是一個對象湾笛,它包含一些要寫入或者要讀出的數(shù)據(jù)饮怯。在NIO類庫中加入Buffer對象,體現(xiàn)了新庫與原I/O的一個重要區(qū)別嚎研。在面向流的I/O中·可以將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到 Stream 對象中蓖墅。雖然 Stream 中也有 Buffer 開頭的擴(kuò)展類,但只是流的包裝類,還是從流讀到緩沖區(qū)论矾,而 NIO 卻是直接讀到 Buffer 中進(jìn)行操作教翩。
在NIO厙中,所有數(shù)據(jù)都是用緩沖區(qū)處理的贪壳。在讀取數(shù)據(jù)時饱亿,它是直接讀到緩沖區(qū)中的; 在寫入數(shù)據(jù)時,寫入到緩沖區(qū)中闰靴。任何時候訪問NIO中的數(shù)據(jù)彪笼,都是通過緩沖區(qū)進(jìn)行操作。
最常用的緩沖區(qū)是 ByteBuffer,一個 ByteBuffer 提供了一組功能用于操作 byte 數(shù)組蚂且。除了ByteBuffer,還有其他的一些緩沖區(qū)配猫,事實上,每一種Java基本類型(除了Boolean類型)都對應(yīng)有一種緩沖區(qū)膘掰。
3)Channel (通道)
NIO 通過Channel(通道) 進(jìn)行讀寫章姓。
通道是雙向的,可讀也可寫识埋,而流的讀寫是單向的凡伊。無論讀寫,通道只能和Buffer交互窒舟。因為 Buffer系忙,通道可以異步地讀寫。
4)Selector (選擇器)
NIO有選擇器惠豺,而IO沒有银还。
選擇器用于使用單個線程處理多個通道。因此洁墙,它需要較少的線程來處理這些通道蛹疯。線程之間的切換對于操作系統(tǒng)來說是昂貴的。 因此热监,為了提高系統(tǒng)效率選擇器是有用的捺弦。
[圖片上傳失敗...(image-cb65d4-1611913166435)]
2.3 NIO 讀數(shù)據(jù)和寫數(shù)據(jù)方式
通常來說NIO中的所有IO都是從 Channel(通道) 開始的。
- 從通道進(jìn)行數(shù)據(jù)讀取 :創(chuàng)建一個緩沖區(qū)孝扛,然后請求通道讀取數(shù)據(jù)列吼。
- 從通道進(jìn)行數(shù)據(jù)寫入 :創(chuàng)建一個緩沖區(qū),填充數(shù)據(jù)苦始,并要求通道寫入數(shù)據(jù)寞钥。
數(shù)據(jù)讀取和寫入操作圖示:
[圖片上傳失敗...(image-68d295-1611913037325)]
2.4 NIO核心組件簡單介紹
NIO 包含下面幾個核心的組件:
- Channel(通道)
- Buffer(緩沖區(qū))
- Selector(選擇器)
整個NIO體系包含的類遠(yuǎn)遠(yuǎn)不止這三個,只能說這三個是NIO體系的“核心API”陌选。我們上面已經(jīng)對這三個概念進(jìn)行了基本的闡述理郑,這里就不多做解釋了蹄溉。
2.5 代碼示例
代碼示例出自閃電俠的博客,原地址如下:
http://www.reibang.com/p/a4e03835921a
客戶端 IOClient.java 的代碼不變香浩,我們對服務(wù)端使用 NIO 進(jìn)行改造类缤。以下代碼較多而且邏輯比較復(fù)雜,大家看看就好邻吭。
/**
*
* @author 閃電俠
* @date 2019年2月21日
* @Description: NIO 改造后的服務(wù)端
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1\. serverSelector負(fù)責(zé)輪詢是否有新的連接餐弱,服務(wù)端監(jiān)測到新的連接之后,不再創(chuàng)建一個新的線程囱晴,
// 而是直接將新連接綁定到clientSelector上膏蚓,這樣就不用 IO 模型中 1w 個 while 循環(huán)在死等
Selector serverSelector = Selector.open();
// 2\. clientSelector負(fù)責(zé)輪詢連接是否有數(shù)據(jù)可讀
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 對應(yīng)IO編程中服務(wù)端啟動
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(3333));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 監(jiān)測是否有新的連接,這里的1指的是阻塞的時間為 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1) 每來一個新連接畸写,不需要創(chuàng)建一個線程驮瞧,而是直接注冊到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
while (true) {
// (2) 批量輪詢是否有哪些連接有數(shù)據(jù)可讀,這里的1指的是阻塞的時間為 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(
Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}
為什么大家都不愿意用 JDK 原生 NIO 進(jìn)行開發(fā)呢枯芬?從上面的代碼中大家都可以看出來论笔,是真的難用!除了編程復(fù)雜千所、編程模型難之外狂魔,它還有以下讓人詬病的問題:
- JDK 的 NIO 底層由 epoll 實現(xiàn),該實現(xiàn)飽受詬病的空輪詢 bug 會導(dǎo)致 cpu 飆升 100%
- 項目龐大之后淫痰,自行實現(xiàn)的 NIO 很容易出現(xiàn)各類 bug最楷,維護(hù)成本較高,上面這一坨代碼我都不能保證沒有 bug
Netty 的出現(xiàn)很大程度上改善了 JDK 原生 NIO 所存在的一些讓人難以忍受的問題待错。
3. AIO (Asynchronous I/O)
AIO 也就是 NIO 2籽孙。在 Java 7 中引入了 NIO 的改進(jìn)版 NIO 2,它是異步非阻塞的IO模型。異步 IO 是基于事件和回調(diào)機(jī)制實現(xiàn)的火俄,也就是應(yīng)用操作之后會直接返回犯建,不會堵塞在那里,當(dāng)后臺處理完成瓜客,操作系統(tǒng)會通知相應(yīng)的線程進(jìn)行后續(xù)的操作胎挎。
AIO 是異步IO的縮寫,雖然 NIO 在網(wǎng)絡(luò)操作中忆家,提供了非阻塞的方法,但是 NIO 的 IO 行為還是同步的德迹。對于 NIO 來說芽卿,我們的業(yè)務(wù)線程是在 IO 操作準(zhǔn)備好時,得到通知胳搞,接著就由這個線程自行進(jìn)行 IO 操作卸例,IO操作本身是同步的称杨。(除了 AIO 其他的 IO 類型都是同步的,這一點可以從底層IO線程模型解釋筷转,推薦一篇文章:《漫話:如何給女朋友解釋什么是Linux的五種IO模型姑原?》 )
查閱網(wǎng)上相關(guān)資料,我發(fā)現(xiàn)就目前來說 AIO 的應(yīng)用還不是很廣泛呜舒,Netty 之前也嘗試使用過 AIO锭汛,不過又放棄了。