
本文介绍如何使用 Java NIO 的 FileChannel 在单次文件锁保护下完成文件读取与写入,避免因流关闭导致通道失效的问题,并提供可直接运行的 Kotlin 示例代码。
本文介绍如何使用 java nio 的 `filechannel` 在单次文件锁保护下完成文件读取与写入,避免因流关闭导致通道失效的问题,并提供可直接运行的 kotlin 示例代码。
在多线程或多进程环境中对同一文件进行读-改-写(read-modify-write)操作时,必须确保整个流程原子性,否则极易引发数据竞争或覆盖丢失。核心挑战在于:不能让 InputStream 或 OutputStream 的 use 作用域提前关闭底层 FileChannel——这正是原代码失败的根本原因:Channels.newInputStream(channel).use { ... } 会在块结束时调用 channel.close(),导致后续写入抛出 ClosedChannelException。
正确做法是显式管理锁与通道生命周期,避免依赖流的自动关闭机制。FileChannel.lock() 提供的是操作系统级的 advisory lock(建议性锁),需配合 StandardOpenOption.READ | WRITE | CREATE 打开通道,并手动控制锁的获取与释放时机。注意:lock() 默认阻塞直至获取成功,如需非阻塞可传入 false 参数。
以下为推荐实现(Kotlin):
import java.io.*
import java.nio.channels.*
import java.nio.file.*
fun <t> withFileLock(
path: Path,
options: Set<standardopenoption> = setOf(StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE),
block: (FileChannel) -> T
): T {
val channel = FileChannel.open(path, *options.toTypedArray())
return try {
val lock = channel.lock() // 获取排他锁(阻塞)
try {
block(channel)
} finally {
lock.release() // 必须释放锁
}
} finally {
channel.close() // 最终关闭通道
}
}
// 使用示例:读取 JSON 数据、修改后写回
fun updateJsonFile(path: Path, transform: (String) -> String) {
withFileLock(path) { channel ->
// 安全读取:不关闭 channel
val bytes = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()).asByteBuffer()
val content = String(bytes.array(), Charsets.UTF_8)
val modified = transform(content)
// 安全写入:先清空,再写入新内容
channel.truncate(0)
channel.position(0)
channel.write(ByteBuffer.wrap(modified.toByteArray(Charsets.UTF_8)))
}
}</standardopenoption></t>
⚠️ 关键注意事项:
-
不要在
use块中包装Channels.newInputStream()/newOutputStream(),因其close()会级联关闭FileChannel; -
FileChannel.lock()是 advisory lock,仅当所有访问方都主动检查锁才生效,无法阻止未加锁的进程写入; - 若文件可能被外部程序修改,建议结合
Files.getLastModifiedTime()进行乐观并发控制; - 对于大文件,避免
map()全量加载;可改用channel.read()分块读取 +channel.write()分块写入,保持内存友好; - Windows 下锁粒度为整个文件,Linux/macOS 支持字节范围锁,但本场景使用全文件锁已足够。
综上,RandomAccessFile 并非唯一解——FileChannel 完全胜任读写同锁需求,关键在于规避流封装带来的隐式关闭陷阱,并严格遵循“开通道 → 加锁 → 操作 → 释锁 → 关通道”的顺序。










