NIO 是什么?
nio 是 non-blocking 的簡(jiǎn)稱,在 jdk1.4 里提供的新 api。Sun 官方標(biāo)榜的特性如下:為所有的原始類型提供(Buffer)緩存支持。字符集編碼解碼解決方案。Channel:一個(gè)新的原始 I/O 抽象。支持鎖和內(nèi)存映射文件的文件訪問(wèn)接口。提供多路(non-blocking)非阻塞式的高伸縮性 I/O。
NIO 實(shí)現(xiàn)高性能處理的原理是使用較少的線程來(lái)處理更多的任務(wù)。使用較少的 Thread 線程,通過(guò) Selector 選擇器來(lái)執(zhí)行不同的 Channel 通道中的任務(wù),執(zhí)行的任務(wù)再結(jié)合 AIO(異步 I/O)就能發(fā)揮服務(wù)器最大的性能,大大提升軟件運(yùn)行效率。
Java NIO
Java NIO 采用非阻塞高性能運(yùn)行的方式來(lái)避免出現(xiàn)以前“笨拙”的同步I/O帶來(lái)的低效率問(wèn)題。NIO在大文件操作上相比常規(guī)I/O更加優(yōu)秀。
Buffer
基礎(chǔ)知識(shí)點(diǎn)
在使用傳統(tǒng)的 I/O 操作時(shí),比如 InputStream/OutputStream ,通常是將數(shù)據(jù)暫存到 byte[] 或者 char[] 中,亦或者從 byte[] 或者 char[] 中來(lái)獲取數(shù)據(jù),但是在 Java 語(yǔ)言中對(duì) array 數(shù)組自身提供的可操作的 API 非常少,常用的操作僅僅是 length 屬性和下標(biāo)[x],如果相對(duì)數(shù)組中的數(shù)據(jù)進(jìn)行更高級(jí)的操作,需要自己寫(xiě)代碼來(lái)實(shí)現(xiàn),處理方式比較原始。而 Java NIO 中的 Buffer 類在暫存數(shù)據(jù)的同時(shí)還提供了很多工具方法,大大提高了程序開(kāi)發(fā)效率。
Buffer 是一個(gè)抽象類,用于存儲(chǔ)基本數(shù)據(jù)類型的容器,每個(gè)基本數(shù)據(jù)類型(除去 boolean )都有一個(gè)子類與之對(duì)應(yīng)。它具有 7 個(gè)直接子類:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。
注意:
Buffer 類沒(méi)有 BooleanBuffer 這個(gè)子類。
StringBuffer 在 java.lang 包下,而在 nio 包下并沒(méi)有,在 Nio 中存儲(chǔ)字符的緩沖區(qū)可以使用 CharBuffer 類。
緩沖區(qū)為非線程安全的。
在 Buffer 中有 4 個(gè)核心技術(shù)點(diǎn):capacity、limit、position、mark。他們之間值的大小關(guān)系如下:
0 <= mark <= position <= limit <= capacity
- capacity:容量。代表該緩沖區(qū)當(dāng)前所能容納的元素的數(shù)量。不能為負(fù)數(shù),且不能更改。
- limit:限制。代表第一個(gè)不應(yīng)該讀取或?qū)懭朐氐?index 索引。不能為負(fù)數(shù),且不能大于其 capacity。
- position:位置。代表下一個(gè)將要讀取或?qū)懭朐氐?index 索引。不能為負(fù)數(shù),且不能大于其 limit 。如果新設(shè)置的 limit 小于 position,那么新的 limit 值就是 limit。
- mark:標(biāo)記。緩沖區(qū)的標(biāo)記是一個(gè)索引,定義標(biāo)記時(shí),不能將其定義為負(fù)數(shù),且不能大于其 position。標(biāo)記并不是必需的,如果定義了 mark,在調(diào)用 reset() 方法時(shí),會(huì)將緩沖區(qū)的 position 重置為該標(biāo)記索引;在將 position 或者 limit 調(diào)整為小于該 mark 的值時(shí),該 mark 會(huì)被丟棄,丟棄后 mark 的值時(shí) -1。如果未定義 mark 調(diào)用 reset() 方法將導(dǎo)致拋出 invalidMarkException 異常。
Buffer 中常用 API
返回值 | 方法名 | 作用 |
int | capacity() | 返回此緩沖區(qū)的容量 |
int | limit() | 返回此緩沖區(qū)的限制 |
Buffer | limit(int newLimit) | 設(shè)置此緩沖區(qū)的限制 |
int | position() | 返回此緩沖區(qū)的位置 |
Buffer | position(int newPosition) | 設(shè)置此緩沖區(qū)的位置 |
Buffer | mark() | 在此緩沖區(qū)的位置設(shè)置標(biāo)記 |
int | remaining() | 返回當(dāng)前位置(position)與限制(limit)之間的元素個(gè)數(shù) return limit – position |
boolean | hasRemaining() | 判斷在當(dāng)前位置和限制之間是否有元素。return position < limit |
boolean | isReadOnly() | 返回此緩沖區(qū)是否為只讀緩沖區(qū) |
boolean | isDirect() | 判斷此緩沖區(qū)是否為直接緩沖區(qū) |
Buffer | clear() | 還原緩沖區(qū)到初始狀態(tài),包含將位置設(shè)置為 0,將限制設(shè)置為容量,丟棄標(biāo)記,即 “一切默認(rèn)”,但不會(huì)清除數(shù)據(jù)。 主要使用場(chǎng)景:在對(duì)緩沖區(qū)存儲(chǔ)數(shù)據(jù)之前調(diào)用此方法 |
Buffer | rewind() | 重繞此緩沖區(qū),將位置設(shè)置為0并丟棄標(biāo)記。 主要使用場(chǎng)景:常在重新讀取緩沖區(qū)數(shù)據(jù)時(shí)使用。 |
Buffer | flip() | 反轉(zhuǎn)此緩沖區(qū)。首先將限制設(shè)置為當(dāng)前位置,然后將位置設(shè)置為0。如果定義了標(biāo)記,則丟棄該標(biāo)記。 主要使用場(chǎng)景:當(dāng)向緩沖區(qū)存儲(chǔ)數(shù)據(jù),然后再?gòu)木彌_區(qū)讀取這些數(shù)據(jù)之前調(diào)用 |
堆內(nèi)存與堆外內(nèi)存
使用間接緩沖區(qū)(堆內(nèi)存)向硬盤(pán)存取數(shù)據(jù)時(shí)需要首先將數(shù)據(jù)復(fù)制暫存到 JVM 的中間緩沖區(qū)中,然后 Java 程序才能對(duì)數(shù)據(jù)進(jìn)行實(shí)際的讀寫(xiě)操作。如果有頻繁操作數(shù)據(jù)的情況發(fā)生,會(huì)提高內(nèi)存占有率,大大降低軟件對(duì)數(shù)據(jù)的吞吐量。
使用非間接緩沖區(qū)(堆外內(nèi)存)無(wú)需 JVM 創(chuàng)建新的中間緩沖區(qū),可直接在內(nèi)核空間完成數(shù)據(jù)的處理,這樣就減少了在 JVM 中創(chuàng)建緩沖區(qū)的步驟,增加了程序運(yùn)行效率。
處理數(shù)據(jù)常用操作
以 ByteBuffer 為例,提供了 5 類操作。
相對(duì) / 絕對(duì)位置操作
相對(duì)位置操作是指在讀取或?qū)懭胍粋€(gè)或多個(gè)元素時(shí),它從“當(dāng)前位置開(kāi)始”,然后將位置增加鎖傳輸?shù)脑貍€(gè)數(shù)。如果請(qǐng)求的傳輸超出限制,則相對(duì) get 操作將拋出 BufferUnderflowException 異常,相對(duì) put 操作將拋出 BufferOverflowException 異常,也就是說(shuō),在這兩種情況下 ,都沒(méi)有數(shù)據(jù)傳輸。
絕對(duì)位置操作采用顯示元素索引,該操作不影響位置。如果索引參數(shù)超出限制,則絕對(duì) get 操作和絕對(duì) put 操作將拋出 IndexOutBoundsException 異常。
返回值類型 | 方法名 | 作用 |
Buffer | put(byte b) | 將給定的字節(jié)寫(xiě)入緩沖區(qū)的“當(dāng)前位置”。 |
byte | get() | 讀取此緩沖區(qū)“當(dāng)前位置”的字節(jié)。 |
Buffer | put(byte[] src, int offset, int length) | 把給定源數(shù)組中的字節(jié)寫(xiě)入此緩沖區(qū)的“當(dāng)前位置中”。如果要從該數(shù)據(jù)中心復(fù)制的字節(jié)數(shù)多于此緩沖區(qū)中的剩余字節(jié)(即 length > remaining),則不傳輸字節(jié)且拋出 bufferOverflowException 異常。否則,將給定數(shù)組中的 length 個(gè)字節(jié)復(fù)制到此緩沖區(qū)中。將數(shù)組中給定 offset 偏移量位置的數(shù)據(jù)復(fù)制到此緩沖區(qū)的當(dāng)前位置,復(fù)制的元素個(gè)數(shù)為 length。 |
byte[] | get(byte[] dst, int offset, int length) | 將此緩沖區(qū)當(dāng)前位置的字節(jié)傳輸?shù)浇o定目標(biāo)數(shù)組中。如果此緩沖區(qū)中剩余的字節(jié)少于滿足請(qǐng)求所需要的字節(jié)(即 length > remaining),則不傳輸字節(jié)且拋出 BufferUnderflowWxception 異常。否則此方法將此緩沖區(qū)中的 length 個(gè)字節(jié)復(fù)制到給定數(shù)組中。從此緩沖區(qū)的當(dāng)前位置和數(shù)組中的給定偏移量位置開(kāi)始復(fù)制。然后,此緩沖區(qū)的位置將增加 length。 |
Buffer | put(byte[] src) | 將給定的源 byte 數(shù)組的所有內(nèi)容存儲(chǔ)到此緩沖區(qū)的當(dāng)前位置。等同于:dst.put(a,0,a.length) |
byte[] | get(byte[] dst) | 將緩沖區(qū) remaining 字節(jié)傳輸?shù)浇o定的目標(biāo)數(shù)組中。等同于:src.get(a,0,a.length) |
Buffer | put(ByteBuffer src) | 相對(duì)批量 put 操作。將給定源緩沖區(qū)中的剩余字節(jié)傳輸?shù)酱司彌_區(qū)當(dāng)前位置中。如果源緩沖區(qū)中的剩余字節(jié)多于此緩沖區(qū)的剩余字節(jié),即 src.remaining() > remaining(),則不傳輸字節(jié)且拋出 BufferOverflowException 異常。兩個(gè)緩沖區(qū)的位置都會(huì)相應(yīng)遞增。 |
Buffer | put(int index, byte b) | 絕對(duì) put 操作,將給定字節(jié)寫(xiě)入此緩沖區(qū)的給定索引位置。 |
byte | get(int index) | 絕對(duì) get 操作,讀取指定位置索引處的字節(jié)。 |
getType / putType 操作
可以直接根據(jù)源基本數(shù)據(jù)類型將數(shù)據(jù)寫(xiě)入此緩沖區(qū),或者從此緩沖區(qū)讀取指定類型的數(shù)據(jù),同時(shí)此為緩沖區(qū)的位置位置根據(jù)不同的數(shù)據(jù)類型所占的字節(jié)數(shù)做相應(yīng)的增加。
返回值類型 | 方法名 | 作用 |
ByteBuffer | putChar(char value) | 相對(duì)操作,將給定 char 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的當(dāng)前位置,然后將此緩沖區(qū)位置增加 2,因?yàn)橐粋€(gè)字符占 2 個(gè)字節(jié)。 |
ByteBuffer | putChar(int index, char value) | 絕對(duì)操作,將給定 char 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的給定位置。 |
ByteBuffer | putDouble(double value) | 相對(duì)操作,將給定 double 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的當(dāng)前位置,然后將此緩沖區(qū)位置增加 8,因?yàn)橐粋€(gè) double 類型占 8 個(gè)字節(jié)。 |
ByteBuffer | putDouble(int index, double value) | 絕對(duì)操作,將給定 double 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的給定位置。 |
ByteBuffer | putFloat(float value) | 相對(duì)操作,將給定 float 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的當(dāng)前位置,然后將此緩沖區(qū)位置增加 4,因?yàn)橐粋€(gè)float 類型占 4 個(gè)字節(jié)。 |
ByteBuffer | putFloat(int index, float value) | 絕對(duì)操作,將給定 float 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的給定位置。 |
ByteBuffer | put(int value) | 相對(duì)操作,將給定 int 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的當(dāng)前位置,然后將此緩沖區(qū)位置增加 4,因?yàn)橐粋€(gè) int 類型占 4 個(gè)字節(jié)。 |
ByteBuffer | put(int index, int value) | 絕對(duì)操作,將給定 int 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的給定位置。 |
ByteBuffer | put(long value) | 相對(duì)操作,將給定 long 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的當(dāng)前位置,然后將此緩沖區(qū)位置增加 8,因?yàn)橐粋€(gè) long 類型占 8 個(gè)字節(jié)。 |
ByteBuffer | put(int index, long value) | 絕對(duì)操作,將給定 long 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的給定位置。 |
ByteBuffer | putShort(short value) | 相對(duì)操作,將給定 short 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的當(dāng)前位置,然后將此緩沖區(qū)位置增加 2,因?yàn)橐粋€(gè) short 類型占 2 個(gè)字節(jié)。 |
ByteBuffer | putShort(int index, short value) | 絕對(duì)操作,將給定 short 值按照當(dāng)前字節(jié)順序?qū)懭氲酱司彌_區(qū)的給定位置。 |
緩沖區(qū)類型轉(zhuǎn)換
通過(guò)調(diào)用 asXXXBuffer() 方法,將源字節(jié)緩沖區(qū)轉(zhuǎn)換成特定類型的緩沖區(qū)。新緩沖區(qū)的內(nèi)容將從此緩沖區(qū)的當(dāng)前位置開(kāi)始。此緩沖區(qū)內(nèi)容的更改,在新緩沖區(qū)中是可見(jiàn)的,反之亦然。這兩個(gè)緩沖區(qū)的位置、限制和標(biāo)記值是相互獨(dú)立的。新緩沖區(qū)的位置將為 0,其容量和限制與所轉(zhuǎn)換的視圖緩沖區(qū)類型有關(guān),比如,將字節(jié)緩沖區(qū)通過(guò) asCharBuffer() 方法轉(zhuǎn)換成字符緩沖區(qū),那么新的字符緩沖區(qū)的容量和限制將為源字節(jié)緩沖區(qū)中所剩字節(jié)數(shù)的 1/2,其標(biāo)記時(shí)不確定的。當(dāng)且僅當(dāng)源緩沖區(qū)為直接緩沖區(qū)時(shí),新緩沖區(qū)才是直接緩沖區(qū);當(dāng)且僅當(dāng)源緩沖區(qū)是只讀緩沖區(qū)時(shí),新緩沖區(qū)才是只讀緩沖區(qū)。
注意:
當(dāng)緩沖區(qū)類型轉(zhuǎn)換后,再讀取時(shí),需要注意其讀寫(xiě)時(shí)的編碼,如果編碼不一致,會(huì)導(dǎo)致中文亂碼。解決辦法就是調(diào)整在轉(zhuǎn)換前后的讀寫(xiě)編碼一致。
返回值類型 | 方法名 | 作用 |
CharBuffer | asCharBuffer() | 創(chuàng)建此字節(jié)緩沖區(qū)的視圖,作為 char 緩沖區(qū)。新的字符緩沖區(qū)的容量和限制將為源字節(jié)緩沖區(qū)中所剩字節(jié)數(shù)的 1/2 |
DoubleBuffer | asDoubleBuffer() | 創(chuàng)建此字節(jié)緩沖區(qū)的視圖,作為 double 緩沖區(qū)。新的字符緩沖區(qū)的容量和限制將為源字節(jié)緩沖區(qū)中所剩字節(jié)數(shù)的 1/8 |
FloatBuffer | asFloatBuffer() | 創(chuàng)建此字節(jié)緩沖區(qū)的視圖,作為 float 緩沖區(qū)。新的字符緩沖區(qū)的容量和限制將為源字節(jié)緩沖區(qū)中所剩字節(jié)數(shù)的 1/4 |
IntBuffer | asIntBuffer() | 創(chuàng)建此字節(jié)緩沖區(qū)的視圖,作為 int 緩沖區(qū)。新的字符緩沖區(qū)的容量和限制將為源字節(jié)緩沖區(qū)中所剩字節(jié)數(shù)的 1/4 |
LongBuffer | asLongBuffer | 創(chuàng)建此字節(jié)緩沖區(qū)的視圖,作為 long 緩沖區(qū)。新的字符緩沖區(qū)的容量和限制將為源字節(jié)緩沖區(qū)中所剩字節(jié)數(shù)的 1/8 |
ShortBuffer | asShortBuffer() | 創(chuàng)建此字節(jié)緩沖區(qū)的視圖,作為 float 緩沖區(qū)。新的字符緩沖區(qū)的容量和限制將為源字節(jié)緩沖區(qū)中所剩字節(jié)數(shù)的 1/2 |
只讀緩沖區(qū)
通過(guò) asReadOnlyBuffer() 方法創(chuàng)建共享此緩沖區(qū)內(nèi)容的只讀緩沖區(qū)。新緩沖區(qū)的內(nèi)容將為此緩沖區(qū)的內(nèi)容。此緩沖區(qū)內(nèi)容的更改在新緩沖區(qū)中是可見(jiàn)的,但是新緩沖區(qū)是只讀,不允許修改共享內(nèi)容。兩個(gè)緩沖區(qū)的位置、限制和標(biāo)記值是相互獨(dú)立的。新緩沖區(qū)的容量、限制、位置和標(biāo)記值將于此緩沖區(qū)相同。
壓縮緩沖區(qū)
將緩沖區(qū)的當(dāng)前位置和限制之間的字節(jié)(如果有)復(fù)制到緩沖區(qū)的開(kāi)始處。即將所有 p = position() 處的字節(jié)復(fù)制到索引 0 處,將索引 p+1 處的字節(jié)復(fù)制到索引 1 處,以此類推,直到將索引 limit() – 1 處的字節(jié)復(fù)制到索引 n = limit() – 1 – p 處。然后,將緩沖區(qū)的位置設(shè)置為 n + 1 ,并且將其限制設(shè)置為其容量。如果已定義了標(biāo)記,則丟棄它。
// 1. 緩沖區(qū)中的內(nèi)容|1|2|3|4|5|6|7|8|9|// 2. 執(zhí)行讀取操作到索引 3 處|1|2|3|>4|5|6|7|8|9|// 3. 經(jīng)過(guò) compact 壓縮后緩沖區(qū)數(shù)據(jù)內(nèi)容為|4|5|6|7|8|9|7|8|9|
復(fù)制緩沖區(qū)
通過(guò) duplicate() 方法創(chuàng)建共享此緩沖區(qū)內(nèi)容的新的緩沖區(qū)。新緩沖區(qū)的內(nèi)容將為此緩沖區(qū)的內(nèi)容。此緩沖區(qū)內(nèi)容的更改在新緩沖區(qū)中是可見(jiàn)的,反之亦然。在創(chuàng)建新緩沖區(qū)時(shí),容量、限制、位置和標(biāo)記值將與此緩沖區(qū)相同,但是這兩個(gè)緩沖區(qū)的位置、限制和標(biāo)記值是相互獨(dú)立的。當(dāng)且僅當(dāng)此緩沖區(qū)為直接緩沖區(qū)時(shí),新緩沖區(qū)才是直接緩沖區(qū);當(dāng)且僅當(dāng)此緩沖區(qū)為只讀緩沖區(qū)時(shí),新緩沖區(qū)才是只讀緩沖區(qū)。
截取緩沖區(qū)
通過(guò) slice() 方法創(chuàng)建新的字節(jié)緩沖區(qū),其內(nèi)容是此緩沖區(qū)內(nèi)容的共享子序列。新的緩沖區(qū)內(nèi)容將從此緩沖區(qū)的當(dāng)前位置開(kāi)始。此緩沖區(qū)內(nèi)容的更改在新緩沖區(qū)中是可見(jiàn)的,反之亦然。這兩個(gè)緩沖區(qū)的位置、限制和標(biāo)記是相互獨(dú)立的。新緩沖區(qū)的位置將為 0,其容量和限制為此緩沖區(qū)中所剩余的字節(jié)數(shù)量,標(biāo)記是不確定的。當(dāng)且僅當(dāng)此緩沖區(qū)為直接緩沖區(qū)時(shí),新緩沖區(qū)才是直接緩沖區(qū);當(dāng)且僅當(dāng)此緩沖區(qū)為只讀緩沖區(qū)時(shí),新緩沖區(qū)才是只讀緩沖區(qū)。
比較緩沖區(qū)的內(nèi)容
比較緩沖區(qū)內(nèi)容是否相同有兩種方法:equals() 和 compareTo()。這兩種方法還是有使用細(xì)節(jié)上的區(qū)別。
public boolean equals(Object ob) { // 1. 如果比較的是同一個(gè)對(duì)象,則直接返回 true if (this == ob) return true; // 2. 如果所比較的對(duì)象非 ByteBuffer 類型對(duì)象,直接返回 false if (!(ob instanceof ByteBuffer)) return false; // 3. 將所比較的對(duì)象轉(zhuǎn)換成 ByteBuffer 類型,然后比較兩者剩余元素個(gè)數(shù),即 remaing 值,如果不相等,直接返回 false ByteBuffer that = (ByteBuffer)ob; if (this.remaining() != that.remaining()) return false; // 4. 倒敘逐個(gè)比較兩個(gè) ByteBuffer 對(duì)象剩余元素是否相同,如果有一個(gè)不同,則直接返回 false。 int p = this.position(); for (int i = this.limit() – 1, j = that.limit() – 1; i >= p; i–, j–) if (!equals(this.get(i), that.get(j))) return false; return true;}
從源碼中可以看出 equals() 方法比較的是兩個(gè) ByteBuffer 對(duì)象中剩余元素是否相等,包括個(gè)數(shù)及每個(gè)元素的序列值。而兩個(gè)緩沖區(qū)的容量可以不同。
public int compareTo(ByteBuffer that) { /** * 1.在此緩沖區(qū)的基礎(chǔ)上,計(jì)算需要比較的元素終點(diǎn)位置。 * 以當(dāng)前位置為起點(diǎn),加上兩個(gè) ByteBuffer 對(duì)象最小的剩余元素個(gè)數(shù)為終點(diǎn) * 說(shuō)明判斷范圍是兩個(gè) ByteBuffer 對(duì)象的 remaining 的交集 **/ int n = this.position() + Math.min(this.remaining(), that.remaining()); // 2. 正序比較兩個(gè) ByteBuffer 對(duì)象 remaining 交集中的元素是否相同,如果有一個(gè)不同,返回兩者的差值 thisIndex – thatIndex for (int i = this.position(), j = that.position(); i < n; i++, j++) { int cmp = compare(this.get(i), that.get(j)); if (cmp != 0) return cmp; } // 3. 如果交集中的元素都相同,那么比較兩個(gè) ByteBuffer 對(duì)象的 remaining 元素個(gè)數(shù),返回兩者的差值 thisRemaining – thatRemaining return this.remaining() – that.remaining();}
源碼可以看出 compareTo 方法也是比較的兩個(gè) ByteBuffer 對(duì)象的剩余元素,只不過(guò)返回的是某個(gè)字節(jié)序列的差值或者兩個(gè) remaining 的差值。與兩個(gè)緩沖區(qū)的容量無(wú)關(guān),這一點(diǎn)與 equals() 方法一致。
- 如果兩個(gè) ByteBuffer 對(duì)象的 remaining 交集中有一個(gè)元素的序列值不相等,那么返回他們的差值。
- 如果兩個(gè) ByteBuffer 對(duì)象的 remaining 交集中的所有元素的序列值都相等,在進(jìn)行比較兩個(gè) ByteBuffer 對(duì)象的 remaining 個(gè)數(shù),并返回他們的差值。
Channel
緩沖區(qū)是將數(shù)據(jù)進(jìn)行打包,而通道是將數(shù)據(jù)進(jìn)行傳輸。緩沖區(qū)是類,而通道都是接口,因?yàn)橥ǖ赖墓δ軐?shí)現(xiàn)是要依賴操作系統(tǒng)的,Channel 接口只定義有哪些功能,而功能的具體實(shí)現(xiàn)在不同的操作系統(tǒng)中是不一樣的。
通道是用于 IO 操作的連接,可處于打開(kāi)或關(guān)閉兩種狀態(tài),當(dāng)創(chuàng)建通道時(shí),通道就處于打開(kāi)狀態(tài),一旦將其關(guān)閉,則保持關(guān)閉狀態(tài)。通過(guò) isOpen() 方法可以測(cè)試通道是否處于打開(kāi)狀態(tài),避免出現(xiàn) ClosedChannelException 異常。
Channel 接口類圖結(jié)構(gòu)
Channel接口類圖
AutoCloseable 接口
AutoCloseable 接口的作用是可以自動(dòng)關(guān)閉,而不需要顯示地調(diào)用 close() 方法。AutoCloseable 接口強(qiáng)調(diào)的是與 try() 結(jié)合實(shí)現(xiàn)自動(dòng)關(guān)閉。該接口之定義了一個(gè) close() 方法,因?yàn)獒槍?duì)的是任何資源的關(guān)閉,而不只是 I/O,因此 close() 方法拋出的是 Exception 異常。而且該接口不要求是冪等的,也就是重復(fù)調(diào)用此接口的 close() 方法會(huì)出現(xiàn)副作用。
public class DBOperate implements AutoCloseable { @Override public void close() throws Exception { System.out.println(“關(guān)閉連接”); }}public class Test { public static void main(String[] args){ try (DBOprate dbo = new DBOprate()){ System.out.println(“開(kāi)始數(shù)據(jù)庫(kù)操作”); }catch (Exception e){ e.printStackTrace(); } }}//輸出結(jié)果:開(kāi)始數(shù)據(jù)庫(kù)操作關(guān)閉連接
Closeable 接口
Closeable 接口繼承自 AutoCloseable 接口,其作用是關(guān)閉 I/O 流,釋放系統(tǒng)資源,所以該接口的 close() 方法拋出 IOException 異常。該接口的 close() 方法是冪等的,可以重復(fù)調(diào)用此接口的 close() 方法,而不會(huì)出現(xiàn)任何效果與影響。
AsynchronousChannel 接口
主要作用是使通道支持異步 I/O 操作。異步 I/O 操作有以下兩種方式進(jìn)行實(shí)現(xiàn):
- 方法Future operation(…)Future 對(duì)象可以用于檢測(cè) I/O 操作是否完成,或者等待完成,以及用于接收 I/O 操作處理后的結(jié)果。但是需要開(kāi)發(fā)人員編寫(xiě)檢測(cè)邏輯。
- 回調(diào)void operation(… A attachment, CompletionHandler handler)A 類型的對(duì)象 attachment 的主要作用是讓外部與 CompletionHandler 對(duì)象內(nèi)部進(jìn)行通信。有點(diǎn)是 CompletionHandler 對(duì)象可以被復(fù)用,當(dāng) I/O 操作成功或失敗時(shí),CompletionHandler 對(duì)象中的指定方法會(huì)自動(dòng)被調(diào)用,不需要開(kāi)發(fā)人員編寫(xiě)檢測(cè)的邏輯。
異步通道在多線程并發(fā)的情況下是線程安全的。某些通道的實(shí)現(xiàn)是可以支持并發(fā)讀和寫(xiě)的,但是不允許在一個(gè)未完成的 I/O 操作上再次調(diào)用 read 或 wwrite 操作。
異步通道支持取消操作,通過(guò)調(diào)用 Future 接口定義的 cancel() 方法來(lái)取消執(zhí)行,這會(huì)導(dǎo)致那些等待處理 I/O 結(jié)果的線程拋出 CancellationException 異常。
AsynchronousByteChannel 接口
主要作用是使通道支持異步 I/O 操作,操作單位為字節(jié)。在上一個(gè) read() 或 write() 方法未完成之前再次調(diào)用,會(huì)拋出 ReadPendingException 或者 WritePendingException 異常。
ByteBuffer 類不是線程安全的,盡量保證在對(duì)其進(jìn)行讀寫(xiě)操作時(shí),沒(méi)有其他線程一同進(jìn)行讀寫(xiě)操作。
ReadableByteChannel 接口
主要作用是使通道運(yùn)行對(duì)字節(jié)進(jìn)行讀操作。該接口只允許有 1 個(gè)讀操作在進(jìn)行,如果 1 個(gè)線程正在 1 個(gè)通道上執(zhí)行 1 個(gè) read() 操作,那么任何試圖發(fā)起另一個(gè) read() 操作的線程都會(huì)被阻塞,直到第 1 個(gè) read() 操作完成。即該接口的 read() 方法是同步的。
該通道只接受以字節(jié)為單位的數(shù)據(jù)處理,因?yàn)橥ǖ篮筒僮飨到y(tǒng)進(jìn)行交互時(shí),操作系統(tǒng)只接受字節(jié)數(shù)據(jù)。
ScatteringByteChannel 接口
主要作用是可以從通道中讀取字節(jié)到多個(gè)緩沖區(qū)中。
WritableByteChannel 接口
主要作用是使通道運(yùn)行對(duì)字節(jié)進(jìn)行寫(xiě)操作。將字節(jié)緩沖區(qū)中的字節(jié)序列寫(xiě)入到通道的當(dāng)前位置,該接口只允許有 1 個(gè)寫(xiě)操作在進(jìn)行,如果 1 個(gè)線程正在 1 個(gè)通道上執(zhí)行 1 個(gè) write() 操作,那么任何試圖發(fā)起另一個(gè) write() 操作的線程都會(huì)被阻塞,直到第 1 個(gè) write() 操作完成。即該接口的 write() 方法是同步的。
GatheringByteChannel 接口
主要作用是可以將多個(gè)緩沖區(qū)中的數(shù)據(jù)寫(xiě)入到通道中。
ByteChannel 接口
主要作用是將 ReadableByteChannel(可讀字節(jié)通道)與 WritableByteChannel(可寫(xiě)字節(jié)通道)的規(guī)范進(jìn)行了統(tǒng)一。ByteChannel 沒(méi)有添加任何新的方法就實(shí)現(xiàn)了具有讀和寫(xiě)的功能,是雙向的操作。
SeekableByteChannel 接口
主要作用是在字節(jié)通道中維護(hù) position,以及允許 position 發(fā)生改變。
NetworkChannel 接口
主要作用是使通道與 Socket 進(jìn)行關(guān)聯(lián),使通道中的數(shù)據(jù)能在 Socket 技術(shù)上進(jìn)行傳輸。
MulticastChannel 接口
主要作用是使通道支持 Internet Protocol(IP) 多播。也就是將多個(gè)主機(jī)地址進(jìn)行打包,形成一個(gè)組(group),然后將 IP 報(bào)文向這個(gè)組進(jìn)行發(fā)送,也就相當(dāng)于同時(shí)向多個(gè)主機(jī)傳輸數(shù)據(jù)。
InterruptibleChannel 接口
主要作用是使通道能以異步的方式進(jìn)行關(guān)閉與中斷。
FileChannel 類的使用
以 FileChannel 為例來(lái)介紹下通道(Channel)的一般常用操作,不同的通道雖然 Channel 類型不同,但是在程序中所起到的作用是相同的,再結(jié)合上述的接口類圖,根據(jù)不同類型的 Channel 接口實(shí)現(xiàn)可以實(shí)現(xiàn)特定的功能。
FileChannel類圖
通過(guò)類圖分析得知 FileChannel 是一個(gè)可以讀取、寫(xiě)入、可中斷和操作文件的通道:
- 實(shí)現(xiàn)了 WritebleByteChannel 和 ReadableByteChannel 類型的接口,說(shuō)明支持讀和寫(xiě)操作;
- 繼承了 AbstractInterruptibleChannel 類,說(shuō)明是一個(gè)可中斷的通道;
- 沒(méi)有實(shí)現(xiàn) AsynchronousChannel 類型的接口,所以 FileChnnel 不支持異步,永遠(yuǎn)是阻塞的操作;
FileChannel 在內(nèi)部維護(hù)當(dāng)前文件的 position ,可對(duì)其進(jìn)行查詢和修改。該文件本身包含一個(gè)可讀寫(xiě)、長(zhǎng)度可變的字節(jié)序列,并且可以查詢?cè)撐募漠?dāng)前大小。當(dāng)寫(xiě)入的字節(jié)超出文件的當(dāng)前大小時(shí),則增加文件的大?。唤厝≡撐募r(shí),則減小文件的大??;但此類未定義訪問(wèn)元數(shù)據(jù)的方法,所以無(wú)法訪問(wèn)文件的元數(shù)據(jù),比如:權(quán)限、內(nèi)容類型和最后修改時(shí)間等。
除了通道常見(jiàn)的讀、寫(xiě)和關(guān)閉操作外,此類還定義了下列特定于文件的操作:
FileChannel 類沒(méi)有定義打開(kāi)現(xiàn)有文件通道或創(chuàng)建新文件通道的方法,可通過(guò)調(diào)用現(xiàn)有的 FileInputStream、FileOutputStream、或 RandomAccessFile 對(duì)象的 getChannel() 方法來(lái)獲得。
- 通過(guò) FiltInputStream 實(shí)例的 getChannel() 方法獲得的通道將允許進(jìn)行讀取操作;
- 通過(guò) FileOutputStream 實(shí)例的 getChannel() 方法獲得的通道將允許進(jìn)行寫(xiě)入操作,如果輸出流對(duì)象是通過(guò) FileOutputStream(File,boolean) 構(gòu)造方法且第二個(gè)參數(shù)傳入 true 創(chuàng)建的,則該通道模式可能處于添加模式,每次調(diào)用相關(guān)的寫(xiě)入操作都會(huì)首先將位置移動(dòng)到文件的末尾,然后寫(xiě)入請(qǐng)求的數(shù)據(jù);
- 通過(guò)調(diào)用通過(guò) r 模式創(chuàng)建的 RandomAccessFile 實(shí)例的 getChannel() 方法獲得的通道將允許進(jìn)行讀取操作,RandomAccessFile 實(shí)例是通過(guò) rw 模式創(chuàng)建,那么獲得的通道將允許進(jìn)行讀取和寫(xiě)入操作;
返回值類型 | 方法名 | 作用 |
int | write(ByteBuffer src) | 同步方法,將給定 ByteBuffer 的 remaining 字節(jié)序列寫(xiě)入到通道的當(dāng)前位置; |
int | read(ByteBuffer dst) | 同步方法,將字節(jié)序列從通道的當(dāng)前位置讀入給定的緩沖區(qū)的當(dāng)前位置,如果該通道已到達(dá)流的末尾,則返回 -1;正數(shù) – 代表讀入緩沖區(qū)的字節(jié)個(gè)數(shù);0 – 代表從通道中沒(méi)有讀取任何字節(jié),可能發(fā)生的情況就是緩沖區(qū)中沒(méi)有 remaining 剩余空間了;-1 – 代表到達(dá)流的末端; |
long | write(ByteBuffer[] srcs) | 同步方法,將給定的緩沖區(qū)數(shù)組中的每個(gè)緩沖區(qū)的 remaining 字節(jié)序列寫(xiě)入到通道的當(dāng)前位置; |
long | read(ByteBuffer[] dsts) | 同步方法,從此通道當(dāng)前位置開(kāi)始將通道中剩余的字節(jié)序列,讀入到多個(gè)給定的字節(jié)緩沖區(qū)中;如果通道中可讀出來(lái)的數(shù)據(jù)大于 ByteBuffer[] 緩沖區(qū)組總共的容量,那么 ByteBuffer[] 緩沖區(qū)組總共的容量多少,就讀取多少字節(jié)的數(shù)據(jù) |
long | write(ByteBuffer[] srcs, int offset, int length) | 同步方法,以指定緩沖區(qū)數(shù)組的 offset 下標(biāo)開(kāi)始,向后使用 length 個(gè)字節(jié)緩沖區(qū),再將每個(gè)緩沖區(qū)的 remaining 剩余字節(jié)序列寫(xiě)入到此通道的當(dāng)前位置; |
long | read(ByteBuffer[] dsts, int offset, int length) | 同步方法,將通道中當(dāng)前位置的字節(jié)序列讀入以下標(biāo)為 offset 開(kāi)始的 ByteBuffer[] 數(shù)組中的 remaining 剩余空間中,并且連續(xù)寫(xiě)入 length 個(gè) ByteBuffer 緩沖區(qū); |
int | write(ByteBuffer src, long position) | 將緩沖區(qū)的 remaining 字節(jié)序列寫(xiě)入通道的指定位置;如果給定的位置大于該文件的當(dāng)前大小,則該文件將擴(kuò)大以容納新的字節(jié),在文件末尾和新寫(xiě)入字節(jié)之間的字節(jié)值是未指定的;該方法不影響此通道的當(dāng)前位置; |
int | read(ByteBuffer dst, long position) | 將通道的指定位置的字節(jié)序列讀入給定的緩沖區(qū)的當(dāng)前位置;如果給定的位置大于該文件的當(dāng)前大小,則不讀取任何字節(jié);該方法不影響此通道的當(dāng)前位置; |
long | position(long newPosition) | 設(shè)置此通道的當(dāng)前位置。當(dāng)設(shè)置為大于當(dāng)前文件大小的值時(shí),并不會(huì)改變文件的大小,稍后試圖在這樣的位置讀取字節(jié)將立即返回已到達(dá)文件末尾的指示,稍后試圖在這樣的位置寫(xiě)入字節(jié)將導(dǎo)致文件擴(kuò)大,以容納新的字節(jié),在原來(lái)的文件位置和新設(shè)置的文件位置之間的字節(jié)值時(shí)未指定的; |
long | size() | 返回此通道所關(guān)聯(lián)文件的當(dāng)前大?。?/p> |
FileChannel | truncate(long sieze) | 將此通道所關(guān)聯(lián)文件截取為給定大小。如果給定大小小于該文件的當(dāng)前大小,則截取該文件,丟棄文件新末尾后面的所有字節(jié);如果給定大小大于或等于該文件的當(dāng)前大小,則不修改文件;如果該通道的位置大于給定的大小,則將位置設(shè)置為給定的大??; |
long | transferTo(long position, long count, WritableByteChannel dest) | 將字節(jié)從此通道的文件傳輸?shù)浇o定的可寫(xiě)入字節(jié)通道。讀取從此通道的文件中給定 position 處開(kāi)始的 count 個(gè)字節(jié),并將其寫(xiě)入目標(biāo)通道的當(dāng)前位置。如果此通道的文件從給定的 position 處開(kāi)始包含的字節(jié)數(shù)小于 count 個(gè)字節(jié),或者如果目標(biāo)通道是非阻塞的并且其輸出緩沖區(qū)中的自由空間少于 count 個(gè)字節(jié),則所傳輸?shù)淖止?jié)數(shù)小于請(qǐng)求的字節(jié)數(shù);該方法不影響此通道的當(dāng)前位置;如果給定的 position 位置大于當(dāng)前文件的大小,則不傳輸任何字節(jié); |
long | transferFrom(ReadableByteChannel src, long position, long count) | 將字節(jié)從給定的可讀取字節(jié)通道傳輸?shù)酱送ǖ赖奈募?。試著從源通?src 中最多讀取 count 個(gè)字節(jié),并將其寫(xiě)入到此通道的文件中從給定的 position 處開(kāi)始的位置;如果源通道的剩余空間小于 count 個(gè)字節(jié),或者如果源通道是非阻塞的并且其輸入緩沖區(qū)中直接可用的空間小于 count 個(gè)字節(jié),則所傳輸?shù)淖止?jié)數(shù)要小于請(qǐng)求的字節(jié)數(shù);如果給定的位置大于該文件的當(dāng)前大小,則不傳輸任何字節(jié);該方法不影響此通道的當(dāng)前位置; |
FileLock | lock(long position, long size, boolean shared) | 同步方法,獲取此通道的文件給定區(qū)域上的鎖定。在可以鎖定該區(qū)域之前、已關(guān)閉此通道之前或者已中斷調(diào)用線程之前(以先到者為準(zhǔn)),將阻塞此方法的調(diào)用;在此方法調(diào)用期間,如果另一個(gè)線程關(guān)閉了此通道,則拋出 AsynchronousCloseException 異常;如果在等待獲取鎖定的同時(shí)中斷了調(diào)用線程,則將狀態(tài)設(shè)置為中斷并拋出 FileLockInterruptionException 異常。如果調(diào)用此方法時(shí)已設(shè)置調(diào)用方的中斷狀態(tài),則立即拋出該異常;不更改線程的中斷狀態(tài);lock() 方法只鎖定大小為 Long.MAX_VALUES 的區(qū)域;文件鎖要么是獨(dú)占的,要么是共享的。共享鎖定可以阻止其他并發(fā)運(yùn)行的程序獲取重疊的獨(dú)占鎖定,但是允許該程序獲取重疊的共享鎖定。獨(dú)占鎖定則阻止其他程序獲取共享或獨(dú)占類型的重疊鎖定;某些操作系統(tǒng)不支持共享鎖定,這種情況下,自動(dòng)將對(duì)共享鎖定的請(qǐng)求轉(zhuǎn)換為對(duì)獨(dú)占鎖定的請(qǐng)求; |
FileLock | tryLock(long position, long size, boolean shared) | 試圖獲取對(duì)此通道的文件給定區(qū)域的鎖定。此方法不會(huì)阻塞,無(wú)論是否成功獲取請(qǐng)求區(qū)域上的鎖定,都會(huì)立即返回;如果由于另一個(gè)程序保持著一個(gè)重疊鎖定而無(wú)法獲取鎖定,此方法返回 null,其他原因則拋出異常; |
void | force(boolean metaData) | 強(qiáng)制將所有對(duì)此通道的文件更新寫(xiě)入包含該文件的存儲(chǔ)設(shè)備中。如果此通道的文件駐留在本地存儲(chǔ)設(shè)備上,此方法返回時(shí)可以保證:在此通道創(chuàng)建后或在最后一次調(diào)用此方法后,對(duì)該文件進(jìn)行的所有更改都寫(xiě)入存儲(chǔ)設(shè)備中。這對(duì)確保在系統(tǒng)崩潰時(shí)不會(huì)丟失重要信息特別有用。如果該文件不再本地設(shè)備商,則無(wú)法提供這樣的保證。metaData 參數(shù)可用于限制此方法是否必須更新文件元數(shù)據(jù)信息,fase 表示對(duì)文件內(nèi)容的更新寫(xiě)入存儲(chǔ)設(shè)備; true 表示必須寫(xiě)入對(duì)文件內(nèi)容和元數(shù)據(jù)的更新,這通常需要一個(gè)以上的 I/O 操作。此參數(shù)是否有效取決于底層操作系統(tǒng);調(diào)用此方法可能導(dǎo)致發(fā)送 I/O 操作,即使該通道僅允許進(jìn)行讀取操作時(shí)也是如此,例如,某些操作系統(tǒng)將最后一次訪問(wèn)的時(shí)間作為元數(shù)據(jù)的一部分進(jìn)行維護(hù),每當(dāng)讀取問(wèn)件時(shí)就更新此時(shí)間,但實(shí)際是否會(huì)執(zhí)行 I/O 操作還是與操作系統(tǒng)相關(guān);該方法只能保證強(qiáng)制進(jìn)行通過(guò)此類中已定義的方法對(duì)此通道的文件所進(jìn)行的更改,而不一定強(qiáng)制那些通過(guò)修改已映射字節(jié)緩沖區(qū)的內(nèi)容所進(jìn)行的更改。 |
FileLock 類具有平臺(tái)依賴性,此文件鎖定 API 直接映射到底層操作系統(tǒng)的本機(jī)鎖定機(jī)制。因此,無(wú)論程序使用何種語(yǔ)言編寫(xiě)的,某個(gè)文件上所保持的鎖定對(duì)于所有訪問(wèn)該文件的程序來(lái)說(shuō)都應(yīng)該是可見(jiàn)的。
關(guān)于 force(boolean metaData) 強(qiáng)制更新
其實(shí)在調(diào)用 FileChannel 類的 Write() 方法時(shí),操作系統(tǒng)為了運(yùn)行的效率,顯示把那些將要保存到硬盤(pán)上的數(shù)據(jù)暫時(shí)放入操作系統(tǒng)的緩存中,以減少硬盤(pán)的讀寫(xiě)次數(shù),然后在某一個(gè)時(shí)間點(diǎn)再將內(nèi)核緩存中的數(shù)據(jù)批量地同步到硬盤(pán)中,但同步的時(shí)間卻是由操作系統(tǒng)決定的。通過(guò) force(boolean metaData) 強(qiáng)制進(jìn)行同步,這樣做的目的是防止在系統(tǒng)崩潰或斷電時(shí)緩存中的數(shù)據(jù)丟失而造成損失。但是,force(boolean metaData) 方法并不能完全保證數(shù)據(jù)不丟失,如果正在執(zhí)行 force(boolean metaData) 方法時(shí)出現(xiàn)斷電的情況,那么硬盤(pán)上的數(shù)據(jù)有可能就不是完整的,而且由于斷電的原因?qū)е聝?nèi)核緩存中的數(shù)據(jù)也丟失了,最終造成的結(jié)果就是 force(boolean metaData) 方法執(zhí)行了,數(shù)據(jù)也有可能丟失。所以,force(boolean metaData) 方法的最終目的是盡最大的努力減少數(shù)據(jù)的丟失。
內(nèi)存映射
通過(guò) MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) 方法可以將此通道的文件區(qū)域直接映射到內(nèi)存中。
映射模式有以下 3 種:
對(duì)于只讀映射關(guān)系,此通道必須可以進(jìn)行讀取操作;對(duì)于讀取/寫(xiě)入或?qū)S糜成潢P(guān)系,此通道必須可以進(jìn)行讀取和寫(xiě)入操作。
映射關(guān)系一經(jīng)創(chuàng)建,就不再依賴于創(chuàng)建它時(shí)所用的文件通道。特別是關(guān)閉該通道對(duì)映射關(guān)系的有效性沒(méi)有任何影響。
對(duì)于大多數(shù)操作系統(tǒng)而言,與通過(guò)普通的 read() 和 write() 方法讀取或?qū)懭霐?shù)千字節(jié)的數(shù)據(jù)相比,將文件映射到內(nèi)存中開(kāi)銷更大。從性能的觀點(diǎn)來(lái)看,通常將相對(duì)較大的文件映射到內(nèi)存中才是值得的。
MappedByteBuffer 類的 force() 方法的作用是將此緩沖區(qū)所做的更改強(qiáng)制寫(xiě)入包含映射文件的存儲(chǔ)設(shè)備中。如果此緩沖區(qū)不是以讀/寫(xiě)模式映射的,則調(diào)用此方法無(wú)效。
零拷貝
零拷貝(Zero-copy)技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí),CPU 不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域。這種技術(shù)通常用于通過(guò)網(wǎng)絡(luò)傳輸文件時(shí)節(jié)省CPU周期和內(nèi)存帶寬?!俣劝倏?/p>
如果要讀取一個(gè)文件并通過(guò)網(wǎng)絡(luò)發(fā)送它,傳統(tǒng)方式下每個(gè)讀/寫(xiě)周期都需要復(fù)制兩次數(shù)據(jù)和切換兩次上下文(用戶空間和內(nèi)核空間之間的切換),而數(shù)據(jù)的復(fù)制都需要依靠CPU。通過(guò)零復(fù)制技術(shù)完成相同的操作,上下文切換減少到兩次,并且不需要CPU復(fù)制數(shù)據(jù)。
傳統(tǒng)I/O操作
BIO
- 讀虛擬機(jī)會(huì)從用戶空間向內(nèi)核空間發(fā)起一個(gè)讀命令的系統(tǒng)調(diào)用,由用戶空間模式切換到內(nèi)核空間模式(一次上下文切換)。內(nèi)核空間通過(guò) DAM 將數(shù)據(jù)讀取到內(nèi)核空間的一個(gè)緩沖區(qū)(第一次拷貝)完成真正向磁盤(pán)讀取數(shù)據(jù)的請(qǐng)求,。將內(nèi)核空間緩沖區(qū)中的數(shù)據(jù)通過(guò) CPU 再次拷貝到用戶空間的緩沖區(qū)中(第二次拷貝)。讀取操作完成,程序?qū)?shù)據(jù)業(yè)務(wù)處理。
- 寫(xiě)虛擬機(jī)從用戶空間向內(nèi)核空間發(fā)起一個(gè)寫(xiě)命令的系統(tǒng)調(diào)用,將用戶空間緩沖區(qū)中的數(shù)據(jù)通過(guò) CPU 拷貝到內(nèi)核空間緩沖區(qū)。并由用戶空間模式切換到內(nèi)核空間模式(一次上下文切換和一次數(shù)據(jù)拷貝)。內(nèi)核空間通過(guò)DAM完成數(shù)據(jù)寫(xiě)入網(wǎng)絡(luò)的功能(第二次拷貝,將數(shù)據(jù)拷貝到協(xié)議棧)。并返回內(nèi)核空間。內(nèi)核空間將結(jié)果反饋給用戶空間(第二次上下文切換)。結(jié)束。
代碼示例
File file = new File(“test.txt”);RandomAccessFile = raf = new RandomAccessFile(file,”rw”);byte[] arr = new byte[(int)file.length()];raf.read(arr);Socket socket = new ServerSocket(8888).accept();socket.getOutputStream().write(arr);
這段代碼就是讀取一個(gè)本地文件內(nèi)容,然后再將其內(nèi)容通過(guò) Socket 連接寫(xiě)出去。看起來(lái)就幾行代碼,但是涉及到多次內(nèi)存拷貝。其流程如下:
Java NIO 操作
Java NIO 中的 transferTo 可以實(shí)現(xiàn)零拷貝。零拷貝,并不是不拷貝,而是整個(gè)過(guò)程不需要進(jìn)行 CPU 拷貝。
NIO
首先還是通過(guò) DAM 拷貝到內(nèi)核空間的 buffer 中,然后再通過(guò) CPU 拷貝到 socket buffer ,最后由 DAM 拷貝到協(xié)議棧。但這次的 CPU 拷貝內(nèi)容很少,只拷貝內(nèi)核 buffer 的長(zhǎng)度、偏移量等信息,消耗很低,可以忽略。因此稱為零拷貝。減少了用戶空間與內(nèi)核空間的相互切換和數(shù)據(jù)拷貝次數(shù)。
代碼示例
傳統(tǒng) I/O 拷貝大文件
/** * 服務(wù)端 */public class OldIoServer { @SuppressWarnings(“resource”) public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(6666); while (true) { Socket socket = serverSocket.accept(); DataInputStream dataInputStream = new DataInputStream(socket.getInputStream()); byte[] byteArray = new byte[4096]; while (true) { int readCount = dataInputStream.read(byteArray, 0, byteArray.length); if (-1 == readCount) { break; } } } }}/** * 客戶端 */public class OldIoClient { @SuppressWarnings(“resource”) public static void main(String[] args) throws Exception { Socket socket = new Socket(“127.0.0.1”, 6666); // 需要拷貝的文件 String fileName = “E:downloadsoftwindowsjdk-8u171-windows-x64.exe”; InputStream inputStream = new FileInputStream(fileName); DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream()); byte[] buffer = new byte[4096]; long readCount; long total = 0; long start = System.currentTimeMillis(); while ((readCount = inputStream.read(buffer)) >= 0) { total += readCount; dataOutputStream.write(buffer); } long end = System.currentTimeMillis(); System.out.println(“傳輸總字節(jié)數(shù):” + total + “,耗時(shí):” + (end – start) + “毫秒”); dataOutputStream.close(); inputStream.close(); socket.close(); }}
這里拷貝了一個(gè) JDK ,最后運(yùn)行結(jié)果如下:
傳輸總字節(jié)數(shù):217342912,耗時(shí):4803毫秒
使用 Java NIO 的 transferTo 拷貝
/** * 服務(wù)端 */public class NioServer { public static void main(String[] args) throws IOException { InetSocketAddress address = new InetSocketAddress(6666); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); ServerSocket serverSocket = serverSocketChannel.socket(); serverSocket.bind(address); ByteBuffer buffer = ByteBuffer.allocate(4096); while (true) { SocketChannel socketChannel = serverSocketChannel.accept(); int readCount = 0; while (-1 != readCount) { readCount = socketChannel.read(buffer); buffer.rewind(); // 倒帶,將position設(shè)置為0,mark設(shè)置為-1 } } }}/** * 客戶端 */public class NioClient { @SuppressWarnings(“resource”) public static void main(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress(“127.0.0.1”, 6666)); String fileName = “E:downloadsoftwindowsjdk-8u171-windows-x64.exe”; FileChannel channel = new FileInputStream(fileName).getChannel(); long start = System.currentTimeMillis(); // 在linux下,transferTo方法可以一次性發(fā)送數(shù)據(jù) // 在windows中,transferTo方法傳輸?shù)奈募^(guò)8M得分段 long totalSize = channel.size(); long transferTotal = 0; long position = 0; long count = 8 * 1024 * 1024; if (totalSize > count) { BigDecimal totalCount = new BigDecimal(totalSize).pide(new BigDecimal(count)).setScale(0, RoundingMode.UP); for (int i=1; i<=totalCount.intValue(); i++) { if (i == totalCount.intValue()) { transferTotal += channel.transferTo(position, totalSize, socketChannel); } else { transferTotal += channel.transferTo(position, count + position, socketChannel); position = position + count; } } } else { transferTotal += channel.transferTo(position, totalSize, socketChannel); } long end = System.currentTimeMillis(); System.out.println("發(fā)送的總字節(jié):" + transferTotal + ",耗時(shí):" + (end – start) + "毫秒"); channel.close(); socketChannel.close(); }}
運(yùn)行結(jié)果如下:
發(fā)送的總字節(jié):217342912,耗時(shí):415毫秒
從結(jié)果可以看到,BIO與NIO耗時(shí)相差一個(gè)數(shù)量級(jí),NIO只要0.4s,而B(niǎo)IO要4s。所以在網(wǎng)絡(luò)傳輸中,使用 NIO 的零拷貝,可以大大提高性能。
Selector
Selector 與 I/O 多路復(fù)用
Selector 稱為選擇器,可以將通道注冊(cè)進(jìn)選擇器中,選擇器與通道之間屬于一對(duì)多的關(guān)系,也就是使用 1 個(gè)線程來(lái)操作多個(gè)通道。主要作用就是使用 1 個(gè)線程來(lái)對(duì)多個(gè)通道中已就緒的通道進(jìn)行選擇,然后就可以對(duì)選擇的通道進(jìn)行數(shù)據(jù)處理。這種機(jī)制在 NIO 技術(shù)中稱為 I/O 多路復(fù)用。它的優(yōu)勢(shì)是可以節(jié)省 CPU 資源,因?yàn)橹挥?1 個(gè)線程,CPU 不需要在不同的線程間進(jìn)行上下文切換(線程的上下文切換是一個(gè)非常耗時(shí)的動(dòng)作),并且因?yàn)榫€程對(duì)象的數(shù)量大幅減少,降低了內(nèi)存占用率,這對(duì)設(shè)計(jì)高性能服務(wù)器具有很重要的意義。
多路復(fù)用的核心目的是使用最少的線程去操作更多的通道,在其內(nèi)部其實(shí)并不永遠(yuǎn)只是一個(gè)線程,線程數(shù)量會(huì)隨著通道的多少而動(dòng)態(tài)地增減以進(jìn)行適配。在 JDK 源碼中,創(chuàng)建線程的個(gè)數(shù)是根據(jù)通道的數(shù)量來(lái)決定的,每注冊(cè) 1023 個(gè)通道就創(chuàng)建 1 個(gè)新的線程。
注意:
在使用 I/O 多路復(fù)用時(shí),這個(gè)線程不是以 for 循環(huán)的方式來(lái)判斷每個(gè)通道是否有數(shù)據(jù)進(jìn)行處理,而是以操作系統(tǒng)底層作為”通知器”,來(lái) “通知JVM 中的線程”哪個(gè)通道中的數(shù)據(jù)需要進(jìn)行處理。當(dāng)不使用 for 循環(huán)的方式來(lái)進(jìn)行判斷,而是使用通知的方式時(shí),這就大大提高了程序運(yùn)行的效率,不會(huì)出現(xiàn)無(wú)限期的 for 循環(huán)迭代空運(yùn)行了。
Selector 類是抽象類,是 SelectableChannel 對(duì)象的多路復(fù)用器。也就是說(shuō)只有 selectableChannel 通道才能被 Selector 所復(fù)用。
通過(guò) Selector.open() 方法來(lái)獲得一個(gè) Selector 對(duì)象。open() 方法內(nèi)部又是通過(guò) SelectorProvider 對(duì)象來(lái)獲取/打開(kāi)一個(gè)選擇器并返回的。SelectorProvider 類的作用是用于選擇器和可選擇通道的服務(wù)提供者類,給定的對(duì) Java 虛擬機(jī)的調(diào)用維護(hù)了單個(gè)系統(tǒng)級(jí)的默認(rèn)提供者實(shí)例,它由 provider() 方法返回。
在通過(guò)調(diào)用選擇器的close() 方法關(guān)閉選擇器之前,選擇器一直保持打開(kāi)狀態(tài)。
通過(guò) SelectionKey 對(duì)象來(lái)表示 SelectableChannel 到選擇器的注冊(cè)。選擇器維護(hù)了 3 種 SelectionKey-Set (選擇鍵集),在新建的選擇器中,這 3 個(gè)集合都是空集合:
無(wú)論是通過(guò)關(guān)閉某個(gè)鍵的通道還是調(diào)用該鍵的 cancel() 方法來(lái)取消鍵,該鍵都被添加到選擇器的已取消鍵集中。取消某個(gè)鍵會(huì)導(dǎo)致在下一次 select() 方法選擇操作期間注銷該鍵的通道,而在注銷時(shí)將從所有選擇器的鍵集中移除該鍵。
通過(guò) select() 方法選擇操作將鍵添加到已選擇鍵集中??赏ㄟ^(guò)調(diào)用已選擇鍵集的 remove() 方法,或者通過(guò)調(diào)用從該鍵集獲得的 iterator 的 remove() 方法直接移除某個(gè)鍵。通過(guò)任何其他方式都無(wú)法直接將鍵從已選擇鍵集中移除。無(wú)法將鍵直接添加到已選擇鍵集中。
select() 方法返回值的含義是已更新其準(zhǔn)備就緒操作集的鍵的數(shù)目,該數(shù)目可能為零或非零,非零的情況就是新的準(zhǔn)備就緒鍵的個(gè)數(shù)。零說(shuō)明當(dāng)前沒(méi)有通道準(zhǔn)備就緒,更新準(zhǔn)備就緒操作集的鍵的個(gè)數(shù)為零,如果沒(méi)有調(diào)用 remove() 方法,此時(shí)準(zhǔn)備就緒操作集的鍵與上次保持一致。
注意:
在每次處理完一個(gè)已選擇鍵對(duì)應(yīng)的事件后,需要手動(dòng)調(diào)用 remove() 方法將其從已選擇鍵集中移除,不然會(huì)造成重復(fù)消費(fèi)的情況,導(dǎo)致程序異常。
在每次 select() 操作期間,都可以將鍵添加到選擇器的已選擇鍵集或從中將其移除,并且可以從其鍵集合已取消鍵集中移除。涉及以下 3 個(gè)步驟:
在執(zhí)行選擇操作的過(guò)程中,更改選擇器鍵的相關(guān)集合對(duì)該操作沒(méi)有影響,在進(jìn)行下一次選擇操作時(shí)才會(huì)看到此更改。一般情況下,選擇器的鍵和已選擇鍵集由多個(gè)并發(fā)線程使用是不安全的。
返回值類型 | 方法名 | 作用 |
int | select() | 同步操作,選擇一組鍵,其相應(yīng)的通道已為 I/O 操作準(zhǔn)備就緒 |
int | select(long timeout) | 在指定時(shí)間內(nèi)同步操作,選擇一組鍵,其相應(yīng)的通道已為 I/O 操作準(zhǔn)備就緒。如果 timeout 參數(shù)為 0 則無(wú)限期地阻塞 |
int | selectNow() | 非阻塞操作,選擇一組鍵,其相應(yīng)的通道已為 I/O 操作準(zhǔn)備就緒 |
SelectionKey
SelectionKey 表示 SelectableChannel 在選擇器中的注冊(cè)的標(biāo)記。
SelectionKey 支持將單個(gè)任意對(duì)象附加到某個(gè)鍵的操作??赏ㄟ^(guò) attach() 方法附加對(duì)象,然后通過(guò) attachment() 方法獲取該對(duì)象。
返回值類型 | 方法名 | 作用 |
SelectableChannel | cancel() | 將 SelectionKey 放入取消鍵集中,并且在下一次執(zhí)行 select() 方法是刪除這個(gè) SelectionKey 所有的鍵集,并且注銷其對(duì)應(yīng)的通道。 |
boolean | isAcceptable() | 測(cè)試此鍵的通道是否已準(zhǔn)備好接受新的套接字連接。 |
boolean | isConnectable() | 測(cè)試此鍵的通道是否已完成其套接字的連接操作。 |
boolean | isReadable() | 測(cè)試此鍵的通道是否已準(zhǔn)備好進(jìn)行讀取。 |
boolean | isWritable() | 測(cè)試此鍵的通道是否已準(zhǔn)備好進(jìn)行寫(xiě)入。 |
Selector | selector() | 返回 SelectionKey 關(guān)聯(lián)的選擇器,即使已取消該鍵,此方法仍然有效。 |
Object | attach(Object obj) | 將給定的對(duì)象附加到此鍵。一次只能附加一個(gè)對(duì)象,調(diào)用此方法會(huì)導(dǎo)致丟棄所有以前的附加對(duì)象。返回值代表先前已附加的對(duì)象(如果有),否則返回 null。 |
Object | attachment() | 獲取已附加的對(duì)象(如果有),否則返回 null。 |
SelectableChannel
SelectableChannel 類和 FileChannel 類是平級(jí)關(guān)系,都是繼承自父類 AbstractInterruptibleChannel。抽象類 SelectableChannel 有很多子類,這里只展示了在 Socket 編程中常用的 ServerSocketChannel 和 SocketChannel, 如下圖:
SelectableChannel 類可以通過(guò)選擇器實(shí)現(xiàn)多路復(fù)用。在與選擇器結(jié)合使用的時(shí)候,首先需要調(diào)用 SelectableChannel 對(duì)象的 register() 方法在選擇器對(duì)象里注冊(cè)當(dāng)前 SelectableChannel。
一個(gè)通道最多只能在任意特定選擇器上注冊(cè)一次??梢酝ㄟ^(guò) isRegistered() 方法來(lái)確定是否已經(jīng)向一個(gè)或多個(gè)選擇器注冊(cè)了某個(gè)通道。
新創(chuàng)建的 SelectableChannel 總是處于阻塞模式,在結(jié)合使用基于選擇器的多路復(fù)用時(shí),向選擇器注冊(cè)某個(gè)通道前,必須先將該通道置于非阻塞模式。
ServerSocketChannel 類是抽象的,可通過(guò)其 open() 方法創(chuàng)建實(shí)例。其實(shí) ServerSocketChannel 和 SocketChannel 只是對(duì) ServerSocket 和 Socket 的封裝,目的就是要結(jié)合選擇器達(dá)到多路復(fù)用的效果。單純的使用 SocketChannel 只是對(duì) ServerSocket 是實(shí)現(xiàn)不了 I/O 多路復(fù)用的。
SelectionKey register(Selector sel, int ops) 方法的作用是向給定的選擇器注冊(cè)此通道,返回一個(gè)選擇鍵(SelectionKey)。參數(shù) sel 代表要向其注冊(cè)此通道的選擇器,ops 參數(shù)就是通道感興趣的時(shí)間,也就是通道能執(zhí)行操作的集合,包括:SelectionKey.OP_ACCEPT – 用于套接字接受操作的操作集位、SelectionKey.OP_CONNECT – 用于套接字連接操作的操作集位、SelectionKey.OP_READ – 用于讀取操作的操作集位和 SelectionKey.OP_WRITE – 用于寫(xiě)入操作的操作集位。