1. 概述
在进行文件读写操作时,为了保证并发环境下数据的一致性,必须引入合理的文件锁机制。尤其在多进程或跨 JVM 的场景下,缺乏锁控制极易导致数据损坏或读取到脏数据。
本文将聚焦 Java NIO 提供的文件锁能力,通过实战示例讲解如何正确使用 FileChannel 和 FileLock 实现独占锁与共享锁,帮你避开常见的“踩坑”点。
✅ 核心依赖:
java.nio.channels.FileChannel
与FileLock
⚠️ 注意:本文讨论的是操作系统级文件锁,而非 JVM 内部的线程同步
2. 文件锁的类型
Java 支持两种标准的文件锁模式:
- ✅ 独占锁(Exclusive Lock):又称写锁(write lock),一旦加锁,其他任何进程都无法读写该文件区域
- ✅ 共享锁(Shared Lock):又称读锁(read lock),允许多个进程同时读取,但会阻止写操作
简单粗暴理解:
- 写文件?上独占锁
- 读文件?上共享锁
操作系统层面的实现差异较大:
- ✅ Unix/POSIX 系统:锁是“建议性”的(advisory),依赖所有进程自觉遵守协议
- ✅ Windows 系统:锁是“强制性”的(mandatory),OS 会主动拦截违规访问
⚠️ 因此跨平台项目要特别小心,锁的行为可能不一致。
3. Java 中的文件锁机制
Java 通过 java.nio.channels.FileChannel
提供了对底层操作系统文件锁的支持。核心方法有两个:
lock()
:阻塞式获取锁,成功返回FileLock
,失败抛异常tryLock()
:非阻塞尝试获取锁,成功返回FileLock
,失败返回null
获取 FileChannel
的方式有三种:
// 方式1:通过 FileOutputStream
FileOutputStream fos = new FileOutputStream("data.txt");
FileChannel channel = fos.getChannel();
// 方式2:通过 FileInputStream
FileInputStream fis = new FileInputStream("data.txt");
FileChannel channel = fis.getChannel();
// 方式3:通过 RandomAccessFile
RandomAccessFile raf = new RandomAccessFile("data.txt", "rw");
FileChannel channel = raf.getChannel();
// 推荐方式:直接 open(推荐)
try (FileChannel channel = FileChannel.open(Paths.get("data.txt"), StandardOpenOption.WRITE)) {
// 使用 channel
}
🔗 更多 FileChannel 用法参考:Java FileChannel 指南
4. 独占锁(写锁)
当你要修改文件内容时,必须使用独占锁,防止其他进程同时读写造成数据错乱。
获取独占锁的前提是:FileChannel
必须是可写的。
4.1 使用 FileOutputStream 获取独占锁
try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/testfile.txt");
FileChannel channel = fileOutputStream.getChannel();
FileLock lock = channel.lock()) {
// ✅ 此处可安全写入
channel.write(ByteBuffer.wrap("Hello".getBytes()));
} catch (IOException e) {
// 处理异常
}
📌 channel.lock()
会阻塞直到获取锁成功,或抛出异常:
- ❌
OverlappingFileLockException
:目标区域已被其他进程锁定 - ❌
IOException
:I/O 错误
如果想非阻塞尝试,使用 tryLock()
:
FileLock lock = channel.tryLock();
if (lock != null) {
// 成功获取锁
} else {
// 被占用,直接放弃
}
4.2 使用 RandomAccessFile 获取独占锁
try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "rw");
FileChannel channel = file.getChannel();
FileLock lock = channel.lock()) {
// ✅ 可读可写
channel.write(ByteBuffer.wrap("World".getBytes()));
}
📌 构造函数第二个参数 "rw"
表示读写模式,"r"
仅为只读。
4.3 独占锁必须基于可写 Channel
下面这段代码会直接翻车:
Path path = Files.createTempFile("foo", ".txt");
try (FileInputStream fis = new FileInputStream(path.toFile());
FileLock lock = fis.getChannel().lock()) {
// ❌ 不可达代码
} catch (NonWritableChannelException e) {
// 🚨 抛出 NonWritableChannelException
}
原因很简单:FileInputStream
返回的是只读 Channel,无法申请写锁(即使是独占锁也需要写权限)。这是新手常踩的坑。
5. 共享锁(读锁)
当你只是读取文件,不希望别人修改时,使用共享锁即可。它允许多个进程并发读,但会阻止写操作。
获取共享锁的前提是:FileChannel
必须是可读的。
5.1 使用 FileInputStream 获取共享锁
try (FileInputStream fileInputStream = new FileInputStream("/tmp/testfile.txt");
FileChannel channel = fileInputStream.getChannel();
FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
// ✅ 安全读取
ByteBuffer buf = ByteBuffer.allocate(1024);
channel.read(buf);
}
📌 关键参数说明:
0
:锁定起始位置Long.MAX_VALUE
:锁定长度(这里表示整个文件)true
:表示是共享锁(shared)
5.2 使用 RandomAccessFile 获取共享锁
try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "r");
FileChannel channel = file.getChannel();
FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
// ✅ 只读模式下也能加共享锁
ByteBuffer buf = ByteBuffer.allocate(1024);
channel.read(buf);
}
📌 "r"
模式即只读,适合纯读场景。
5.3 共享锁必须基于可读 Channel
反例:试图在 FileOutputStream
的 Channel 上加共享锁:
Path path = Files.createTempFile("foo", ".txt");
try (FileOutputStream fos = new FileOutputStream(path.toFile());
FileLock lock = fos.getChannel().lock(0, Long.MAX_VALUE, true)) {
// ❌ 不可达
} catch (NonWritableChannelException e) {
// 🚨 同样抛出 NonWritableChannelException
}
虽然叫“共享锁”,但它依然要求 Channel 可读。而 FileOutputStream
创建的是只写 Channel,不具备读能力,因此无法加共享锁。
6. 实际使用注意事项
文件锁看似简单,但在生产环境使用时有几个关键点必须注意:
- ✅ 锁是进程级的:同一个 JVM 内多个线程共享同一个锁,跨 JVM 才体现并发控制
- ✅ 锁的释放依赖 GC 或 close:
FileLock
被 GC 前不会自动释放,务必配合 try-with-resources 使用 - ✅ Windows 与 Linux 行为不一致:
- Linux:建议性锁,靠自觉
- Windows:强制锁,OS 拦截
- ✅ 慎用 lock() 阻塞调用:可能无限等待,建议结合超时机制或使用
tryLock()
- ✅ 锁区域可以指定:不一定锁整个文件,可按字节范围加锁,实现更细粒度控制
📌 建议:高并发场景下,优先考虑数据库或分布式锁(如 Redis),文件锁更适合单机、低频、简单协调场景。
7. 总结
本文系统讲解了 Java 中通过 NIO 实现文件锁的完整方案:
- 独占锁用于写操作,需可写 Channel
- 共享锁用于读操作,需可读 Channel
FileOutputStream
→ 只写 Channel → 只能加独占锁FileInputStream
→ 只读 Channel → 只能加共享锁RandomAccessFile("rw")
→ 可读可写 → 两种锁都支持
⚠️ 最后提醒:文件锁不是银弹,跨平台兼容性和“建议性”本质决定了它只适合特定场景。使用前务必明确需求,避免误用。
💡 示例代码已托管至 GitHub:https://github.com/baomidou/tutorials/tree/master/core-java-modules/core-java-nio-2