Files.newOutputStream不提供原子性写入,必须手动组合临时文件创建与Files.move(…ATOMIC_MOVE)实现;需确保同文件系统、处理异常降级,并避免依赖deleteOnExit。

Files.newOutputStream 本身不提供原子性写入
直接用 Files.newOutputStream 打开文件并写入,**不是原子操作**。它只是打开一个流,后续写入会直接覆盖或追加到目标路径,如果中途失败(如进程崩溃、断电),文件很可能处于中间状态——既不是旧内容,也不是完整新内容。
所谓“原子性写入”,本质是:新内容先写到临时位置,写完再用一次系统级原子操作(如 rename)把临时文件“闪现”成目标文件。Java 没有内置封装这整套流程的 API,Files.newOutputStream 只负责第一步。
实现原子写入必须手动组合临时文件 + rename
核心思路是:用 Files.createTempFile 创建带随机后缀的临时文件(确保不冲突),写入完毕后调用 Files.move 并指定 StandardCopyOption.REPLACE_EXISTING 和 StandardCopyOption.ATOMIC_MOVE(后者在支持的文件系统上才真正原子)。
-
StandardCopyOption.ATOMIC_MOVE在 Linux(ext4/xfs)、macOS(APFS)上通常生效;Windows NTFS 上部分场景可能退化为非原子复制+删除 - 临时文件必须与目标文件在同一个文件系统(同磁盘、同挂载点),否则
ATOMIC_MOVE必然失败并抛出AtomicMoveNotSupportedException - 写入临时文件时建议加上
StandardOpenOption.SYNC或StandardOpenOption.DSYNC,降低缓存导致写入不完整的风险(但会牺牲性能)
示例关键片段:
Path target = Paths.get("data.json");
Path temp = Files.createTempFile("data-", ".json.tmp");
try (OutputStream os = Files.newOutputStream(temp, StandardOpenOption.WRITE, StandardOpenOption.SYNC)) {
os.write("{\"value\":42}".getBytes(StandardCharsets.UTF_8));
}
Files.move(temp, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
注意 deleteOnExit 不适用于原子写入场景
别在临时文件上用 temp.toFile().deleteOnExit() —— 这个方法只在 JVM 正常退出时触发,对崩溃、kill -9、断电完全无效。残留临时文件会越积越多。
更稳妥的做法是:
- 写入前清理上次可能遗留的临时文件(比如按固定命名规则查找
*.tmp并删除) - 或把临时文件放在与目标同目录的隐藏子目录里(如
.atomic-writes/),定期扫描清理 - 生产环境建议搭配守护线程或启动时扫描,避免依赖单次写入逻辑兜底
第三方库(如 Google Guava)也没绕过这个底层约束
像 Files.asByteSink 或 MoreFiles.asCharSink 这类封装,底层仍是调用 Files.newOutputStream + Files.move,它们只是帮你省了模板代码,并未改变原子性依赖文件系统和手动两步走的事实。
真正容易被忽略的是:很多人以为加了 ATOMIC_MOVE 就万事大吉,但没验证是否真的生效。建议在关键路径上捕获 AtomicMoveNotSupportedException,降级为普通 move 并记录告警——因为一旦跨文件系统,原子性就彻底失效,只能靠外部手段(如备份旧文件)来补救。











