开始前,会这些就够了
- 读过 Java 字节流与 try-with-resources,知道文件通道需要关闭。
- 能区分线程、JVM 进程与文件路径;不要求掌握操作系统锁实现。
- 使用完整 JDK 17 或更新版本,允许程序创建自己的临时目录和子进程。
- 需要补基础时:先补字节流和资源关闭
遇到不熟悉的词,先看这 2 个解释
- 文件锁范围
- 文件锁针对从某个字节位置开始、长度固定的一段区域。判断冲突时要比较区域是否重叠。
- JVM 级文件锁
- Java 文件锁代表整个 JVM 持有;同一 JVM 再用另一个通道申请重叠区域,仍然属于同一持有方范围。
两个后台任务都要更新同一份文件。你想到在写入前拿文件锁,再在完成后释放,以为这样就像 synchronized 一样让两个任务排队。结果第二次获取没有等到第一把锁释放,而是抛出了 OverlappingFileLockException。
这个实验先固定文件和字节范围,再只改变“请求来自哪里”:同一通道、同一 JVM 的另一通道,以及另一个 JVM。三个位置调用的都是文件锁 API,却不能把失败统一解释为返回 null。
把例子带到自己的电脑上
下载完整练习包
需要 JDK 17 或更新版本,使用 Java 标准库;制作时实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- FileLockScopeDemo.java:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 FileLockScopeDemo.java
java -cp . FileLockScopeDemo解压到独立练习文件夹,在含 FileLockScopeDemo.java 的目录打开终端运行。Windows本地自建16字节临时文件;同通道/同JVM另一通道重叠异常、另一JVM竞争null、释放后获取;两个隐藏子进程均回收。JDK17/21各8项检查;未测试网络盘、共享锁或不配合写入。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
先说清锁的是哪个文件、哪一段字节
FileLock 表示文件中的一个范围。程序新建一个十六字节的临时文件,对从位置零开始、长度十六的区域请求独占锁。这等价于本例中的 [0,16);文件大小、锁起点与锁长度分别是不同信息。
不要从文件名相同就开始猜测时序。这里两个通道都由同一个 Path 打开,子进程也使用父进程创建的这个文件,因此比较的是同一资源和同一重叠范围。实验没有链接路径、网络盘或不同文件提供者。
锁的范围在获取时确定。文件以后变长,并不自动让已有有限范围覆盖新增部分。本例刻意保持固定大小,先把所有权和结果看清;范围变化留给后面的独立练习。
同一 JVM 里,重叠请求不是第二把线程锁
父进程取得第一把锁后,先从同一通道调用 tryLock,再从另一个已打开的通道调用 tryLock。两次都捕获 OverlappingFileLockException,原有锁仍然有效。换一个 FileChannel 对象没有把请求变成另一个 JVM。
官方 API 把文件锁的持有范围定义在整个 JVM 层面,并明确说明它不适合用于同一 JVM 内多个线程的文件访问控制。因此这里的异常不是“还没等够久”,也不是应该被反复吞掉的普通空结果。
同一进程内部若需要让多个任务依次进入更新步骤,应另外使用明确的线程协调方式,并让相关代码共同遵守。文件锁处理跨进程协作的部分,线程锁处理本进程中的进入次序,设计时要分别说明谁参与、谁释放。
另一 JVM 竞争时,tryLock 的 null 才有这层含义
在第一把锁仍被持有时,父进程启动第一个子 JVM。子进程只尝试同一范围一次,结果为 null,然后立即退出。父进程保持持锁直到它退出,所以没有靠 sleep 猜测哪一方先到。
随后父进程释放原有锁,再启动第二个子 JVM。它这次成功得到 FileLock,并在退出前关闭它。两次子进程的代码相同,变化只有原来那把锁是否已经释放。
tryLock 不等待可用位置;重叠锁由另一程序持有时可以返回 null,其他失败仍可能抛异常。不能把 catch 所有异常后返回 null 当成等价实现,否则关闭通道、权限或文件系统错误也会被误报为普通竞争。
通道关闭和锁释放也属于实验顺序
源码把两个父进程通道都保留到持锁阶段结束。这样做有实际原因:部分系统关闭同一文件的某个通道可能影响这个 JVM 在该文件上的锁。若刚测完第二通道就把它关闭,再去观察子进程,容易把生命周期变化混进结果。
子进程输出重定向到这个实验自己的临时文件,避免读取管道成为新的等待因素。Windows 使用 javaw.exe 隐藏窗口,父进程给每个子进程五秒完成期限;异常路径也会结束并等待自己创建的子进程。这个期限是保护措施,不是性能结论。
锁与通道由 try-with-resources 关闭,全部步骤结束后才删除新建临时目录。源码不遍历用户工作目录,也不要求你指定要锁的真实业务文件。运行时使用无参数入口即可。
保存单文件,实跑并核对每项观察
把代码保存为 FileLockScopeDemo.java,在这个文件所在目录执行下方命令。需要完整 JDK 17 或更新版本;只有 java 而没有 javac 的环境还不能完成这里的编译步骤。无需 Maven、外部依赖或 IDE 插件。
2026 年 10 月 17 日已使用 Microsoft OpenJDK 17.0.20.1 和 21.0.12,各自在新建临时目录中以 --release 17 编译并执行。两版输出相同,每次完成 8 项检查。下方保留实际输出,失败时不会打印 PASS。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/** Only locks a newly-created temporary file; never accepts a user file. */
public class FileLockScopeDemo {
private static int checks;
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
checks++;
}
private static boolean overlaps(FileChannel channel) throws IOException {
try (FileLock unexpected = channel.tryLock(0, 16, false)) {
return false;
} catch (OverlappingFileLockException expected) {
return true;
}
}
private static String child(Path directory, Path file, String label) throws Exception {
boolean windows = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win");
// javaw is the no-console Windows launcher. Output goes to our temporary file.
Path java = Path.of(System.getProperty("java.home"), "bin", windows ? "javaw.exe" : "java");
Path output = directory.resolve(label + ".out");
Process process = new ProcessBuilder(java.toString(), "-cp", System.getProperty("java.class.path"),
FileLockScopeDemo.class.getName(), "--child", file.toString())
.redirectErrorStream(true).redirectOutput(output.toFile()).start();
try {
process.getOutputStream().close();
if (!process.waitFor(5, TimeUnit.SECONDS)) throw new IOException("child deadline");
String result = Files.readString(output).strip();
check(process.exitValue() == 0, "child failed: " + result);
return result;
} finally {
if (process.isAlive()) {
process.destroyForcibly();
if (!process.waitFor(5, TimeUnit.SECONDS)) throw new IOException("child not reaped");
}
process.getInputStream().close();
process.getErrorStream().close();
process.getOutputStream().close();
}
}
private static void removeOwnedDirectory(Path directory) throws IOException {
try (var paths = Files.walk(directory)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(path);
}
}
public static void main(String[] args) throws Exception {
if (args.length == 2 && args[0].equals("--child")) {
// This branch is invoked only by the parent with its own temporary file.
try (FileChannel channel = FileChannel.open(Path.of(args[1]), StandardOpenOption.WRITE);
FileLock lock = channel.tryLock(0, 16, false)) {
System.out.println(lock == null ? "other_jvm=null" : "other_jvm=acquired");
}
return;
}
if (args.length != 0) throw new IllegalArgumentException("run without arguments");
Path directory = Files.createTempDirectory("qinghuan-filelock-");
try {
Path file = directory.resolve("owned.bin");
Files.write(file, new byte[16]);
// Keep BOTH channels open until after the held lock is released.
try (FileChannel first = FileChannel.open(file, StandardOpenOption.WRITE);
FileChannel second = FileChannel.open(file, StandardOpenOption.WRITE)) {
try (FileLock held = first.lock(0, 16, false)) {
check(overlaps(first), "same channel must reject overlap");
check(overlaps(second), "another channel in same JVM must reject overlap");
check(held.isValid(), "original lock still held");
check(child(directory, file, "held").equals("other_jvm=null"), "child must see contention");
System.out.println("same_channel=OverlappingFileLockException; same_jvm_other_channel=OverlappingFileLockException");
System.out.println("while_parent_holds: other_jvm=null");
}
check(child(directory, file, "released").equals("other_jvm=acquired"), "released lock must be available");
System.out.println("after_parent_release: other_jvm=acquired");
}
} finally {
removeOwnedDirectory(directory);
}
check(!Files.exists(directory), "temporary directory removed");
System.out.println("PASS checks=" + checks + "; children_reaped=true; temporary_files_cleaned=true");
}
}
怎样运行:在源码所在目录,先单独执行 javac -encoding UTF-8 --release 17 FileLockScopeDemo.java;编译成功后,再执行 java -cp . FileLockScopeDemo。两条命令分别回车。2026-10-17 已在 Windows、JDK 17.0.20.1 / 21.0.12 编译运行此份源码并通过检查。Windows本地自建16字节临时文件;同通道/同JVM另一通道重叠异常、另一JVM竞争null、释放后获取;两个隐藏子进程均回收。JDK17/21各8项检查;未测试网络盘、共享锁或不配合写入。
运行后对照结果
same_channel=OverlappingFileLockException; same_jvm_other_channel=OverlappingFileLockException
while_parent_holds: other_jvm=null
after_parent_release: other_jvm=acquired
PASS checks=8; children_reaped=true; temporary_files_cleaned=true拿到文件锁,不等于所有程序都会自动守规矩
文件锁映射到底层系统能力,但是否强制阻止不配合的读写,存在平台差异。跨平台设计应要求参与者遵守同一锁定协议,不能只看到本机两个配合的 JVM 顺利交接,就宣布任意程序都无法绕开文件更新规则。
本次只验证 Windows 本地临时文件、独占重叠范围与两个自建 JVM 的交接。没有运行网络文件系统、共享锁、映射文件或强行绕过锁的写入,也没有把它包装为数据库事务或分布式锁。
排错时至少记录文件身份、请求范围、是否同一 JVM、获取方式、实际返回或异常,以及通道关闭顺序。这些事实比只留一句“加锁失败”更能解释程序到底走到了哪一步。
记住这三点
- 同一 JVM 的重叠文件锁请求会抛
OverlappingFileLockException,换通道不改变这个范围。 - 另一程序持有重叠锁时,
tryLock可以返回null;释放后才有机会成功。 - 线程协调、文件锁范围和资源关闭顺序要分别设计。
别把例子用过头
- 把文件锁当作同 JVM 内线程排队的直接替代品。
- 把所有异常转成
null,丢失关闭通道或其他 I/O失败。 - 在对照中提前关闭相关通道,改变锁的生命周期。
- 从两个配合 JVM 的成功推断任意程序、网络盘都具备相同行为。
自己试一小步
如果同一文件里有互不重叠的两个区域,哪些获取会冲突?
- 复制项目,将自建文件扩大为三十二字节,明确列出两段区间。
- 分别为同一 JVM 和另一 JVM 设计重叠、相邻但不重叠的请求,先写下预测。
- 实际获取后记录锁的 position、size、是否得到对象、是否为
null以及异常类型。 - 让全部参与者释放资源,提交区间表、源码、JDK 版本与实际输出。
做到这些,就算完成
- 范围采用明确的起点和长度,没有把末端包含关系写反。
- 同 JVM 与另一个 JVM 的结果分列,失败与异常未混写。
- 只使用项目新建的临时文件,子进程有完成期限和回收。
- 不能把一次本机结果写成所有文件系统的行为保证。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java 17:FileLock 的 JVM 范围与平台差异 (新标签页)↗
核对 JVM 级持有、线程控制限制、协作式锁和关闭相关通道的注意点。
- Java 17:FileChannel.lock 与 tryLock (新标签页)↗
核对
tryLock的null、非阻塞、重叠范围与异常条件。 - Java 17:OverlappingFileLockException (新标签页)↗
确认同 JVM 重叠持有或等待对应的异常。