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