多线程安全写入同一文件的关键是用FileChannel.lock()实施跨进程排他锁,全程复用同一通道、显式关闭,配合读写锁优化读多写少场景,并避免流自动关闭导致通道中断。

多线程下安全写入同一个文件,关键不是“避免并发”,而是“控制并发”——让读-改-写全过程在一次文件锁保护内完成,且不因流关闭意外中断通道。Java NIO 的 FileChannel.lock() 是跨进程有效的系统级排他锁,比 JVM 层面的 synchronized 或 ReentrantLock 更适合文件场景。
用 FileChannel + advisory lock 保证原子性
操作系统提供的 advisory lock(建议性锁)虽不强制拦截未配合锁逻辑的进程,但在所有线程/进程都遵守同一套锁协议的前提下,能可靠实现互斥。重点在于:锁必须在 同一个 FileChannel 实例上获取和释放,且不能让 InputStream/OutputStream.use{} 自动关闭通道。
- 用
FileChannel.open(path, READ, WRITE, CREATE)打开通道,不要用new FileInputStream().getChannel() - 调用
channel.lock()获取排他锁(阻塞等待),之后再读取、修改、写入,全程复用该 channel - 锁释放后,再显式调用
channel.close();不要依赖流封装的自动关闭 - 注意:
lock()默认锁定整个文件;如需区间锁,传入起始偏移、长度和是否共享参数
读写分离时考虑 ReadWriteLock + 文件锁组合
若业务存在明显“读多写少”特征(比如配置文件被频繁读、偶发更新),可结合 JVM 层读写锁与文件锁提升并发度:
- 读操作先尝试获取
ReentrantReadWriteLock.readLock(),成功后再用FileChannel.open(READ)读取——此时无需文件锁,因只读不改 - 写操作必须先获取
writeLock(),再打开READ + WRITE + CREATE通道并调用lock(),确保写入期间无其他进程干扰 - 这种组合既利用了 JVM 锁的轻量快速,又借助文件锁守住跨进程边界
避免常见陷阱
很多失败案例源于对资源生命周期的误判:
立即学习“Java免费学习笔记(深入)”;
-
别在 try-with-resources 里包装通道关联的流:例如
Channels.newOutputStream(channel).use { ... }会关 channel,后续写失败 -
锁不是万能的,得配合理想的文件格式:追加日志可用
StandardOpenOption.APPEND+ 无锁;但修改中间内容必须全文件锁+重写或随机写 - 不要依赖 lock() 的强制性:它是 advisory 的,Linux/macOS 下需所有访问方主动检查锁;Windows 上行为略有差异,建议统一用 NIO 方式访问
-
异常时务必释放锁:必须在 finally 块中调用
lock.release(),否则其他线程永久阻塞
简单可靠的封装建议
把锁获取、通道管理、异常清理打包成工具函数,降低出错概率:
- Kotlin 示例中
withFileLock { channel -> ... }就是典型做法:自动开通道、加锁、保通道可用、终态释放锁与关闭通道 - Java 可用 try-finally 模板,或封装为
AutoCloseable类型,在 close() 中释放锁并关通道 - 若需超时获取锁,调用
channel.tryLock(timeout, unit),返回 null 表示失败,避免无限等待


















