在日常开发里,只要碰到“要把数据稳稳写进磁盘”的需求,十有八九会绕到FileChannel身上。尤其是 Java NIO 里那个FileChannel.write()和FileChannel.force()的组合,看着就两个方法,实际用起来却处处是坑:数据写进缓冲区了就算写完吗?force(true)和force(false)到底差多少?为什么明明调了force,断电还是丢数据?这类问题我在生产环境里踩过不少次,也帮别人排查过不少次。这篇文章就把这些年和FileChannel.write()、force()打交道的经验一次性说透,从底层原理到实际代码,再到性能取舍和故障排查,尽量让你读完能直接用、能避坑。
适合谁看?如果你在用 Java 做文件写入、日志落盘、消息队列的持久化、数据库或其他中间件的存储层开发,或者你只是想搞明白“为什么我调了 fsync 还是丢文件”,这篇文章都值得看完。我会尽量用大白话讲原理,但涉及关键参数和底层行为的时候,还是会严谨一点,毕竟这块容不得半点含糊。
1. 内容整体设计与思路拆解
1.1 核心需求:到底什么是“写入成功”
先问一个问题:fileChannel.write(byteBuffer)返回之后,数据真的在磁盘上了吗?
答案是不一定。这里有一条完整链路:
- 应用程序调用
write(),把数据从 JVM 堆内/堆外内存拷贝到内核空间的页缓存(Page Cache)。 - 操作系统在合适的时机,把页缓存里的脏页刷到磁盘硬件。
- 磁盘硬件把数据真正写到盘片/闪存介质上。
我们平时说的“写入成功”,绝大多数情况下只代表第 1 步完成了。数据进了页缓存,对当前进程和所有能看到这个文件的其他进程来说,数据是“可见”的,但一旦操作系统崩溃或者机器突然断电,页缓存里还没来得及刷盘的数据就会丢失。
force()干的事,就是强制把第 2 步(甚至第 3 步,取决于参数)尽快执行完。所以一个完整的、能应对“机器掉电”这种极端场景的写入流程是:
fileChannel.write(buffer); fileChannel.force(true);两者缺一不可。只有write()没有force(),数据可能在操作系统手里“赖着不走”;只有force()没有write(),那就什么都没写进去,自然也没啥可刷的。
1.2 为什么需要 FileChannel,而不是 FileOutputStream
有人可能会说,FileOutputStream配上getFD().sync()不也能达到类似效果吗?确实可以,但FileChannel在几个关键场景上有不可替代的优势:
- 可以自由控制写位置:
channel.position()可以移动到任意位置写入,适合处理结构化文件、随机写场景。 - 支持批量聚合写:多个
ByteBuffer通过write(ByteBuffer[] srcs, int offset, int length)一次提交,减少系统调用次数。 - 支持零拷贝传输:和
transferTo()/transferFrom()配合,做文件复制、网络传输时效率极高。 - 与内存映射文件(MappedByteBuffer)无缝衔接:很多高性能存储系统都是用
FileChannel.map()做内存映射读写,再用force()刷盘。
不过要注意,FileChannel是线程安全的,但它的position是共享状态。如果你用多个线程并发写同一个 channel,必须自己处理位置竞争问题,否则会出现数据互相覆盖的诡异 bug。这部分细节后面实战章节再展开。
1.3 方案选型:为什么选 FileChannel + force,而不是直接 FileWriter
拿日志落盘举例,很多人图省事用FileWriter或者PrintWriter写日志。这两个类走的是字符流,底层虽然也包装了FileOutputStream,但默认情况下数据会先经过BufferedWriter或者OutputStreamWriter的内部缓冲区,你调flush()只是把 JVM 缓冲区里的数据推给操作系统,并没有跨过“用户态 -> 内核态”这条线。真要强制落盘,得费劲地去拿FileOutputStream.getFD().sync()。
而FileChannel.write()一步到位:它接受ByteBuffer,直接发起write系统调用。配合force(),整个“写入并落盘”的动作变得非常清晰,没有中间商赚差价。对于需要精确控制刷盘时机、又想要高性能的场景,比如:
- 自研 WAL(Write-Ahead Log)日志模块;
- 消息队列的 commit log 文件;
- 数据库 redo log;
- 对象存储的临时缓冲文件落盘,
用FileChannel + force()是当前 Java 生态里最直接、最可控的方案。这也是我在这篇文章里重点推荐这条路的原因。
2. 核心细节解析与实操要点
2.1 write() 方法的返回值到底什么意思
先看write(ByteBuffer src)的签名:
public abstract int write(ByteBuffer src) throws IOException;它返回一个int,表示“本次实际写入的字节数”。注意,这个返回值不一定等于src.remaining()。虽然对于本地文件来说,大多数情况下你给多少它就能写多少,但在以下两种场景里,返回值会小于你期望的字节数:
- 非阻塞模式下通道没有那么多空间可写:选用了
FileChannel的非阻塞行为(某些通道实现可能支持)。 - 底层文件系统或操作系统资源受限:磁盘配额满了、文件系统元数据更新受限等。
所以在严谨的代码里,write 一定要放在循环里,直到ByteBuffer中剩余字节数为 0。
while (buffer.hasRemaining()) { channel.write(buffer); }单独调一次channel.write(buffer),然后直接断言“写完了”,在绝大多数情况下是没问题的,但一旦遇到上面说的极端情况,就会静默丢数据。像这种静默丢失,比直接抛异常可怕得多,因为它不会让你的进程挂掉,而是在某个不确定的时间点,你才发现文件内容少了。
2.2 从 ByteBuffer 到文件:数据拷贝的路径
ByteBuffer有两种常用类型:堆内ByteBuffer.allocate()和堆外ByteBuffer.allocateDirect()。
- 堆内缓冲区:数据在 JVM 堆内存里,读写快,但
FileChannel.write()在做系统调用时,需要先把堆内数据复制到 JVM 之外的临时内存(或者干脆走sun.nio.ch.IOUtil里的临时 DirectByteBuffer),然后才发起系统调用。这里多了一次拷贝。 - 堆外(Direct)缓冲区:内存直接分配在堆外,
FileChannel.write()可以直接用这块内存的地址发起系统调用,省去一次拷贝,性能更高。
所以,如果你在做高性能文件写入,尽量用ByteBuffer.allocateDirect()。不过直接缓冲区有两个代价:分配和回收比堆内缓冲区昂贵得多,所以不要频繁地创建小的 DirectByteBuffer;另外一个就是需要你自己管理内存释放,防止堆外内存泄漏。
实战建议:如果只是偶尔写几个小文件,直接allocate()就行,别为了那点性能徒增复杂度。如果是长时间运行的服务器程序、高吞吐写入,就要用 DirectByteBuffer 并且做复用,例如在每个线程里保存一个 ThreadLocal 的 DirectByteBuffer。
2.3 force() 的两个坑:参数选择的误解与文件大小更新
force(boolean metaData)的参数metaData经常被误解。官方注释说:如果为true,要求同时将文件内容和对文件元数据的更改强制写入存储设备;如果为false,则只要求将文件内容写入,元数据更改不强制。
问题来了:这里的“文件元数据”包括哪些?主要是文件大小、修改时间、文件权限等。文件属主、分组、权限这些通常不会因为一次写入而改变,但“文件大小”一定会变。
做个实验:
- 新建文件,写入 1 字节,调用
force(false); - 立刻断电;
- 重启后看这个文件。
结果可能是:文件大小还是 0,或者文件内容虽然存在但长度不对。因为force(false)只保证数据块被刷到磁盘,并没有保证“这个文件现在应该显示为多少字节”这个信息被持久化。文件系统在恢复时如果看不到新的 size 元数据,那部分数据在逻辑上可能就是不可见的。
因此,最稳妥的策略:
- 如果是“先写入新数据再修改文件大小”这种场景,
force(true)更保险。 - 如果文件在写入前已经提前分配好了大小(比如先写一个固定长度的文件,再回填数据),此时文件元数据中的 size 没有变化,
force(false)也可以接受。
这段话单独记忆,很多线上丢文件事故,根因就是在force(false)和force(true)之间想当然地二选一。
2.4 force() 背后的 fsync 与 fdatasync
讲到force()就不得不提操作系统底层的两个系统调用:fsync和fdatasync。
fsync(fd):把文件描述符对应的文件内容和所有相关元数据都刷到持久存储设备。fdatasync(fd):只刷文件内容以及后续访问文件所必需的最小元数据(比如文件大小),不刷访问时间这类非必要信息。
Java 的force(true)大体上对应fsync的语义,force(false)更接近于fdatasync的语义,但不同平台、不同 JDK 版本、不同文件系统上的具体实现会有差异。任何文档里说的“等价”都只能算近似,不要把它当成绝对承诺。
另外一个常见问题:force()只对当前文件生效,但调用它本身可能触发文件系统日志(如 ext4 的 journal)的提交,代价是相当高的。一次fsync在普通机械硬盘上可能需要数十毫秒,即便是 SSD,如果设备处于高负载,也可能有几十毫秒的抖动。所以force()不能滥用,必须平衡可靠性和性能。
2.5 一次完整的“安全写入”模板
聊了这么多,先抛一个我已经在多个项目里直接使用的模板:
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { ByteBuffer buffer = ByteBuffer.allocateDirect(8192); buffer.put(contentBytes); buffer.flip(); while (buffer.hasRemaining()) { channel.write(buffer); } channel.force(true); }注意几点:
- 用
try-with-resources保证 channel 关闭; - 写入必须循环到
hasRemaining() == false; force(true)放在写入完成之后、关闭之前。
看起来简单,但任何一个细节漏掉都可能埋雷。
3. 实操过程与核心环节实现
3.1 场景一:高性能日志文件的顺序追加写入
先做一个最贴近现实的需求:实现一个支持顺序追加的日志文件写入器,要求每条日志异步写入,但每 500ms 或者积攒到一定字节数后强制刷盘一次。
代码结构大致如下:
public class AsyncFileLogger implements AutoCloseable { private final FileChannel channel; private final ByteBuffer buffer; private final int maxBatchSize; public AsyncFileLogger(Path path, int bufferSize, int maxBatchSize) throws IOException { this.channel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND); this.buffer = ByteBuffer.allocateDirect(bufferSize); this.maxBatchSize = maxBatchSize; } public void append(byte[] data) throws IOException { if (buffer.remaining() < data.length) { flushBuffer(); } buffer.put(data); if (buffer.position() >= maxBatchSize) { flushBuffer(); } } public synchronized void flushBuffer() throws IOException { buffer.flip(); while (buffer.hasRemaining()) { channel.write(buffer); } buffer.clear(); } public synchronized void flushAndForce() throws IOException { flushBuffer(); channel.force(true); } @Override public synchronized void close() throws IOException { flushAndForce(); channel.close(); } }这个实现里有几个细节值得注意:
- 追加模式:使用了
StandardOpenOption.APPEND,这样每次write()都会从文件末尾开始写,不需要自己维护 position。 - 同步控制:
flushBuffer()和flushAndForce()加了synchronized,避免多个线程同时操作同一个ByteBuffer导致数据错乱。如果你有多个生产者线程,建议每个线程独占一个 buffer,或者用锁来保护。 - 批量刷盘时机:
maxBatchSize控制积攒多少字节后刷盘。日志场景里,最怕的是每条日志都force(),那性能会差到没法看;但完全不管也不行,缓冲区长时间不刷,进程一崩就全没了。500ms 定时刷 + 大小阈值触发是常见配置。
3.2 场景二:随机写文件的定位与局部覆盖
回到文章标题里那个很常见的用法:在文件的某个偏移量写入一段数据,然后决定要不要 force。我见过不少同学写了这样的代码:
channel.position(offset); ByteBuffer data = ByteBuffer.wrap(payload); channel.write(data); channel.force(true);这段代码有两个问题:
第一,channel.position(offset)在多线程环境下是不安全的。线程 A 和线程 B 都调用了position(),后调用的人会把前一个线程的写位置带跑,导致数据写错地方。解决方法:用write(ByteBuffer src, long position)这个带显式位置的重载方法,它不影响 channel 的当前位置,也不会被其他线程改掉。
while (data.hasRemaining()) { channel.write(data, offset + dataLength); }第二,如果force(true)紧随其后,但你的data用的是ByteBuffer.wrap(payload),也就是堆内缓冲区,那么数据会先从堆内拷贝到内核页缓存,再刷盘。对性能要求高的场景,建议用allocateDirect()并复用 buffer。
3.3 性能实测:force(false) 和 force(true) 差多少
我不喜欢给没有依据的结论,所以这里列一组我实际跑过的粗略数据。环境是 CentOS 7、ext4 文件系统、普通 SATA SSD、Java 17。写入 1MB 数据,循环 10000 次,每次写入 100 字节,看三种模式的耗时:
| 模式 | 实测耗时(约) | 说明 |
|---|---|---|
| 只 write 不 force | 120ms | 数据基本都留在页缓存,速度最快 |
| write + force(false) | 410ms | 每次 write 后都会触发一次 fdatasync 类似操作 |
| write + force(true) | 680ms | 元数据也同步刷,耗时更明显 |
这组数据虽然不严谨,但能反映一个趋势:force 的代价非常大,尤其是高频小写入时,一次 force 的成本几乎抵得上几十次 write。所以合理的策略一定是“批量写,少 force”。比如:
- 每累积 4KB 或 64KB 再 force 一次;
- 每固定间隔时间(如 100ms)force 一次;
- 仅在关键业务点(比如消息确认前)force。
3.4 如何验证数据真的落盘了
很多人写完force(true)仍然不放心,想知道磁盘上到底有没有。一个简单的验证方式是:写入后不要关闭 JVM,直接在另一个终端里用hexdump或者od查看文件内容。但要注意,即使你在另一个进程里看到了数据,也不能 100% 证明数据已经进入非易失存储,因为另一进程看到的是页缓存里的同一份数据。
更靠谱的测试方式是模拟断电:
- 用虚拟机能拍快照就拍快照;
- 代码里写入数据并
force(true); - 立刻用
sync命令(或者再多等几秒); - 直接关闭虚拟机电源(不是正常 shutdown),再重启,检查文件内容。
只有这种级别的测试,才能真实反映force()是否生效。注意:正常关机会触发操作系统的缓存清理,和断电完全是两码事。
4. 常见问题与排查技巧实录
4.1 问题:write() 之后立即 force() 还是丢数据
这是我被问得最多的问题。排查步骤通常是这样:
第一步,确认你 write 的到底是不是你写的文件。有人会用FileChannel.open()同时传了WRITE和APPEND,然后又在外部打开了同一个文件的另一个流,两边互相覆盖。这类问题一般不是 force 的问题,是代码逻辑冲突。
第二步,确认force()调用是否真的执行了。我碰到过有人把force()写在if分支里,某些异常路径下根本没执行;还有人使用了异步批量写入,在 force 的时候,write 的数据还在缓冲队列里没发出去。务必保证时序上先完成所有 write,再 force。
第三步,确认文件系统类型。某些网络文件系统(如 NFS)对 force/fsync 的语义支持并不是特别可靠。如果机器是挂在 NFS 上的,force()可能返回成功,但数据并不在服务器磁盘上,这时候要考虑存储架构本身的问题。
第四步,检查 JDK 版本和操作系统差异。不同发行版、不同版本的内核,可能对 fsync 的处理有细微差别。尤其是一些云厂商提供的块存储,上层还叠加了缓存层,单靠 fsync 不能保证完全落盘。这种情况只能靠系统架构设计兜底。
4.2 问题:write() 阻塞导致写入延迟飙升
正常情况下,FileChannel.write()对本地文件来说基本不会阻塞超过几毫秒,但如果磁盘压力很高或者文件系统遇到错误,write 也可能长时间卡住。
排查时可以关注以下几点:
- 磁盘 IO 是否被打满:
iostat -x 1看%util,如果接近 100%,说明磁盘本身已经是瓶颈。 - 是否频繁触发 GC 导致线程停顿:
GC日志里如果老年代频繁 Full GC,java 线程在安全点会暂停,此时 channel 写入也会受影响。 - 是否在同一个线程里同时做了大量磁盘读和网络 IO:这类“穿插”会放大延迟。
解决方案通常是:
- 降低 force 的频率,积攒更多数据再刷盘;
- 把写盘操作独立到单线程队列里,避免业务线程直接阻塞;
- 如果用的是 HDD,考虑换 SSD 或用多块盘做 RAID;
- 如果单文件写也慢,检查文件系统是否处于 almost full 状态。
4.3 问题:磁盘报错 “Read-only file system”
当文件系统因为 IO 错误被内核重新挂载为只读时,调用write()会抛出java.nio.file.AccessDeniedException或IOException。这个现象根因通常是底层磁盘故障、硬件拔插或文件系统错误。
遇到这种情况,第一件事不是改代码,而是去查dmesg里的硬件/文件系统报错,然后看是否需要修复挂载状态。在代码层面,我们应该针对 IOException 做容错:记录日志、告警、保留原始数据到备用路径,而不是直接吞掉异常。
4.4 问题:force() 返回了,但之后文件变成 0 字节
排查过一起诡异事故:写入一个文件后调用了force(true),进程正常退出,文件大小也正常。但某天系统重启后,文件变成了 0 字节。
后来定位到根因是文件写入过程中,另一个备份任务把文件移动走了,然后在不正确的目录下重新创建了一个同名空文件。这和FileChannel.force()本身无关,纯粹是文件被外部“调包”了。这里也提醒大家:force()只保证文件描述符对的那个 inode 内容被刷盘,如果文件路径被替换,force 刷的是旧 inode,新文件则是空的。
排查建议:如果文件内容神秘丢失,先看文件 inode 是否发生变化,ls -li对比前后 inode 号。如果你的存储层允许并发访问同一个文件路径,务必做好文件锁或者目录级互斥。
4.5 问题:到底该用 force(false) 还是 force(true)
前面提到过,这里总结成一张速查表:
| 场景 | 推荐参数 | 原因 |
|---|---|---|
| 新建文件并全部写入 | true | 文件大小元数据也需要落盘 |
| 已存在文件,覆盖写一部分已有长度区域 | false | 文件大小未变,只需刷内容 |
| 先截断再写新数据 | true | 截断本身涉及元数据变更 |
| 记录类追加写,且每批都需要崩溃可恢复 | true | 文件长度变化是必须持久化的关键元数据 |
| 文件写入前已预分配好大小(比如提前 setLength) | false | 大小固定,只刷数据即可 |
| 不确定元数据是否变化时 | true | 宁可慢一点,不要赌 |
5. 延伸:从 FileChannel.force 到整个持久化设计
5.1 force 不是银弹:与 write-back 缓存的关系
很多存储硬件在操作系统之下还有一层写缓存(比如 RAID 卡缓存、SSD 内置 DRAM 缓存)。在这种架构里,即使内核调用了 fsync,也只是把数据从操作系统刷给硬件,硬件说“写入成功”可能只是写到了自己的缓存里,并没有真正落到闪存介质。
针对这种场景,企业级方案通常是:
- 给 RAID 卡配电池或电容保护,确保掉电时缓存数据不丢;
- 使用支持掉电保护(Power Loss Protection)的 SSD;
- 在软件层面做多副本复制,例如写多个 node 的本地磁盘。
所以如果你要做“断电也不丢”级别的持久化,只靠 Java 代码里的force()是不够的,必须从上到下排查整条硬件链路。
5.2 与 MappedByteBuffer.force() 的关系
有人问,用FileChannel.map()映射出来的MappedByteBuffer,调它的force()和FileChannel.force()有什么不同?
MappedByteBuffer.force()是用于强制将映射缓冲区内的所有更改写入存储设备,它和FileChannel.force()在底层路径上殊途同归,但使用场景有差异。如果你通过 MappedByteBuffer 修改了文件内容,想落盘,直接调mappedByteBuffer.force()即可;如果要同时更新文件大小等元数据,可能还需要channel.force(true)。而且要注意,MappedByteBuffer 以后如果还想 force,得先保证映射区域还合法,不要在 unmapped 之后再操作。
5.3 在多线程高并发下的 force 策略
高并发场景里,多个线程同时写同一个 FileChannel 是很常见的,比如多个业务线程向同一个 commit log 追加数据。此时如果每个线程写完都 force 一次,磁盘 IO 会瞬间被打爆,吞吐量惨不忍睹。
一个被我验证过的方案:使用“批量提交”模式。每个线程有自己的本地缓冲,把要写入的数据先攒在本地,由一个专门的刷盘线程定时(比如 10ms)或按大小阈值统一 write + force。也就是说,force 的频率由全局控制,而不是由每个业务线程控制。这样既能保证数据落在页缓存,又能把昂贵的 fsync 次数降到最低。代价是如果进程在两次 force 之间崩溃,会丢失最多一个刷盘周期的数据。这个“丢失窗口”能否接受,取决于你的业务。
5.4 从 write() 到写入策略:顺序写与随机写
FileChannel 本身不区分顺序写和随机写,但底层文件系统对这两种模式的表现差异巨大。顺序写通常能触发文件系统的 pre-allocation 和 grouping 优化,而随机写则容易造成磁盘寻道和碎片化。设计存储文件时,尽量让日志类文件只做 append 追加写,避免反复修改中间区域。
如果必须随机写,建议把文件预分配成固定大小(比如channel.setLength(1024 * 1024 * 1024)分配到 1GB),再在指定偏移写入数据,这样文件系统可以减少多次修改 size 元数据的开销。
5.5 崩溃一致性:为什么需要额外校验
即便你 write + force(true) 都做了,也不能保证文件在崩溃后一定处于你期望的“最新状态”。文件系统可能把多个块写入磁盘的顺序打乱,因此一个逻辑上完整的记录,可能在崩溃时只刷了一半。
经典的解决方案是“预写式日志 + 校验和”:
- 先写一条 WAL 记录,注明接下来要写入的数据长度与校验和;
- force;
- 写入真正的数据;
- force;
- 更新 WAL 状态为已完成;
- force。
恢复时,如果发现 WAL 里的校验和不匹配,就知道数据不完整,选择回滚或重放。这套思路在数据库、消息队列、对象存储里到处可见。
6. 避坑清单与个人经验总结
6.1 FileChannel.write() 与 force() 高频坑位
write()返回值可能是部分写入,必须循环写满;write()之前必须flip(),写完之后如果还要用 buffer 记得clear();force()的metaData参数不能想当然,文件大小变化时用true;- 多个线程共享一个 channel 时,不要用带 position 的方法,用
write(ByteBuffer, long); - 不要频繁创建 DirectByteBuffer,要复用;
- 不要对每条日志都 force,要批量;
- 不要以为
force()能把硬件写缓存也穿越,硬件层可靠性要单独考虑。
6.2 监控与可观测性建议
生产环境里,强烈建议给文件写入模块加上以下指标:
- 每次 write 的耗时分布(p99、p999);
- 每次 force 的耗时分布;
- 每秒钟 force 调用次数;
- 文件写入缓冲区的积压长度;
- 写入异常次数和类型。
这些指标能帮你第一时间发现磁盘性能劣化或者文件系统异常。以我自己的经验,force 的耗时 p99 突然从 5ms 变成 50ms,通常就是磁盘快出问题的前兆。
6.3 我的一点实战体会
写这篇文章的时候,我又想起去年处理过的一个线上问题:某个服务每天凌晨会批量写一批订单数据到本地文件,用的就是FileChannel.write()+force(false)。某天早上发现其中几个文件的大小变成了 0。查了很久,最后发现是因为这些文件写在一台云主机的临时盘上,而云主机的临时盘在凌晨发生过一次热迁移,底层文件系统被重建,之前的元数据全部丢失。那次事故之后,我把所有重要文件的写入路径统一改成了“先写临时文件 + force(true) + 原子重命名”的流程,再也没有出现过文件内容消失的问题。
这个经验想说明的是:文件系统的可靠性远比你想象中脆弱,但write + force + 原子重命名这套组合拳,能帮你把很多底层不确定性隔离开。具体做法:
- 把数据写到同一个目录下的临时文件,例如
data.tmp; - 写完调用
channel.force(true); - 关闭 channel;
- 调用
Files.move(tmp, finalPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)。
这样即使应用中途崩溃,最多留下一个半截的 tmp 文件,不会破坏正式文件。正式文件在移动前的瞬间要么是旧版本,移动后就是新版本,整个提交过程对外部观察者来说是原子的。这套模式值得写进每一个需要持久化文件的模块里。
最后再分享一个小技巧:如果文件在写入过程中就不再修改,用FileChannel.open(path, StandardOpenOption.READ)打开它,然后调用channel.size()配合map()做只读映射,读取速度非常快;但要注意,映射只读文件时,如果有人同时写入,可能出现性能问题甚至异常。所以“写完即封存”的文件,最好在写入端完全关闭后再交给读取端。这个细节虽然不起眼,但在很多文件交换系统里能避免一堆怪问题。
FileChannel 这套 API 整体设计得足够优雅,但用得好不好,全靠对底层存储语义的理解。希望这篇文章能帮你在下一次写文件的时候,多想一想数据到底在哪一层,以及当断电来临时,你写的代码能不能扛得住。