1. 概述

在进行文件读写操作时,为了保证并发环境下数据的一致性,必须引入合理的文件锁机制。尤其在多进程或跨 JVM 的场景下,缺乏锁控制极易导致数据损坏或读取到脏数据。

本文将聚焦 Java NIO 提供的文件锁能力,通过实战示例讲解如何正确使用 FileChannelFileLock 实现独占锁与共享锁,帮你避开常见的“踩坑”点。

✅ 核心依赖:java.nio.channels.FileChannelFileLock
⚠️ 注意:本文讨论的是操作系统级文件锁,而非 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 或 closeFileLock 被 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


原始标题:How to Lock a File in Java