Java文件的簡單讀寫、隨機讀寫、NIO讀寫與使用MappedByteBuffer讀寫

2021-03-02 Java藝術
文件與目錄的創建和刪除較為簡單,因此忽略這部分內容的介紹,我們重點學習文件的讀寫。本篇內容包括:

簡單文件讀寫

隨機訪問文件讀寫

NIO文件讀寫-FileChannel

使用MappedByteBuffer讀寫文件

簡單文件讀寫FileOutputStream由於流是單向的,簡單文件寫可使用FileOutputStream,而讀文件則使用FileInputStream。任何數據輸出到文件都是以字節為單位輸出,包括圖片、音頻、視頻。以圖片為例,如果沒有圖片格式解析器,那麼圖片文件其實存儲的就只是按某種格式存儲的字節數據罷了。FileOutputStream指文件字節輸出流,用於將字節數據輸出到文件,僅支持順序寫入、支持以追加方式寫入,但不支持在指定位置寫入。

public class FileOutputStreamStu{
public void testWrite(byte[] data) throws IOException {
try(FileOutputStream fos = new FileOutputStream("/tmp/test.file",true)) {
fos.write(data);
fos.flush();
}
}
}

注意,如果不指定追加方式打開流,new FileOutputStream時會導致文件內容被清空,而FileOutputStream的默認構建函數是以非追加模式打開流的。FileOutputStream的參數1為文件名,參數2為是否以追加模式打開流,如果為true,則字節將寫入文件的尾部而不是開頭。調用flush方法目的是在流關閉之前清空緩衝區數據,實際上使用FileOutputStream並不需要調用flush方法,此處的刷盤指的是將緩存在JVM內存中的數據調用系統函數write寫入。如BufferedOutputStream,在調用BufferedOutputStream方法時,如果緩存未滿,實際上是不會調用系統函數write的,如下代碼所示。

public class BufferedOutputStream extends FilterOutputStream {
public synchronized void write(byte b[], int off, int len) throws IOException {
if (len >= buf.length) {
flushBuffer();
out.write(b, off, len);
return;
}
if (len > buf.length - count) {
flushBuffer();
}
System.arraycopy(b, off, buf, count, len); // 只寫入緩存
count += len;
}
}

FileInputStreamFileInputStream指文件字節輸入流,用於將文件中的字節數據讀取到內存中,僅支持順序讀取,不可跳躍讀取。

public class FileInputStreamStu{
public void testRead() throws IOException {
try (FileInputStream fis = new FileInputStream("/tmp/test/test.log")) {
byte[] buf = new byte[1024];
int realReadLength = fis.read(buf);
}
}
}

其中buf數組中下標從0到realReadLength的字節數據就是實際讀取的數據,如果realReadLength返回-1,則說明已經讀取到文件尾並且未讀取到任何數據。當然,我們還可以一個字節一個字節的讀取,如下代碼所示。

public class FileInputStreamStu{
public void testRead() throws IOException {
try (FileInputStream fis = new FileInputStream("/tmp/test/test.log")) {
int byteData = fis.read(); // 返回值取值範圍:[-1,255]
if (byteData == -1) {
return; // 讀取到文件尾了
}
byte data = (byte) byteData;
// data為讀取到的字節數據
}
}
}

至於讀取到的字節數據如何使用就需要看你文件中存儲的是什麼數據了。如果整個文件存儲的是一張圖片,那麼需要將整個文件讀取完,再按格式解析成圖片,而如果整個文件是配置文件,則可以一行一行讀取,遇到\n換行符則為一行,代碼如下。

public class FileInputStreamStu{
@Test
public void testRead() throws IOException {
try (FileInputStream fis = new FileInputStream("/tmp/test/test.log")) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int byteData;
while ((byteData = fis.read()) != -1) {
if (byteData == '\n') {
buffer.flip();
String line = new String(buffer.array(), buffer.position(), buffer.limit());
System.out.println(line);
buffer.clear();
continue;
}
buffer.put((byte) byteData);
}
}
}
}

Java基於InputStream、OutputStream還提供了很多的API方便讀寫文件,如BufferedReader,但如果懶得去記這些API的話,只需要記住FileInputStream與FileOutputStream就夠了。隨機訪問文件讀寫RandomAccessFile相當於是FileInputStream與FileOutputStream的封裝結合,即可以讀也可以寫,並且RandomAccessFile支持移動到文件指定位置處開始讀或寫。

public class RandomAccessFileStu{
public void testRandomWrite(long index,long offset){
try (RandomAccessFile randomAccessFile = new RandomAccessFile("/tmp/test.idx", "rw")) {
randomAccessFile.seek(index * indexLength());
randomAccessFile.write(toByte(index));
randomAccessFile.write(toByte(offset));
}
}
}

RandomAccessFile的seek方法通過調用native方法實現,源碼如下。

JNIEXPORT void JNICALL
Java_java_io_RandomAccessFile_seek0(JNIEnv *env,
jobject this, jlong pos) {
FD fd;
fd = GET_FD(this, raf_fd);
if (fd == -1) {
JNU_ThrowIOException(env, "Stream Closed");
return;
}
if (pos < jlong_zero) {
JNU_ThrowIOException(env, "Negative seek offset");
}
// #define IO_Lseek lseek
else if (IO_Lseek(fd, pos, SEEK_SET) == -1) {
JNU_ThrowIOExceptionWithLastError(env, "Seek failed");
}
}

Java_java_io_RandomAccessFile_seek0函數的參數1表示RandomAccessFile對象,參數2表示偏移量。函數中調用的IO_Lseek方法實際是作業系統的lseek方法。RandomAccessFile提供的讀、寫、指定偏移量其實都是通過調用作業系統函數完成的,包括前面介紹的文件輸入流和文件輸出流也不例外。NIO文件讀寫-FileChannelChannel(通道)表示IO源與目標打開的連接,Channel類似於傳統的流,但Channel本身不能直接訪問數據,只能與Buffer進行交互。Channel(通道)主要用於傳輸數據,從緩衝區的一側傳到另一側的實體(如File、Socket),支持雙向傳遞。正如SocketChannel是客戶端與服務端通信的通道,FileChannel就是我們讀寫文件的通道。FileChannel是線程安全的,也就是一個FileChannel可以被多個線程使用。對於多線程操作,同時只會有一個線程能對該通道所在文件進行修改。如果需要確保多線程的寫入順序,就必須要轉為隊列寫入。FileChannel可通過FileOutputStream、FileInputStream、RandomAccessFile獲取,也可以通過FileChannel#open方法打開一個通道。以通過FileOutputStream獲取FileChannel為例,通過FileOutputStream或RandomAccessFile獲取FileChannel方法相同,代碼如下。

public class FileChannelStu{
public void testGetFileCahnnel(){
try(FileOutputStream fos = new FileOutputStream("/tmp/test.log");
FileChannel fileChannel = fos.getChannel()){
// do....
}catch (IOException exception){
}
}
}

需要注意,通過FileOutputStream獲取的FileChannel只能執行寫操作,通過FileInputStream獲取的FileChannel只能執行讀操作,原因可查看getChannel方法源碼。通過FileOutputStream或FileInputStream或RandomAccessFile打開的FileChannel,在流關閉時也會被關閉,可查看這幾個類的close方法源碼。若想要獲取一個同時支持讀和寫的FileChannel需要通過open方法打開,代碼如下。

public class FileChannelStu{
public void testOpenFileCahnnel(){
FileChannel channel = FileChannel.open(
Paths.get(URI.create("file:" + rootPath + "/" + postion.fileName)),
StandardOpenOption.READ,StandardOpenOption.WRITE);
// do....
channel.close();
}
}

open方法第二個變長參數傳StandardOpenOption.READ和StandardOpenOption.WRITE即可打開一個雙向讀寫的通道。FileChannel允許對文件加鎖,文件鎖是進程級別的,不是線程級別的,文件鎖可以解決多個進程並發訪問、修改同一個文件的問題。文件鎖會被當前進程持有,一旦獲取到文件鎖就要調用一次release釋放鎖,當關閉對應的FileChannel對象時或當前JVM進程退出時,鎖也會自動被釋鎖。

public class FileChannelStu{
public void testFileLock(){
FileChannel channel = this.channel;
FileLock fileLock = null;
try {
fileLock = channel.lock();// 獲取文件鎖
// 執行寫操作
channel.write(...);
channel.write(...);
} finally {
if (fileLock != null) {
fileLock.release(); // 釋放文件鎖
}
}
}
}

當然,只要我們能確保同時只有一個進程對文件執行寫操作,那麼就不需要鎖文件。RocketMQ也並沒有使用文件鎖,因為每個Broker有自己數據目錄,即使一臺機器上部署多個Broker也不會有多個進程對同一個日記文件操作的情況。

public class FileChannelStu{
public void testWrite(){
FileChannel channel = this.channel;
channel.write(...);
channel.write(...);
}
}

這裡還存在一個問題,就是並發寫數據問題。雖然FileChannel是線程安全的,但兩次write並不是原子性操作,如果要確保兩次write是連續寫入的,還必須要加鎖。在RocketMQ中,通過引用計數器替代了鎖。FileChannel提供的force方法用於刷盤,即調用作業系統的fsync函數,使用如下。

public class FileChannelStu{
public void closeChannel(){
this.channel.force(true);
this.channel.close();
}
}

force方法的參數表示除強制寫入內容更改外,文件元數據的更改是否也強制寫入。後面使用MappedByteBuffer時,可直接使用MappedByteBuffer的force方法。FileChannel的force方法最終調用的C方法源碼如下:

JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this,
jobject fdo, jboolean md)
{
jint fd = fdval(env, fdo);
int result = 0;
if (md == JNI_FALSE) {
result = fdatasync(fd);
} else {
result = fsync(fd);
}
return handle(env, result, "Force failed");
}

參數md對應調用force方法傳遞的metaData參數。使用FileChannel支持seek(position)到指定位置讀或寫數據,代碼如下。

public class FileChannelStu{
public void testSeekWrite(){
FileChannel channel = this.channel;
synchronized (channel) {
channel.position(100);
channel.write(ByteBuffer.wrap(toByte(index)));
channel.write(ByteBuffer.wrap(toByte(offset)));
}
}
}

上述例子的作用是將指針移動到物理偏移量100byte位置處,順序寫入index和offset。讀取同理,代碼如下。

public class FileChannelStu{
public void testSeekRead(){
FileChannel channel = this.channel;
synchronized (channel) {
channel.position(100);
ByteBuffer buffer = ByteBuffer.allocate(16);
int realReadLength = channel.read(buffer);
if(realReadLength==16){
long index = buffer.getLong();
long offset = buffer.getLong();
}
}
}
}

其中read方法返回的是實際讀取的字節數,如果返回-1則代表已經是文件尾部了,沒有剩餘內容可讀取。使用MappedByteBuffer讀寫文件MappedByteBuffer是Java提供的基於作業系統虛擬內存映射(MMAP)技術的文件讀寫API,底層不再通過read、write、seek等系統調用實現文件的讀寫。我們需要通過FileChannel#map方法將文件的一個區域映射到內存中,代碼如下。

public class MappedByteBufferStu{
@Test
public void testMappedByteBuffer() throws IOException {
FileChannel fileChannel = FileChannel.open(Paths.get(URI.create("file:/tmp/test/test.log")),
StandardOpenOption.WRITE, StandardOpenOption.READ);
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
fileChannel.close();
mappedByteBuffer.position(1024);
mappedByteBuffer.putLong(10000L);
mappedByteBuffer.force();
}
}

上面代碼的功能是通過FileChannel將文件[0~4096)區域映射到內存中,調用FileChannel的map方法返回MappedByteBuffer,在映射之後關閉通道,隨後在指定位置處寫入一個8位元組的long類型整數,最後調用force方法將寫入數據從內存寫回磁碟(刷盤)。映射一旦建立了,就不依賴於用於創建它的文件通道,因此在創建MappedByteBuffer之後我們就可以關閉通道了,對映射的有效性沒有影響。實際上將文件映射到內存比通過read、write系統調用方法讀取或寫入幾十KB的數據要昂貴,從性能的角度來看,MappedByteBuffer適合用於將大文件映射到內存中,如上百M、上GB的大文件。需要注意的是,如果FileChannel是只讀模式,那麼map方法的映射模式就不能指定為READ_WRITE。如果文件是剛剛創建的,只要映射成功,文件的大小就會變成(0+position+size)。通過MappedByteBuffer讀取數據示例如下:

public class MappedByteBufferStu{
@Test
public void testMappedByteBufferOnlyRead() throws IOException {
FileChannel fileChannel = FileChannel.open(Paths.get(URI.create("file:/tmp/test/test.log")),
StandardOpenOption.READ);
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, 4096);
fileChannel.close();
mappedByteBuffer.position(1024);
long value = mappedByteBuffer.getLong();
System.out.println(value);
}
}

mmap繞過了read、write系統函數調用,繞過了一次數據從內核空間到用戶空間的拷貝,即實現零拷貝,MappedByteBuffer使用直接內存而非JVM的堆內存。mmap只是在虛擬內存分配了地址空間,只有在第一次訪問虛擬內存的時候才分配物理內存。在mmap之後,並沒有將文件內容加載到物理頁上,而是在虛擬內存中分配地址空間,當進程在訪問這段地址時,通過查找頁表,發現虛擬內存對應的頁沒有在物理內存中緩存則產生缺頁中斷,由內核的缺頁異常處理程序處理,將文件對應內容以頁為單位(4096)加載到物理內存中。由於物理內存是有限的,mmap在寫入數據超過物理內存時,作業系統會進行頁置換,根據淘汰算法,將需要淘汰的頁置換成所需的新頁,所以mmap對應的內存是可以被淘汰的,被淘汰的內存頁如果是髒頁(有過寫操作修改頁內容),則作業系統會先將數據回寫磁碟再淘汰該頁。RocketMQ正是利用MappedByteBuffer實現索引文件的讀寫,實現一個基於文件系統的HashMap。RocketMQ在創建新的CommitLog文件並通過FileChannel獲取MappedByteBuffer時會做一次預熱操作,即每個虛擬內存頁(Page Cache)都寫入四個字節的0x00,並強制刷盤將數據寫到文件中。這個動作的用處是通過讀寫操作把MMAP映射全部加載到物理內存中。並且在預熱之後還做了一個鎖住內存的操作,這是為了避免磁碟交換,防止作業系統把預熱過的頁臨時保存到swap區,防止程序再次讀取交換出去的數據頁時產生缺頁中斷。參考文獻一個只推送原創文章的技術公眾號,分享Java後端相關技術。

相關焦點

  • Java Web安全 || Java基礎 · Java IO/NIO多種讀寫文件方式
    IO和非阻塞模式的NIO,本章節我將列舉一些我們常用於讀寫文件的方式。我們通常讀寫文件都是使用的阻塞模式,與之對應的也就是java.io.FileSystem。java.io.FileInputStream類提供了對文件的讀取功能,Java的其他讀取文件的方法基本上都是封裝了java.io.FileInputStream類,比如:java.io.FileReader。
  • python使用with as處理文件的讀寫
    文件處理的兩種情況:1、忘記關閉文件。2、文件讀寫異常,未做處理。在python中使用with語句,可以自動調用close()方法,同時也解決了異常問題。with open('test.txt','w') as f:f.write('Hello, python!')
  • Java NIO Buffer【MappedByteBuffer】概述與FileChannel的聯繫
    「 NIO【Non-blocking IO非阻塞式IO】,可以讓你非阻塞的使用
  • 每日一課 | Apache POI –用Java讀寫Excel文件
    在本文中,我們將討論如何使用Apache POI讀寫Excel文件
  • C 文件讀寫
    w打開一個文本文件,允許寫入文件。如果文件不存在,則會創建一個新文件。在這裡,您的程序會從文件的開頭寫入內容。如果文件存在,則該會被截斷為零長度,重新寫入。a打開一個文本文件,以追加模式寫入文件。如果文件不存在,則會創建一個新文件。在這裡,您的程序會在已有的文件內容中追加內容。r+打開一個文本文件,允許讀寫文件。w+打開一個文本文件,允許讀寫文件。
  • 使用pandas進行文件讀寫
    pandas支持讀取非常多類型的文件,示意如下對於文本文件,支持csv, json等格式,當然也支持tsv文本文件;對於二進位文件,支持excel,python序列化文件,hdf5等格式;此外,還支持SQL資料庫文件的讀寫。在日常開發中,最經典的使用場景就是處理csv,tsv文本文件和excel文件了。
  • C# FileStream類:文件讀寫
    在 C# 語言中文件讀寫流使用 FileStream 類來表示,FileStream 類主要用於文件的讀寫,不僅能讀寫普通的文本文件,還可以讀取圖像文件
  • python讀寫文件
    今天我們就以這幾個需求為背景來看看python是如何讀寫文件的。基本概念介紹我們知道python中一切都是對象,「文件」也不例外。下面的實驗可以看出文件是名叫『_io.TextIOWrapper』的class。
  • Python文件讀寫方法
    f1 =open('文件位置', mode='r', encoding='utf-8')# 文件位置可以為絕對為位置,在根目錄下開始的位置,在與程序在相同目錄下的為相對位置# mode 填寫讀寫方式 r:文件只讀 rb: f = open('文件位置
  • python3之如何讀寫文件
    文件的讀寫是在實際開發中經常會遇到的,因此掌握文件的讀寫是必須的。讀文件首先通過一個最簡單的例子感受一下python讀文件的函數。例子中只有兩行代碼,第一行代碼是調用open函數,參數是文件路徑,返回的是一個文件對象。第二行代碼是使用print列印文件對象讀取的內容。可以看出python讀文件的操作很簡單,打開文件(open)、讀取文件(file.read)。當然用完文件應該關閉文件,調用file的close函數即可。
  • Python基礎教程——文件讀寫
    文件讀寫是我們最常見的一個需求,而且,更多的時候,我們是讀寫文本文本,直接讀寫二進位文件是很少見的。一切都要既快又簡單一口氣讀取整個文件的內容是不是也很簡單?需要注意的是,按行遍歷的時候,行尾帶的回車也會讀進來,所以使用print輸出的時候,每一行後面會有個空行,你可以根據需要使用rstrip函數給它刪除掉即可。
  • python讀寫json文件
    6741810096, "q20_rate":0.980488, "q30_rate":0.941583, "read1_mean_length":149, "read2_mean_length":149, "gc_content":0.46685 }    } }上述文件截取自
  • C語言操作EXCEL文件(讀寫)
    C語言操作EXCEL文件(讀寫)本文主要介紹通過純C語言進行EXCEL的讀寫操作:(如果運行結果均是0,請看文章最後一節)在之前需要使用
  • 「讀寫教室」:小學讀寫教學的一種演進
    最初,阿特維爾僅在初中階段推行「閱讀教室」,但是隨著越來越多的教師加入和不斷嘗試,推廣和使用範圍拓展到了幼兒園至八年級。在「寫作教室」中,學生自主選擇寫作話題,並在創作完成後通過寫作分享會等形式分享自己的作品和收穫。「寫作教室」一般採用微課、獨立寫作和分享的課堂教學方式。
  • 持續讀寫不算啥 4K隨機讀寫才是SSD專長
    固態硬碟的確能夠極致的提升電腦開機速度,但這並不是因為固態硬碟的持續讀寫速度優於機械硬碟而實現的,這其中的關鍵在於4K隨機讀寫性能。而機械硬碟就不說了,使用電腦時輕微的移動都有可能產生壞道。    在性能上,SSD並不是完勝機械硬碟,要是認為它在各個方面都是100%強大的話,在某些時候可是會鬧笑話的。其實超大容量機械硬碟寫入速度能達到160MB/s,突發速度甚至可以上到477MB/s,幾乎與一些低端SSD相當。那麼為什麼更換SSD之後感覺電腦變快了呢?在於小文件隨機性能上SSD有明顯的變化。
  • Java並發包下Java多線程並發之讀寫鎖鎖學習第五篇-讀寫鎖
    Java多線程並發之讀寫鎖本文主要內容:讀寫鎖的理論;通過生活中例子來理解讀寫鎖;讀寫鎖的代碼演示;讀寫鎖總結。通過理論(總結)-例子-代碼-然後再次總結,這四個步驟來讓大家對讀寫鎖的深刻理解。本篇是《凱哥(凱哥Java:kagejava)並發編程學習》系列之《Lock系列》教程的第七篇:《Java並發包下鎖學習第七篇:讀寫鎖》。一:讀寫鎖的理論什麼是讀寫鎖?
  • 文科生快速入門python(十三) | 文件讀寫詳解
    所以,讀取一個文件,需要通過open()函數打開文件並生成 file object文件對象,並將該文件對象賦值於一個變量,再選擇read()等方法對該文件對象進行讀寫操作,在完成讀寫等操作後,需要再使用close()關閉該文件。
  • python文件讀寫的基本操作
    創建一個文件使用電腦創建一個文件非常簡單,滑鼠右鍵新建就可以搞定,用程序創建也非常簡單,下面一行代碼就搞定。>解釋:用記事本打開文件發現裡面多了一行文字,這就是剛剛我們寫進去的內容,通常打開文件後最後不再使用了還需要將其關閉。
  • Python入門 - 如何在Python中讀寫文件
    任何文件在讀寫之前都需要打開。大多數程式語言都使用open()方法來打開文件,以便使用文件對象(file object)讀寫。可以使用不同類型的文件訪問模式作為open()方法的參數,以說明打開文件的目的。這個參數是可選的。close()方法用於在完成文件操作後釋放文件對象佔用的資源。Python編程可以處理兩種類型的文件。它們是文本文件和二進位文件。
  • 「讀寫教室」理念下的讀寫教學嘗試 ——以《小巴掌童話》讀寫交流課為例
    筆者便讓他們和同伴隨機組成「文學圈」,進行深度論證。我們還舉辦了一個簡潔而不簡單的歡迎儀式,讓《小巴掌童話》中的角色一一「來到教室」,走上講臺。「文學圈」的討論交流方式非常符合三年級學生愛和同伴交流的心理,他們自然而然組成兩到三人的「文學圈」,一開始,你一言我一語,呈無序狀態。