上篇說了最基礎(chǔ)的五種IO模型,相信大家對(duì)IO相關(guān)的概念應(yīng)該有了一定的了解克锣,這篇文章主要講講基于多路復(fù)用IO的Java NIO拢切。
背景
Java誕生至今虚循,有好多種IO模型,從最早的Java IO到后來的Java NIO以及最新的Java AIO棍现,每種IO模型都有它自己的特點(diǎn)调煎,詳情請(qǐng)看我的上篇文章Java IO初探,而其中的的Java NIO應(yīng)用非常廣泛己肮,尤其是在高并發(fā)領(lǐng)域士袄,比如我們常見的Netty,Mina等框架谎僻,都是基于它實(shí)現(xiàn)的窖剑,相信大家都有所了解,下面讓我們來看看Java NIO的具體架構(gòu)戈稿。
Java NIO架構(gòu)
其實(shí)Java NIO模型相對(duì)來說也還是比較簡(jiǎn)單的,它的核心主要有三個(gè)讶舰,分別是:Selector鞍盗、Channel和Buffer,我們先來看看它們之間的關(guān)系:
它們之間的關(guān)系很清晰,一個(gè)線程對(duì)應(yīng)著一個(gè)Selector跳昼,一個(gè)Selector對(duì)應(yīng)著多個(gè)Channel般甲,一個(gè)Channel對(duì)應(yīng)著一個(gè)Buffer,當(dāng)然這只是通常的做法鹅颊,一個(gè)Channel也可以對(duì)應(yīng)多個(gè)Selector敷存,一個(gè)Channel對(duì)應(yīng)著多個(gè)Buffer。
Selector
個(gè)人認(rèn)為Selector是Java NIO的最大特點(diǎn)堪伍,之前我們說過锚烦,傳統(tǒng)的Java IO在面對(duì)大量IO請(qǐng)求的時(shí)候有心無力,因?yàn)槊總€(gè)維護(hù)每一個(gè)IO請(qǐng)求都需要一個(gè)線程帝雇,這帶來的問題就是涮俄,系統(tǒng)資源被極度消耗,吞吐量直線下降尸闸,引起系統(tǒng)相關(guān)問題彻亲,那么Java NIO是如何解決這個(gè)問題的呢?答案就是Selector吮廉,簡(jiǎn)單來說它對(duì)應(yīng)著多路IO復(fù)用中的監(jiān)管角色苞尝,它負(fù)責(zé)統(tǒng)一管理IO請(qǐng)求,監(jiān)聽相應(yīng)的IO事件宦芦,并通知對(duì)應(yīng)的線程進(jìn)行處理宙址,這種模式下就無需為每個(gè)IO請(qǐng)求單獨(dú)分配一個(gè)線程,另外也減少線程大量阻塞调卑,資源利用率下降的情況曼氛,所以說Selector是Java NIO的精髓豁辉,在Java中我們可以這么寫:
// 打開服務(wù)器套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 服務(wù)器配置為非阻塞
ssc.configureBlocking(false);
// 進(jìn)行服務(wù)的綁定
ssc.bind(new InetSocketAddress("localhost", 8001));
// 通過open()方法找到Selector
Selector selector = Selector.open();
// 注冊(cè)到selector,等待連接
ssc.register(selector, SelectionKey.OP_ACCEPT);
...
Channel
Channel本意是通道的意思舀患,簡(jiǎn)單來說徽级,它在Java NIO中表現(xiàn)的就是一個(gè)數(shù)據(jù)通道,但是這個(gè)通道有一個(gè)特點(diǎn)聊浅,那就是它是雙向的餐抢,也就是說,我們可以從通道里接收數(shù)據(jù)低匙,也可以向通道里寫數(shù)據(jù)旷痕,不用像Java BIO那樣,讀數(shù)據(jù)和寫數(shù)據(jù)需要不同的數(shù)據(jù)通道顽冶,比如最常見的Inputstream和Outputstream欺抗,但是它們都是單向的,Channel作為一種全新的設(shè)計(jì)强重,它幫助系統(tǒng)以相對(duì)小的代價(jià)來保持IO請(qǐng)求數(shù)據(jù)傳輸?shù)奶幚斫食剩撬⒉徽嬲娣艛?shù)據(jù),它總是結(jié)合著緩存區(qū)(Buffer)一起使用间景,另外Channel主要有以下四種:
- FileChannel:讀寫文件時(shí)使用的通道
- DatagramChannel:傳輸U(kuò)DP連接數(shù)據(jù)時(shí)的通道,與Java IO中的DatagramSocket對(duì)應(yīng)
- SocketChannel:傳輸TCP連接數(shù)據(jù)時(shí)的通道佃声,與Java IO中的Socket對(duì)應(yīng)
- ServerSocketChannel: 監(jiān)聽套接詞連接時(shí)的通道,與Java IO中的ServerSocket對(duì)應(yīng)
當(dāng)然其中最重要以及最常用的就是SocketChannel和ServerSocketChannel倘要,也是Java NIO的精髓圾亏,ServerSocketChannel可以設(shè)置成非阻塞模式,然后結(jié)合Selector就可以實(shí)現(xiàn)多路復(fù)用IO封拧,使用一個(gè)線程管理多個(gè)Socket連接志鹃,具體使用可以參數(shù)上面的代碼。
Buffer
顧名思義泽西,Buffer的含義是緩沖區(qū)弄跌,它在Java NIO中的主要作用就是作為數(shù)據(jù)的緩沖區(qū)域,Buffer對(duì)應(yīng)著某一個(gè)Channel尝苇,從Channel中讀取數(shù)據(jù)或者向Channel中寫數(shù)據(jù)铛只,Buffer與數(shù)組很類似,但是它提供了更多的特性糠溜,方便我們對(duì)Buffer中的數(shù)據(jù)進(jìn)行操作淳玩,后面我也會(huì)主要分析它的三個(gè)屬性capacity,position和limit非竿,我們先來看一下Buffer分配時(shí)的類別(這里不是指Buffer的具體數(shù)據(jù)類型)即Direct Buffer和Heap Buffer蜕着,那么為什么要有這兩種類別的Buffer呢?我們先來看看它們的特性:
Direct Buffer:
- 直接分配在系統(tǒng)內(nèi)存中;
- 不需要花費(fèi)將數(shù)據(jù)庫從內(nèi)存拷貝到Java內(nèi)存中的成本承匣;
- 雖然Direct Buffer是直接分配中系統(tǒng)內(nèi)存中的蓖乘,但當(dāng)它被重復(fù)利用時(shí),只有真正需要數(shù)據(jù)的那一頁數(shù)據(jù)會(huì)被裝載到真是的內(nèi)存中韧骗,其它的還存在在虛擬內(nèi)存中嘉抒,不會(huì)造成實(shí)際內(nèi)存的資源浪費(fèi);
- 可以結(jié)合特定的機(jī)器碼袍暴,一次可以有順序的讀取多字節(jié)單元些侍;
- 因?yàn)橹苯臃峙湓谙到y(tǒng)內(nèi)存中,所以它不受Java GC管理政模,不會(huì)自動(dòng)回收岗宣;
- 創(chuàng)建以及銷毀的成本比較高;
Heap Buffer:
- 分配在Java Heap淋样,受Java GC管理生命周期耗式,不需要額外維護(hù);
- 創(chuàng)建成本相對(duì)較低趁猴;
根據(jù)它們的特性刊咳,我們可以大致總結(jié)出它們的適用場(chǎng)景:
如果這個(gè)Buffer可以重復(fù)利用,而且你也想多個(gè)字節(jié)操作躲叼,亦或者你對(duì)性能要求很高,可以選擇使用Direct Buffer企巢,但其編碼相對(duì)來說會(huì)比較復(fù)雜枫慷,需要注意的點(diǎn)也更多,反之則用Heap Buffer浪规,Buffer的相應(yīng)創(chuàng)建方法:
//創(chuàng)建Heap Buffer
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
//創(chuàng)建Direct Buffer
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
下面我們來看看它的三個(gè)屬性:
- Capacity:顧名思義它的含義是容量或听,代表著Buffer的最大容量,與數(shù)組的Size很類似笋婿,初始化不可更改誉裆,除非你改變的Buffer的結(jié)構(gòu);
- Limit:顧名思義它的含義是界限缸濒,代表著Buffer的目前可使用的最大限制足丢,寫模式下,一般Limit等于Capacity庇配,讀模式下需要你自己控制它的值結(jié)合position讀取想要的數(shù)據(jù)斩跌;
- Position:顧名思義它的含義是位置,代表著Buffer目前操作的位置捞慌,通俗來說耀鸦,就是你下次對(duì)Buffer進(jìn)行操作的起始位置;
接下來我會(huì)用一個(gè)圖解的列子幫助大家理解,現(xiàn)在我們假設(shè)有一個(gè)容量為10的Buffer啸澡,我們先往里面寫入一定字節(jié)的數(shù)據(jù)袖订,然后再根據(jù)編碼規(guī)則從其中讀取我們需要的數(shù)據(jù):
1.初始Buffer:
ByteBuffer buffer = ByteBuffer.allocate(10);
2.向Buffer中寫入兩個(gè)字節(jié):
buffer.put("my".getBytes());
3.再Buffer中寫入四個(gè)字節(jié):
buffer.put("blog".getBytes());
4.現(xiàn)在我們需要從Buffer中獲取數(shù)據(jù)氮帐,首先我們先將寫模式轉(zhuǎn)換為讀模式:
buffer.flip();
我們來看看flip()方法到底做了什么事?
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
從源碼中可以看出洛姑,flip方法根據(jù)Buffer目前的相應(yīng)屬性來修改對(duì)應(yīng)的屬性上沐,所以flip()方法之后,Buffer目前的狀態(tài):
5.接著我們從Buffer中讀取數(shù)據(jù)
從Buffer中讀取數(shù)據(jù)有多種方式吏口,比如get(),get(byte [])等奄容,相關(guān)的具體方法使用可以參考Buffer的官方API文檔,這里我們用最簡(jiǎn)單的get()來獲取數(shù)據(jù):
byte a = buffer.get();
byte b = buffer.get();
此時(shí)Buffer的狀態(tài)如下圖所示:
我們可以按照這種方式讀取完我們所需數(shù)據(jù)产徊,最終調(diào)用clear()方法將Buffer置為初始狀態(tài)昂勒。
總結(jié)
這篇文章主要講解了Java NIO中重要的三個(gè)組成部分,在實(shí)際使用過程也是比較重要的舟铜,掌握它們之間的關(guān)系戈盈,可以讓你對(duì)Java NIO的整個(gè)架構(gòu)更加熟悉,理解相對(duì)來說也會(huì)更加深刻谆刨,并分析了這種模式是如何與多路復(fù)用IO模型的映射塘娶,了解Java NIO在高并發(fā)場(chǎng)景下優(yōu)勢(shì)的原因。