NIO 簡(jiǎn)介
在傳統(tǒng)的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式進(jìn)行的。也就是說(shuō),當(dāng)一個(gè)線程執(zhí)行一個(gè) I/O 操作時(shí),它會(huì)被阻塞直到操作完成。這種阻塞模型在處理多個(gè)并發(fā)連接時(shí)可能會(huì)導(dǎo)致性能瓶頸,因?yàn)樾枰獮槊總€(gè)連接創(chuàng)建一個(gè)線程,而線程的創(chuàng)建和切換都是有開(kāi)銷的。
為了解決這個(gè)問(wèn)題,在 Java1.4 版本引入了一種新的 I/O 模型 — NIO (New IO,也稱為 Non-blocking IO) 。NIO 彌補(bǔ)了同步阻塞 I/O 的不足,它在標(biāo)準(zhǔn) Java 代碼中提供了非阻塞、面向緩沖、基于通道的 I/O,可以使用少量的線程來(lái)處理多個(gè)連接,大大提高了 I/O 效率和并發(fā)。
??需要注意:使用 NIO 并不一定意味著高性能,它的性能優(yōu)勢(shì)主要體現(xiàn)在高并發(fā)和高延遲的網(wǎng)絡(luò)環(huán)境下。當(dāng)連接數(shù)較少、并發(fā)程度較低或者網(wǎng)絡(luò)傳輸速度較快時(shí),NIO 的性能并不一定優(yōu)于傳統(tǒng)的 BIO
NIO 核心組件
NIO 主要包括以下三個(gè)核心組件:
- Buffer(緩沖區(qū)):NIO 讀寫數(shù)據(jù)都是通過(guò)緩沖區(qū)進(jìn)行操作的。讀操作的時(shí)候?qū)?Channel 中的數(shù)據(jù)填充到 Buffer 中,而寫操作時(shí)將 Buffer 中的數(shù)據(jù)寫入到 Channel 中。
- Channel(通道):Channel 是一個(gè)雙向的、可讀可寫的數(shù)據(jù)傳輸通道,NIO 通過(guò) Channel 來(lái)實(shí)現(xiàn)數(shù)據(jù)的輸入輸出。通道是一個(gè)抽象的概念,它可以代表文件、套接字或者其他數(shù)據(jù)源之間的連接。
- Selector(選擇器):允許一個(gè)線程處理多個(gè) Channel,基于事件驅(qū)動(dòng)的 I/O 多路復(fù)用模型。所有的 Channel 都可以注冊(cè)到 Selector 上,由 Selector 來(lái)分配線程來(lái)處理事件。
Buffer(緩沖區(qū))
在傳統(tǒng)的 BIO 中,數(shù)據(jù)的讀寫是面向流的, 分為字節(jié)流和字符流。
在 Java 1.4 的 NIO 庫(kù)中,所有數(shù)據(jù)都是用緩沖區(qū)處理的,這是新庫(kù)和之前的 BIO 的一個(gè)重要區(qū)別,有點(diǎn)類似于 BIO 中的緩沖流。NIO 在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的。在寫入數(shù)據(jù)時(shí),寫入到緩沖區(qū)中。 使用 NIO 在讀寫數(shù)據(jù)時(shí),都是通過(guò)緩沖區(qū)進(jìn)行操作。
Buffer
的子類如下圖所示。其中,最常用的是 ByteBuffer
,它可以用來(lái)存儲(chǔ)和操作字節(jié)數(shù)據(jù)。
你可以將 Buffer 理解為一個(gè)數(shù)組,IntBuffer
、FloatBuffer
、CharBuffer
等分別對(duì)應(yīng) int[]
、float[]
、char[]
等。
為了更清晰地認(rèn)識(shí)緩沖區(qū),我們來(lái)簡(jiǎn)單看看Buffer
類中定義的四個(gè)成員變量:
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
}
這四個(gè)成員變量的具體含義如下:
-
容量(
capacity
):Buffer
可以存儲(chǔ)的最大數(shù)據(jù)量,Buffer
創(chuàng)建時(shí)設(shè)置且不可改變; -
界限(
limit
):Buffer
中可以讀/寫數(shù)據(jù)的邊界。寫模式下,limit
代表最多能寫入的數(shù)據(jù),一般等于capacity
(可以通過(guò)limit(int newLimit)
方法設(shè)置);讀模式下,limit
等于 Buffer 中實(shí)際寫入的數(shù)據(jù)大小。 -
位置(
position
):下一個(gè)可以被讀寫的數(shù)據(jù)的位置(索引)。從寫操作模式到讀操作模式切換的時(shí)候(flip),position
都會(huì)歸零,這樣就可以從頭開(kāi)始讀寫了。 -
標(biāo)記(
mark
):Buffer
允許將位置直接定位到該標(biāo)記處,這是一個(gè)可選屬性;
并且,上述變量滿足如下的關(guān)系:0 <= mark <= position <= limit <= capacity 。
另外,Buffer 有讀模式和寫模式這兩種模式,分別用于從 Buffer 中讀取數(shù)據(jù)或者向 Buffer 中寫入數(shù)據(jù)。Buffer 被創(chuàng)建之后默認(rèn)是寫模式,調(diào)用 flip()
可以切換到讀模式。如果要再次切換回寫模式,可以調(diào)用 clear()
或者 compact()
方法。
Buffer
對(duì)象不能通過(guò) new
調(diào)用構(gòu)造方法創(chuàng)建對(duì)象 ,只能通過(guò)靜態(tài)方法實(shí)例化 Buffer
。
這里以 ByteBuffer
為例進(jìn)行介紹:
// 分配堆內(nèi)存
public static ByteBuffer allocate(int capacity);
// 分配直接內(nèi)存
public static ByteBuffer allocateDirect(int capacity);
Buffer 最核心的兩個(gè)方法:
-
get
: 讀取緩沖區(qū)的數(shù)據(jù) -
put
:向緩沖區(qū)寫入數(shù)據(jù)
除上述兩個(gè)方法之外,其他的重要方法:
-
flip
:將緩沖區(qū)從寫模式切換到讀模式,它會(huì)將limit
的值設(shè)置為當(dāng)前position
的值,將position
的值設(shè)置為 0。 -
clear
: 清空緩沖區(qū),將緩沖區(qū)從讀模式切換到寫模式,并將position
的值設(shè)置為 0,將limit
的值設(shè)置為capacity
的值。 - ……
Channel(通道)
Channel 是一個(gè)通道,它建立了與數(shù)據(jù)源(如文件、網(wǎng)絡(luò)套接字等)之間的連接。我們可以利用它來(lái)讀取和寫入數(shù)據(jù),就像打開(kāi)了一條自來(lái)水管,讓數(shù)據(jù)在 Channel 中自由流動(dòng)。
BIO 中的流是單向的,分為各種 InputStream
(輸入流)和 OutputStream
(輸出流),數(shù)據(jù)只是在一個(gè)方向上傳輸。通道與流的不同之處在于通道是雙向的,它可以用于讀、寫或者同時(shí)用于讀寫。
Channel 與前面介紹的 Buffer 打交道,讀操作的時(shí)候?qū)?Channel 中的數(shù)據(jù)填充到 Buffer 中,而寫操作時(shí)將 Buffer 中的數(shù)據(jù)寫入到 Channel 中
另外,因?yàn)?Channel 是全雙工的,所以它可以比流更好地映射底層操作系統(tǒng)的 API。特別是在 UNIX 網(wǎng)絡(luò)編程模型中,底層操作系統(tǒng)的通道都是全雙工的,同時(shí)支持讀寫操作。
Channel
的子類如下圖所示。
其中,最常用的是以下幾種類型的通道:
-
FileChannel
:文件訪問(wèn)通道; -
SocketChannel
、ServerSocketChannel
:TCP 通信通道; -
DatagramChannel
:UDP 通信通道;
Channel 最核心的兩個(gè)方法:
-
read
:讀取數(shù)據(jù)并寫入到 Buffer 中。 -
write
:將 Buffer 中的數(shù)據(jù)寫入到 Channel 中。
這里我們以 FileChannel
為例演示一下是讀取文件數(shù)據(jù)的
RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r"))
FileChannel channel = reader.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
Selector(選擇器)
Selector(選擇器) 是 NIO 中的一個(gè)關(guān)鍵組件,它允許一個(gè)線程處理多個(gè) Channel。Selector 是基于事件驅(qū)動(dòng)的 I/O 多路復(fù)用模型,主要運(yùn)作原理是:通過(guò) Selector 注冊(cè)通道的事件,Selector 會(huì)不斷地輪詢注冊(cè)在其上的 Channel。當(dāng)事件發(fā)生時(shí),比如:某個(gè) Channel 上面有新的 TCP 連接接入、讀和寫事件,這個(gè) Channel 就處于就緒狀態(tài),會(huì)被 Selector 輪詢出來(lái)。Selector 會(huì)將相關(guān)的 Channel 加入到就緒集合中。通過(guò) SelectionKey 可以獲取就緒 Channel 的集合,然后對(duì)這些就緒的 Channel 進(jìn)行響應(yīng)的 I/O 操作。
一個(gè)多路復(fù)用器 Selector 可以同時(shí)輪詢多個(gè) Channel,由于 JDK 使用了 epoll()
代替?zhèn)鹘y(tǒng)的 select
實(shí)現(xiàn),所以它并沒(méi)有最大連接句柄 1024/2048
的限制。這也就意味著只需要一個(gè)線程負(fù)責(zé) Selector 的輪詢,就可以接入成千上萬(wàn)的客戶端。
Selector 可以監(jiān)聽(tīng)以下四種事件類型:
-
SelectionKey.OP_ACCEPT
:表示通道接受連接的事件,這通常用于ServerSocketChannel
。 -
SelectionKey.OP_CONNECT
:表示通道完成連接的事件,這通常用于SocketChannel
。 -
SelectionKey.OP_READ
:表示通道準(zhǔn)備好進(jìn)行讀取的事件,即有數(shù)據(jù)可讀。 -
SelectionKey.OP_WRITE
:表示通道準(zhǔn)備好進(jìn)行寫入的事件,即可以寫入數(shù)據(jù)。
Selector
是抽象類,可以通過(guò)調(diào)用此類的 open()
靜態(tài)方法來(lái)創(chuàng)建 Selector 實(shí)例。Selector 可以同時(shí)監(jiān)控多個(gè) SelectableChannel
的 IO
狀況,是非阻塞 IO
的核心。
一個(gè) Selector 實(shí)例有三個(gè) SelectionKey
集合:
- 所有的
SelectionKey
集合:代表了注冊(cè)在該 Selector 上的Channel
,這個(gè)集合可以通過(guò)keys()
方法返回。 - 被選擇的
SelectionKey
集合:代表了所有可通過(guò)select()
方法獲取的、需要進(jìn)行IO
處理的 Channel,這個(gè)集合可以通過(guò)selectedKeys()
返回。 - 被取消的
SelectionKey
集合:代表了所有被取消注冊(cè)關(guān)系的Channel
,在下一次執(zhí)行select()
方法時(shí),這些Channel
對(duì)應(yīng)的SelectionKey
會(huì)被徹底刪除,程序通常無(wú)須直接訪問(wèn)該集合,也沒(méi)有暴露訪問(wèn)的方法。
簡(jiǎn)單演示一下如何遍歷被選擇的 SelectionKey
集合并進(jìn)行處理:
NIO 零拷貝
零拷貝是提升 IO 操作性能的一個(gè)常用手段,像 ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等頂級(jí)開(kāi)源項(xiàng)目都用到了零拷貝。
零拷貝是指計(jì)算機(jī)執(zhí)行 IO 操作時(shí),CPU 不需要將數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域復(fù)制到另一個(gè)存儲(chǔ)區(qū)域,從而可以減少上下文切換以及 CPU 的拷貝時(shí)間。也就是說(shuō),零拷貝主主要解決操作系統(tǒng)在處理 I/O 操作時(shí)頻繁復(fù)制數(shù)據(jù)的問(wèn)題。零拷貝的常見(jiàn)實(shí)現(xiàn)技術(shù)有: mmap+write
、sendfile
和 sendfile + DMA gather copy
。
下圖展示了各種零拷貝技術(shù)的對(duì)比圖:
CPU 拷貝 | DMA 拷貝 | 系統(tǒng)調(diào)用 | 上下文切換 | |
---|---|---|---|---|
傳統(tǒng)方法 | 2 | 2 | read+write | 4 |
mmap+write | 1 | 2 | mmap+write | 4 |
sendfile | 1 | 2 | sendfile | 2 |
sendfile + DMA gather copy | 0 | 2 | sendfile | 2 |
可以看出,無(wú)論是傳統(tǒng)的 I/O 方式,還是引入了零拷貝之后,2 次 DMA(Direct Memory Access) 拷貝是都少不了的。因?yàn)閮纱?DMA 都是依賴硬件完成的。零拷貝主要是減少了 CPU 拷貝及上下文的切換。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-823147.html
Java 對(duì)零拷貝的支持:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-823147.html
-
MappedByteBuffer
是 NIO 基于內(nèi)存映射(mmap
)這種零拷??式的提供的?種實(shí)現(xiàn),底層實(shí)際是調(diào)用了 Linux 內(nèi)核的mmap
系統(tǒng)調(diào)用。它可以將一個(gè)文件或者文件的一部分映射到內(nèi)存中,形成一個(gè)虛擬內(nèi)存文件,這樣就可以直接操作內(nèi)存中的數(shù)據(jù),而不需要通過(guò)系統(tǒng)調(diào)用來(lái)讀寫文件。 -
FileChannel
的transferTo()/transferFrom()
是 NIO 基于發(fā)送文件(sendfile
)這種零拷貝方式的提供的一種實(shí)現(xiàn),底層實(shí)際是調(diào)用了 Linux 內(nèi)核的sendfile
系統(tǒng)調(diào)用。它可以直接將文件數(shù)據(jù)從磁盤發(fā)送到網(wǎng)絡(luò),而不需要經(jīng)過(guò)用戶空間的緩沖區(qū)
到了這里,關(guān)于Java NIO的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!