文章目錄
- 一 通道和流的區別
- 二 已實現的主要通道
- 三 FileChannel
- 四 API介紹
- 4.1 獲取FileChannel對象
- 4.2 數據寫入
- 4.2.1 write(ByteBuffer src)
- 4.2.2 write(ByteBuffer\[] srcs)
- 4.2.3 write(ByteBuffer\[] srcs, int offset, int length)
- 4.2.4 write(ByteBuffer src, long position)
- 4.3 數據讀取
- 4.3.1 read(ByteBuffer dst)
- 4.3.2 read(ByteBuffer\[] dsts)
- 4.3.3 read(ByteBuffer\[] dsts, int offset, int length)
- 4.3.4 read(ByteBuffer dst, long position)
- 4.4 設置通道位置
- 4.5 獲取文件大小
- 4.6 切分文件數據
- 4.7 通道傳輸
- 4.8 鎖文件區域
- 4.9 內存映射
- 五 結語
一 通道和流的區別
前一篇介紹了NIO的通道接口,十餘個接口看起來極爲複雜,但實際上NIO提供的實現類,且應用場景覆蓋較廣泛的並不多。
從目前瞭解到的信息來看,通道僅僅是用於將緩衝區數據寫入,或者從其中讀取數據到緩衝區,咋一看和流類似,但從接口設定上來看還是有一些區別的:
- Java提供的流多爲單向操作,或讀或寫,而通道的實現多爲雙向,可讀可寫;
- 通道實現類提供的API基本上都是將緩衝區數據寫入通道,或從通道中讀取數據到緩衝區,二者聯繫緊密;
- NIO提供的通道可支持異步讀寫,這是最重要的特性。
二 已實現的主要通道
NIO提供了非常多的通道實現,但是常用的無非以下四個:
- FileChannel
- DatagramChannel
- SocketChannel
- SocketServerChannel
其中FileChannel支持從文件中讀寫數據;DatagramChannel則支持UDP協議的網絡通信數據讀寫;SocketChannel支持TCP協議的網絡通信數據讀寫,SocketServerChannel和SocketChannel有關聯,是服務端監聽客戶端TCP請求的用的,一旦建立請求會創建一個SocketChannel來支持此連接通道中的數據讀寫。
這裏先鋪墊一下,NIO的主要應用場景是網絡通信,文件讀寫方面未必比傳統流式方便,但是爲了介紹通道的使用的方法,本文以FileChannel爲例,介紹與其相關的主要API。SocketChannel則在後續的網絡通信方面介紹中細說。
三 FileChannel
先看FileChannel的定義:
/**
*
* @see java.io.FileInputStream#getChannel()
* @see java.io.FileOutputStream#getChannel()
* @see java.io.RandomAccessFile#getChannel()
*
* @author Mark Reinhold
* @author Mike McCloskey
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class FileChannel extends AbstractInterruptibleChannel implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
...
}
首先FileChannel是一個抽象類,繼承自AbstractInterruptibleChannel,從註釋中可以很明顯的看出來,io包的FileInputStream、FileOutputStream以及功能更爲全面的RandomAccessFile類都提供了返回FileChannel實例的方法。
其次FileChannel要求實現類必須實現SeekableByteChannel、GatheringByteChannel以及ScatteringByteChannel接口,算上基類AbstractInterruptibleChannel的Channel和InterruptibleChannel接口,可以斷定FileChannel必然支持中斷機制,支持數據讀寫,以及對position的維護操作,從這幾個方面着手,再看FileChannel提供的抽象方法定義,基本上就把FileChannel喫透了。
上面的學習方法適用所有的Java技術體系。
四 API介紹
FileChannel在其內部維護一個與文件相關聯的position參數,基於此參數可支持對文件數據的讀寫,且FileChannel是阻塞的。
除常見的文件讀寫、關閉等操作,FileChannel還支持將文件映射到內存中,常見於大文件的讀寫操作,內存映射後的讀寫更爲高效。如果在寫文件時爲了防止意外情況導致的文件數據丟失,FileChannel還支持實時更新——強制更新存儲設備,甚至可以對文件中的部分數據進行加鎖操作,以防止其他應用程序對文件內容進行訪問。
因通道的阻塞特性,FileChannel是線程安全的,比如說任意執行線程可執行通道的關閉操作,那麼其他線程也發起關閉操作,則會被阻塞。
需要注意的是,多個線程同時操作文件數據時,可操作的數據未必一致,取決於操作系統對數據寫入文件的執行策略。
由於FileChannel提供的文件操作方法較多,後文會根據FileChannel接口的實現來介紹不同的文件操作API,以此來加深對接口的理解,冗餘的說明則儘量掠過以節省篇幅,所以讀者需要格外注意接口的特性。
4.1 獲取FileChannel對象
FileChannel類沒有提供任何打開文件的方法(我本機使用的JDK1.8,未來的版本中可能會提供),正如前文中介紹的,如果想獲得FileChannel對象,可通過FileInputStream、FileOutputStream、RandomAccessFile對象的getChannel()方法實現。
這裏需要注意的是,通過FileChannel對象對文件進行數據讀寫後,會影響提供getChannel方法的對象——稱之爲源文件操作對象,此時通過源文件操作對象訪問文件數據和FileChannel對象操作後的數據一致,比如說通過FileChannel來改變文件大小,那麼通過源文件操作對象訪問到的文件大小是改變後的。
另一個需要注意的點是不同的源文件操作對象提供的FileChannel實例,對文件的操作權限是不一樣的:
- FileInputStream提供的FileChannel可讀文件
- FileOutputStream提供的FileChannel可寫文件
- RandomAccessFile提供的FileChannel,創建RandomAccessFile對象的模式不同則操作權限不同,“r”模式可讀,“w”模式可寫,“rw”模式可讀可寫
public class FileChannelTest {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream(new File("TestFile"));
FileChannel fileChannel = fis.getChannel();
System.out.println("文件是否已經打開:" + fileChannel.isOpen() + " 文件大小:" + fileChannel.size());
fileChannel.close();
RandomAccessFile raf = new RandomAccessFile(new File("TestFile"), "rw");
fileChannel = raf.getChannel();
System.out.println("文件是否已經打開:" + fileChannel.isOpen() + " 文件大小:" + fileChannel.size());
fileChannel.close();
FileOutputStream fos = new FileOutputStream(new File("TestFile"));
fileChannel = fos.getChannel();
System.out.println("文件是否已經打開:" + fileChannel.isOpen() + " 文件大小:" + fileChannel.size());
fileChannel.close();
}
}
輸出結果:
文件是否已經打開:true 文件大小:45
文件是否已經打開:true 文件大小:45
文件是否已經打開:true 文件大小:0
4.2 數據寫入
通過IDE工具查看和write相關的API,可以看到下述四個API:
4.2.1 write(ByteBuffer src)
write(ByteBuffer src)方法是實現的是WritableByteChannel接口,實現的是將參數src的剩餘可操作字節(字節長度爲ByteBuffer的remaining方法返回值)寫入FileChannel,再細緻點說:
- 這是個阻塞方法(前文說了FileChannel的方法都是阻塞的,即當前線程寫入動作未結束時,其他線程的寫入動作均被阻塞,其他的IO操作是否允許併發的處理,取決於FileChannel的實際類型後文不再贅述);
- 將src緩衝區的數據寫入通道
- 從通道的當前位置position開始寫入(此position不是緩衝區的position)
- 寫入長度爲src的剩餘可操作字節數(即remaining方法返回值)
此方法的應用請參考下面的示例:
public class FileChannelTest {
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream(new File("TestFile"));
FileChannel fileChannel = fos.getChannel();
// 設置通道的position爲3
fileChannel.position(3);
ByteBuffer byteBuffer = ByteBuffer.wrap("abcdefg".getBytes(Charset.defaultCharset()));
System.out.println("初始的緩衝區大小爲:" + byteBuffer.limit());
// 設置緩衝區的position爲2
byteBuffer.position(2);
System.out.println("緩衝區remaining長度爲:" + byteBuffer.remaining() + " 通道的position爲:" + fileChannel.position() + " 通道的長度爲:" + fileChannel.size());
fileChannel.write(byteBuffer);
System.out.println("寫入數據後通道的position爲:" + fileChannel.position() + " 通道的長度爲:" + fileChannel.size());
}
}
輸出結果:
初始的緩衝區大小爲:7
緩衝區remaining長度爲:5 通道的position爲:3 通道的長度爲:0
寫入數據後通道的position爲:8 通道的長度爲:8
從示例中很明顯的看出來寫入通道的數據長度爲緩衝去區的remaining長度5,加上預先移動的3字節,寫入後通道的數據長度爲8。
接下來在驗證下阻塞特性,這裏只需要啓動多個線程,同時向通道中寫入數據,然後我們看看數據是否會出現交叉亂序:
public class FileChannelTest {
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream(new File("TestFile"));
FileChannel fileChannel = fos.getChannel();
for (int i = 0; i < 5; i++) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
ByteBuffer byteBuffer = ByteBuffer.wrap("abcdefg".getBytes(Charset.defaultCharset()));
fileChannel.write(byteBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
ByteBuffer byteBuffer = ByteBuffer.wrap("1234567".getBytes(Charset.defaultCharset()));
fileChannel.write(byteBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
}
}
文件內容:
12345671234567abcdefg1234567abcdefg1234567abcdefg1234567abcdefgabcdefg
一目瞭然,沒有出現數字和字母交叉的排序,出現數字或字母重複追加的情況是因爲線程的調度順序是非線性的,但數字和字母沒有交叉則證明了API的阻塞特性是確實存在的,在數字或字母沒有完全寫入前,其他線程無法寫入任何數據。
4.2.2 write(ByteBuffer[] srcs)
此API實現的是GatheringByteChannel接口,GatheringByteChannel派生自WritableByteChannel接口,因此包含章節4.2.1中介紹的特性,此外從參數中也可以看出它支持將多個緩衝區的remaining字節寫入通道。
需要注意的是緩衝區數組的元素順序決定了寫入數據的順序,因阻塞特性多個緩衝區數組同時寫入時,不會出現數據交叉錯亂的場景。
4.2.3 write(ByteBuffer[] srcs, int offset, int length)
此API實現的接口依然是GatheringByteChannel,所以和write(ByteBuffer[] srcs)幾乎一致,唯一不同的是,此API多了兩個參數:
- offset指參數srcs的偏移量,即從第幾個字節緩衝區纔開始需要向通道寫入數據
- length指從offset位置開始有length長度個字節緩衝區需要向通道寫入數據
如果還覺得比較難理解,讀者可認爲write(ByteBuffer[] srcs)等價於write(ByteBuffer[] srcs, 0, srcs.length),這樣是不是就清楚了。
4.2.4 write(ByteBuffer src, long position)
這個API和write(ByteBuffer src)單純的區別在於不是從通道的position開始寫入數據,而是可以指定通道位置了,其他的特性完全一致。
唯一需要說明的是如果指定的position大於了通道關聯的文件大小,不會報錯,而是對文件進行擴容,在參數position之前的被擴容的部分,寫入的是未指定的字節數據。另外調用此方法,不會影響通道的position值,這些特性讀者可自行編寫測試函數驗證。
補充一個不需要解釋的細節,參數position不能爲負。
4.3 數據讀取
同樣的通過IDE查看read相關的API:
這四個API對應前文中的4個寫入API,下面僅介紹下對應的實現接口。
4.3.1 read(ByteBuffer dst)
這個API實現的是ReadableByteChannel接口,以阻塞方式從通道中將數據讀到緩衝區dst中,需要注意以下幾點:
- 從通道的position開始讀;
- 讀到的數據會寫入dst中,從dst的position位置開始寫入;
- 寫入的數據長度爲dsc的remaining方法返回值
另外需要注意的是此API的返回值,返回值類型爲int,可能出現的結果有以下三種場景:
- 正整數,表示讀取到的字節數
- 0,沒有讀到任何數據,不排除dst.remaining返回的值爲0
- -1,讀到了通道末尾
4.3.2 read(ByteBuffer[] dsts)
這個API實現的是ScatteringByteChannel接口,ScatteringByteChanne派生自ReadableByteChannel,因此具備章節4.3.1中介紹的特性,此外它還支持將通道當前位置開始的數據寫入多個字節緩衝區中,每個緩衝區寫入數據的多少取決於緩衝區的remaining。
4.3.3 read(ByteBuffer[] dsts, int offset, int length)
對應章節4.2.3的寫操作,實現的接口是ScatteringByteChanne,這意味着和read(ByteBuffer[] dsts)方法的行爲一致,差異在於兩個參數,不再贅述。
4.3.4 read(ByteBuffer dst, long position)
對應章節4.2.4的寫方法,用於從指定的通道的位置,將通道數據讀入到緩衝區的當前位置。除了可指定文件位置外,其他特性同read(ByteBuffer dst)方法。
需要注意的是,首先參數不能爲負,不解釋。另外如果參數position大於文件的大小,那麼則不會讀取任何數據。同write(ByteBuffer dst, long position),調用此方法不會改變通道的position值。
4.4 設置通道位置
前文中的讀寫API和通道position是緊密相關的,那麼在讀寫數據時時刻掌握通道position值就顯得尤爲重要,必要時還需要對通道位置進行設置。
FileChannel提供了position(long newPosition)方法來設置通道位置,參數newPosition有些特殊,它的值可以大於通道關聯的文件大小,但方法本身卻不會更改文件的大小,而是在後續的讀寫過程中發生作用:
- 後續讀數據時直接返回已讀到文件末尾
- 後續寫數據時會對文件進行擴容,擴容大小可滿足寫入數據,在原文件數據和新寫入數據之間的部分是未指定的字節數據。
這部分需要結合章節4.2、4.3中的數據讀寫來理解。
4.5 獲取文件大小
沒啥好介紹的,size()方法。
4.6 切分文件數據
這個API和前幾篇介紹緩衝區的切分是一樣的道理,truncate(long size)方法會將通道關聯的文件按參數大小進行切分,這裏參數值可能出現以下幾種情況:
- 如果參數值大於文件大小,沒有任何影響
- 參數值小於文件大小,切分文件,並且丟掉size後面的數據,注意!切分後可以認爲是一個新文件
另一個需要注意的點是,如果切分的時候通道position值已經大於了參數size值,那麼切分後position被重置爲size值,比如說原文件內容“1234567”:
public class FileChannelTest {
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream(new File("TestFile"));
FileChannel fileChannel = fos.getChannel();
fileChannel.write(ByteBuffer.wrap("1234567".getBytes(Charset.defaultCharset())));
fileChannel.position(6);
System.out.println("原文件大小:" + fileChannel.size() + " 通道position:" + fileChannel.position());
FileChannel newFileChannel = fileChannel.truncate(3);
System.out.println("切分文件大小:" + newFileChannel.size() + " 通道position:" + newFileChannel.position());
}
}
輸出結果:
原文件大小:7 通道position:6
切分文件大小:3 通道position:3
4.7 通道傳輸
前文中介紹的數據讀寫都是通道和緩衝區之間進行數據的傳輸,本小節則介紹通道間進行數據傳輸的API。
4.7.1 從當前通道傳輸給其他可寫通道
第一個API是transferTo(long position, long count, WritableByteChannel dest),我們可以認爲此API等同於章節4.2介紹的數據寫入API,只不過目標變成了其他可寫通道(第三個參數已經明確告知,目標通道必須是WritableByteChannel接口類型)。
此API用於從當前通道的position位置開始,長count個字節的數據寫入目標通道dest,因涉及兩個通道,那麼實際上數據傳輸能夠成功執行就出現了多種可能:
- 如果當前通道position後的數據長度不足count值,又或者目標通道可接收的數據長度不足count值,則傳輸的實際字節數小於count值;
- 如果參數position值大於當前通道關聯的文件大小,則不傳輸任何數據;
- 如果數據可寫入,那麼目標通道的position值會增加實際寫入數據的長度;
此外還有其他的可能,這裏不一一列舉,讀者可在實際應用中進行函數測試驗證,但是需要注意的是,調用此方法不會改變當前通道的position。
4.7.2 從其他可讀通道傳輸給當前通道
transferFrom(ReadableByteChannel src, long position, long count),此API和章節4.7.1剛好相反,各參數含義互換位置即可,這裏不再贅述。
4.8 鎖文件區域
這裏和前文一開始的介紹遙相呼應,FileChannel支持將關聯的文件的部分區域進行鎖定,以防止其他其他線程對此區域數據進行操作。
鎖文件區域方法爲lock(long position, long size, boolean shared),需要注意的是參數position和size可以和文件的數據不一致 ,即此方法僅針對通道從position位置開始,size長度的數據區域鎖定,且參數shared可以指定鎖類型爲獨佔鎖或是共享鎖,至於最終採用何種類型的鎖,還要看實際的操作系統支不支持(有些操作系統不支持共享鎖,那麼此方法會自動將鎖類型轉爲獨佔鎖 )。
鎖定文件區域是非常複雜的,有諸多場景上的差異,甚至和操作系統有關,所以這裏不過多的介紹,如果有機會,後面會單獨開一個章節來介紹,有興趣的朋友可以自行搜索相關信息 。
那麼既然有部分區域鎖定,就必然支持全通道鎖定,無參數lock方法實現了此功能,我們完全可以認爲lock()等價於lock(0L, Long.MAV_VALUE, false),實際上源碼中也的確是這樣實現的。
因爲篇幅原因,不再介紹和鎖相關的內容(嘗試獲取通道鎖)。
4.9 內存映射
這是一個比較重量級的應用,方法map(FileChannel.MapMode mode, long position, long size)支持將通道關聯的文件,在參數區域內的數據映射到內存中,以此來實現更高效率的數據訪問。
此方法返回一個MappedByteBuffer對象,可認爲是一個文件數據緩衝區副本。
其中參數mode提供了三種枚舉定義:
- MapMode.READ_ONLY,只讀,如果對此區域內數據進行修改會拋出異常;
- MapMode.READ_WRITE,讀寫,操作MappedByteBuffer對象會直接同步到文件,但是需要注意的是其他關聯了此文件的通道、緩衝區等未必能及時看到;
- MapMode.PRIVATE,私有,一個完全獨立的部分,操作MappedByteBuffer對象不會同步到文件,其他關聯了此文件的通道、緩衝區也不可見。
和章節4.8類型,內存映射也涉及操作系統的支持與否,並且用法較多,這裏不再詳細介紹每一個點,後面如果有時間我會單獨寫一個章節來介紹相關內容。
五 結語
如果想關注更多硬技能的分享,可以參考積少成多系列傳送門,未來每一篇關於硬技能的分享都會在傳送門中更新鏈接。