亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

文件IO操作的方法是什么

發布時間:2022-01-04 17:24:41 來源:億速云 閱讀:151 作者:iii 欄目:服務器

本篇內容介紹了“文件IO操作的方法是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

01

/背景

已經過去的中間件性能挑戰賽,和正在進行中的 第一屆 PolarDB 數據性能大賽 都涉及到了文件操作,合理地設計架構以及正確地壓榨機器的讀寫性能成了比賽中獲取較好成績的關鍵。正在參賽的我收到了幾位公眾號讀者朋友的反饋,他們大多表達出了這樣的煩惱:“對比賽很感興趣,但不知道怎么入門”,“能跑出成績,但相比前排的選手,成績相差10倍有余”…為了能讓更多的讀者參與到之后相類似的比賽中來,我簡單整理一些文件IO操作的最佳實踐,而不涉及整體系統的架構設計,希望通過這篇文章的介紹,讓你能夠歡快地參與到之后類似的性能挑戰賽之中來。

02

/知識點梳理

本文主要關注的 Java 相關的文件操作,理解它們需要一些前置條件,比如 PageCache,Mmap(內存映射),DirectByteBuffer(堆外緩存),順序讀寫,隨機讀寫...不一定需要完全理解,但至少知道它們是個啥,因為本文將會主要圍繞這些知識點來展開描述。

03

/初識 FileChannel 和 MMAP

首先,文件IO類型的比賽最重要的一點,就是選擇好讀寫文件的方式,那 JAVA 中文件IO有多少種呢?原生的讀寫方式大概可以被分為三種:普通IO,FileChannel(文件通道),MMAP(內存映射)。區分他們也很簡單,例如 FileWriter,FileReader 存在于 java.io 包中,他們屬于普通IO;FileChannel 存在于 java.nio 包中,屬于 NIO 的一種,但是注意 NIO 并不一定意味著非阻塞,這里的 FileChannel 就是阻塞的;較為特殊的是后者 MMAP,它是由 FileChannel 調用 map 方法衍生出來的一種特殊讀寫文件的方式,被稱之為內存映射。

使用 FIleChannel 的方式:

FileChannel fileChannel = new RandomAccessFile(new File("db.data"), "rw").getChannel();

獲取 MMAP 的方式:

MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, filechannel.size();

MappedByteBuffer 便是 JAVA 中 MMAP 的操作類。

面向于字節傳輸的傳統 IO 方式遭到了我們的唾棄,我們重點探討 FileChannel 和 MMAP 這兩種讀寫方式的區別。

04

/FileChannel 讀寫

// 寫byte[] data = new byte[4096];long position = 1024L;//指定 position 寫入 4kb 的數據fileChannel.write(ByteBuffer.wrap(data), position);//從當前文件指針的位置寫入 4kb 的數據fileChannel.write(ByteBuffer.wrap(data));// 讀ByteBuffer buffer = ByteBuffer.allocate(4096);long position = 1024L;//指定 position 讀取 4kb 的數據fileChannel.read(buffer,position);//從當前文件指針的位置讀取 4kb 的數據fileChannel.read(buffer);

FileChannel 大多數時候是和 ByteBuffer 這個類打交道,你可以將它理解為一個 byte[] 的封裝類,提供了豐富的 API 去操作字節,不了解的同學可以去熟悉下它的 API。值得一提的是,write 和 read 方法均是線程安全的,FileChannel 內部通過一把 privatefinalObjectpositionLock=newObject(); 鎖來控制并發。

FileChannel 為什么比普通 IO 要快呢?這么說可能不嚴謹,因為你要用對它,FileChannel 只有在一次寫入 4kb 的整數倍時,才能發揮出實際的性能,這得益于 FileChannel 采用了 ByteBuffer 這樣的內存緩沖區,讓我們可以非常精準的控制寫盤的大小,這是普通 IO 無法實現的。4kb 一定快嗎?也不嚴謹,這主要取決你機器的磁盤結構,并且受到操作系統,文件系統,CPU 的影響,例如中間件性能挑戰賽時的那塊盤,一次至少寫入 64kb 才能發揮出最高的 IOPS。

文件IO操作的方法是什么

然而 PolarDB 這塊盤就完全不一樣了,可謂是異常彪悍,具體是如何的表現由于比賽仍在進行中,不予深究,但憑借著 benchmark everyting 的技巧,我們完全可以測出來。

另外一點,成就了 FileChannel 的高效,介紹這點之前,我想做一個提問:FileChannel 是直接把 ByteBuffer 中的數據寫入到磁盤嗎?思考幾秒…答案是:NO。ByteBuffer 中的數據和磁盤中的數據還隔了一層,這一層便是 PageCache,是用戶內存和磁盤之間的一層緩存。我們都知道磁盤 IO 和內存 IO 的速度可是相差了好幾個數量級。我們可以認為 filechannel.write 寫入 PageCache 便是完成了落盤操作,但實際上,操作系統最終幫我們完成了 PageCache 到磁盤的最終寫入,理解了這個概念,你就應該能夠理解 FileChannel 為什么提供了一個 force() 方法,用于通知操作系統進行及時的刷盤。

同理,當我們使用 FileChannel 進行讀操作時,同樣經歷了:磁盤->PageCache->用戶內存這三個階段,對于日常使用者而言,你可以忽略掉 PageCache,但作為挑戰者參賽,PageCache 在調優過程中是萬萬不能忽視的,關于讀操作這里不做過多的介紹,我們在下面的小結中還會再次提及,這里當做是引出 PageCache 的概念。

05

/MMAP 讀寫

// 寫byte[] data = new byte[4];int position = 8;//從當前 mmap 指針的位置寫入 4b 的數據mappedByteBuffer.put(data);//指定 position 寫入 4b 的數據MappedByteBuffer subBuffer = mappedByteBuffer.slice();subBuffer.position(position);subBuffer.put(data);// 讀byte[] data = new byte[4];int position = 8;//從當前 mmap 指針的位置讀取 4b 的數據mappedByteBuffer.get(data);//指定 position 讀取 4b 的數據MappedByteBuffer subBuffer = mappedByteBuffer.slice();subBuffer.position(position);subBuffer.get(data);

FileChannel 已經足夠強大了,MappedByteBuffer 還能玩出什么花來呢?請容許我賣個關子先,先介紹一下 MappedByteBuffer 的使用注意點。

當我們執行 fileChannel.map(FileChannel.MapMode.READ_WRITE,0,1.5*1024*1024*1024); 之后,觀察一下磁盤上的變化,會立刻獲得一個 1.5G 的文件,但此時文件的內容全部是 0(字節 0)。這符合 MMAP 的中文描述:內存映射文件,我們之后對內存中 MappedByteBuffer 做的任何操作,都會被最終映射到文件之中,

mmap 把文件映射到用戶空間里的虛擬內存,省去了從內核緩沖區復制到用戶空間的過程,文件中的位置在虛擬內存中有了對應的地址,可以像操作內存一樣操作這個文件,相當于已經把整個文件放入內存,但在真正使用到這些數據前卻不會消耗物理內存,也不會有讀寫磁盤的操作,只有真正使用這些數據時,也就是圖像準備渲染在屏幕上時,虛擬內存管理系統 VMS 才根據缺頁加載的機制從磁盤加載對應的數據塊到物理內存進行渲染。這樣的文件讀寫文件方式少了數據從內核緩存到用戶空間的拷貝,效率很高

看了稍微官方一點的描述,你可能對 MMAP 有了些許的好奇,有這么厲害的黑科技存在的話,還有 FileChannel 存在的意義嗎!并且網上很多文章都在說,MMAP 操作大文件性能比 FileChannel 搞出一個數量級!然而,通過我比賽的認識,MMAP 并非是文件 IO 的銀彈,它只有在一次寫入很小量數據的場景下才能表現出比 FileChannel 稍微優異的性能。緊接著我還要告訴你一些令你沮喪的事,至少在 JAVA 中使用 MappedByteBuffer 是一件非常麻煩并且痛苦的事,主要表現為三點:

  1. MMAP 使用時必須實現指定好內存映射的大小,并且一次 map 的大小限制在 1.5G 左右,重復 map 又會帶來虛擬內存的回收、重新分配的問題,對于文件不確定大小的情形實在是太不友好了。

  2. MMAP 使用的是虛擬內存,和 PageCache 一樣是由操作系統來控制刷盤的,雖然可以通過 force() 來手動控制,但這個時間把握不好,在小內存場景下會很令人頭疼。

  3. MMAP 的回收問題,當 MappedByteBuffer 不再需要時,可以手動釋放占用的虛擬內存,但…方式非常的詭異。

public static void clean(MappedByteBuffer mappedByteBuffer) {    ByteBuffer buffer = mappedByteBuffer;    if (buffer == null || !buffer.isDirect() || buffer.capacity() == 0)        return;    invoke(invoke(viewed(buffer), "cleaner"), "clean");}private static Object invoke(final Object target, final String methodName, final Class<?>... args) {    return AccessController.doPrivileged(new PrivilegedAction<Object>() {        public Object run() {            try {                Method method = method(target, methodName, args);                method.setAccessible(true);                return method.invoke(target);            } catch (Exception e) {                throw new IllegalStateException(e);            }        }    });}private static Method method(Object target, String methodName, Class<?>[] args)        throws NoSuchMethodException {    try {        return target.getClass().getMethod(methodName, args);    } catch (NoSuchMethodException e) {        return target.getClass().getDeclaredMethod(methodName, args);    }}private static ByteBuffer viewed(ByteBuffer buffer) {    String methodName = "viewedBuffer";    Method[] methods = buffer.getClass().getMethods();    for (int i = 0; i < methods.length; i++) {        if (methods[i].getName().equals("attachment")) {            methodName = "attachment";            break;        }    }    ByteBuffer viewedBuffer = (ByteBuffer) invoke(buffer, methodName);    if (viewedBuffer == null)        return buffer;    else        return viewed(viewedBuffer);}

對的,你沒看錯,這么長的代碼僅僅是為了干回收 MappedByteBuffer 這一件事。

所以我建議,優先使用 FileChannel 去完成初始代碼的提交,在必須使用小數據量(例如幾個字節)刷盤的場景下,再換成 MMAP 的實現,其他場景 FileChannel 完全可以 cover(前提是你理解怎么合理使用 FileChannel)。至于 MMAP 為什么在一次寫入少量數據的場景下表現的比 FileChannel 優異,我還沒有查到理論根據,如果你有相關的線索,歡迎留言。理論分析下,FileChannel 同樣是寫入內存,但比 MMAP 多了一次內核緩沖區與用戶空間互相復制的過程,所以在極端場景下,MMAP 表現的更加優秀。至于 MMAP 分配的虛擬內存是否就是真正的 PageCache 這一點,我覺得可以近似理解成 PageCache。

06

/順序讀比隨機讀快,順序寫比隨機寫快

無論你是機械硬盤還是 SSD,這個結論都是一定成立的,雖然背后的原因不太一樣,我們今天不討論機械硬盤這種古老的存儲介質,重點 foucs 在 SSD 上,來看看在它之上進行的隨機讀寫為什么比順序讀寫要慢。即使各個 SSD 和文件系統的構成具有差異性,但我們今天的分析同樣具備參考價值。

首先,什么是順序讀,什么是隨機讀,什么是順序寫,什么是隨機寫?可能我們剛接觸文件 IO 操作時并不會有這樣的疑惑,但寫著寫著,自己都開始懷疑自己的理解了,不知道你有沒有經歷過這樣類似的階段,反正我有一段時間的確懷疑過。那么,先來看看兩段代碼:

寫入方式一:64個線程,用戶自己使用一個 atomic 變量記錄寫入指針的位置,并發寫入

ExecutorService executor = Executors.newFixedThreadPool(64);AtomicLong wrotePosition = new AtomicLong(0);for(int i=0;i<1024;i++){    final int index = i;    executor.execute(()->{        fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),wrote.getAndAdd(4*1024));    })}

寫入方式二:給 write 加了鎖,保證了同步。

ExecutorService executor = Executors.newFixedThreadPool(64);AtomicLong wrotePosition = new AtomicLong(0);for(int i=0;i<1024;i++){    final int index = i;    executor.execute(()->{        write(new byte[4*1024]);    })}public synchronized void write(byte[] data){    fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),wrote.getAndAdd(4*1024));}

答案是方式二才算順序寫,順序讀也是同理。對于文件操作,加鎖并不是一件非常可怕的事,不敢同步 write/read 才可怕!有人會問:FileChannel 內部不是已經有 positionLock 保證寫入的線程安全了嗎,為什么還要自己加同步?為什么這樣會快?我用大白話來回答的話就是多線程并發 write 并且不加同步,會導致文件空洞,它的執行次序可能是

時序1:thread1 write position[0~4096)

時序2:thread3 write position[8194~12288)

時序2:thread2 write position[4096~8194)

所以并不是完全的“順序寫”。不過你也別擔心加鎖會導致性能下降,我們會在下面的小結介紹一個優化:通過文件分片來減少多線程讀寫時鎖的沖突。

再來分析原理,順序讀為什么會比隨機讀要快?順序寫為什么比隨機寫要快?這兩個對比其實都是一個東西在起作用:PageCache,前面我們已經提到了,它是位于 application buffer(用戶內存)和 disk file(磁盤)之間的一層緩存。

文件IO操作的方法是什么

以順序讀為例,當用戶發起一個 fileChannel.read(4kb) 之后,實際發生了兩件事

  1. 操作系統從磁盤加載了 16kb 進入 PageCache,這被稱為預讀

  2. 操作通從 PageCache 拷貝 4kb 進入用戶內存

最終我們在用戶內存訪問到了 4kb,為什么順序讀快?很容量想到,當用戶繼續訪問接下來的[4kb,16kb]的磁盤內容時,便是直接從 PageCache 去訪問了。試想一下,當需要訪問 16kb 的磁盤內容時,是發生4次磁盤 IO 快,還是發生1次磁盤 IO+4 次內存 IO 快呢?答案是顯而易見的,這一切都是 PageCache 帶來的優化。

深度思考:當內存吃緊時,PageCache 的分配會受影響嗎?PageCache 的大小如何確定,是固定的 16kb 嗎?我可以監控 PageCache 的命中情況嗎? PageCache 會在哪些場景失效,如果失效了,我們又要哪些補救方式呢?

我進行簡單的自問自答,背后的邏輯還需要讀者去推敲:

  • 當內存吃緊時,PageCache 的預讀會受到影響,實測,并沒有搜到到文獻支持

  • PageCache 是動態調整的,可以通過 linux 的系統參數進行調整,默認是占據總內存的 20%

  • https://github.com/brendangregg/perf-tools github 上一款工具可以監控 PageCache

  • 這是很有意思的一個優化點,如果用 PageCache 做緩存不可控,不妨自己做預讀如何呢?

順序寫的原理和順序讀一致,都是收到了 PageCache 的影響,留給讀者自己推敲一下。

07

/直接內存 VS 堆內內存

前面 FileChannel 的示例代碼中已經使用到了堆內內存: ByteBuffer.allocate(4*1024),ByteBuffer 提供了另外的方式讓我們可以分配堆外內存 : ByteBuffer.allocateDirect(4*1024)。這就引來的一系列的問題,我什么時候應該使用堆內內存,什么時候應該使用直接內存?

我不花太多筆墨去闡述了,直接上對比:

文件IO操作的方法是什么

關于堆內內存和堆外內存的一些最佳實踐:

  1. 當需要申請大塊的內存時,堆內內存會受到限制,只能分配堆外內存。

  2. 堆外內存適用于生命周期中等或較長的對象。( 如果是生命周期較短的對象,在 YGC 的時候就被回收了,就不存在大內存且生命周期較長的對象在 FGC 對應用造成的性能影響 )。

  3. 直接的文件拷貝操作,或者 I/O 操作。直接使用堆外內存就能少去內存從用戶內存拷貝到系統內存的消耗

  4. 同時,還可以使用池+堆外內存 的組合方式,來對生命周期較短,但涉及到 I/O 操作的對象進行堆外內存的再使用( Netty中就使用了該方式 )。在比賽中,盡量不要出現 頻繁 newbyte[] ,創建內存區域再回收也是一筆不小的開銷,使用 ThreadLocal<ByteBuffer> 和 ThreadLocal<byte[]> 往往會給你帶來意外的驚喜~

  5. 創建堆外內存的消耗要大于創建堆內內存的消耗,所以當分配了堆外內存之后,盡可能復用它。


08

/黑魔法:UNSAFE

public class UnsafeUtil {    public static final Unsafe UNSAFE;    static {        try {            Field field = Unsafe.class.getDeclaredField("theUnsafe");            field.setAccessible(true);            UNSAFE = (Unsafe) field.get(null);        } catch (Exception e) {            throw new RuntimeException(e);        }    }}

我們可以使用 UNSAFE 這個黑魔法實現很多無法想象的事,我這里就稍微介紹一兩點吧。

實現直接內存與內存的拷貝:

ByteBuffer buffer = ByteBuffer.allocateDirect(4 * 1024 * 1024);long addresses = ((DirectBuffer) buffer).address();byte[] data = new byte[4 * 1024 * 1024];UNSAFE.copyMemory(data, 16, null, addresses, 4 * 1024 * 1024);

copyMemory 方法可以實現內存之間的拷貝,無論是堆內和堆外,1~2 個參數是 source 方,3~4 是 target 方,第 5 個參數是 copy 的大小。如果是堆內的字節數組,則傳遞數組的首地址和 16 這個固定的 ARRAYBYTEBASE_OFFSET 偏移常量;如果是堆外內存,則傳遞 null 和直接內存的偏移量,可以通過 ((DirectBuffer) buffer).address() 拿到。為什么不直接拷貝,而要借助 UNSAFE?當然是因為它快啊!少年!另外補充:MappedByteBuffer 也可以使用 UNSAFE 來 copy 從而達到寫盤/讀盤的效果哦。

至于 UNSAFE 還有那些黑科技,可以專門去了解下,我這里就不過多贅述了。

09

/文件分區

前面已經提到了順序讀寫時我們需要對 write,read 加鎖,并且我一再強調的一點是:加鎖并不可怕,文件 IO 操作并沒有那么依賴多線程。但是加鎖之后的順序讀寫必然無法打滿磁盤 IO,如今系統強勁的 CPU 總不能不壓榨吧?我們可以采用文件分區的方式來達到一舉兩得的效果:既滿足了順序讀寫,又減少了鎖的沖突。

那么問題又來了,分多少合適呢?文件多了,鎖沖突變降低了;文件太多了,碎片化太過嚴重,單個文件的值太少,緩存也就不容易命中,這樣的 trade off 如何平衡?沒有理論答案,benchmark everything~

10

/Direct IO

文件IO操作的方法是什么

最后我們來探討一下之前從沒提到的一種 IO 方式,Direct IO,什么,Java 還有這東西?博主你騙我?之前怎么告訴我只有三種 IO 方式!別急著罵我,嚴謹來說,這并不是 JAVA 原生支持的方式,但可以通過 JNA/JNI 調用 native 方法做到。從上圖我們可以看到 :Direct IO 繞過了 PageCache,但我們前面說到過,PageCache 可是個好東西啊,干嘛不用他呢?再仔細推敲一下,還真有一些場景下,Direct IO 可以發揮作用,沒錯,那就是我們前面沒怎么提到的:隨機讀。當使用 fileChannel.read() 這類會觸發 PageCache 預讀的 IO 方式時,我們其實并不希望操作系統幫我們干太多事,除非真的踩了狗屎運,隨機讀都能命中 PageCache,但幾率可想而知。Direct IO 雖然被 Linus 無腦噴過,但在隨機讀的場景下,依舊存在其價值,減少了 Block IO Layed(近似理解為磁盤) 到 Page Cache 的 overhead。

話說回來,Java 怎么用 Direct IO 呢?有沒有什么限制呢?前面說過,Java 目前原生并不支持,但也有好心人封裝好了 Java 的 JNA 庫,實現了 Java 的 Direct IO

int bufferSize = 20 * 1024 * 1024;DirectRandomAccessFile directFile = new DirectRandomAccessFile(new File("dio.data"), "rw", bufferSize);for(int i= 0;i< bufferSize / 4096;i++){    byte[] buffer = new byte[4 * 1024];    directFile.read(buffer);    directFile.readFully(buffer);}directFile.close();


“文件IO操作的方法是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

io
AI

和林格尔县| 来安县| 石泉县| 高安市| 兴国县| 麦盖提县| 铁岭市| 五峰| 会泽县| 库车县| 改则县| 连城县| 台江县| 贵溪市| 乌什县| 延寿县| 罗田县| 班戈县| 普陀区| 雷波县| 南昌县| 鄂州市| 新平| 上犹县| 肥东县| 永福县| 百色市| 张家口市| 信宜市| 固原市| 青海省| 平顶山市| 崇礼县| 海门市| 盐山县| 浦北县| 红安县| 北京市| 高雄市| 武义县| 耿马|