files.copy() 配合 replace_existing 不是原子操作,因其覆盖写入过程可能被并发读取到不一致数据;真正的原子替换需先写临时文件再用 files.move() 原子重命名。

Files.copy() 配合 StandardCopyOption.REPLACE_EXISTING 本身不是原子操作,直接用它替换目标文件无法保证原子性——因为它是“覆盖写入”,可能在中途被其他进程读到半新半旧的内容。
为什么 REPLACE_EXISTING 不是原子的
该选项只是让 copy 操作允许覆盖已存在的目标文件,但底层仍是一边读源文件、一边向目标路径写入字节流。若目标文件原本较大,写入过程持续数毫秒到数秒,期间其他程序 read 它,就可能读到截断、损坏或不一致的数据。
真正的原子替换:先写临时文件,再原子重命名
POSIX 和 Windows(NTFS)均保证 rename(Java 中为 Files.move())对同一文件系统内的文件是原子的:要么完全成功,要么完全失败,中间状态对外不可见。因此标准做法是:
- 将新内容写入一个与目标同目录、随机后缀(如
.tmp或带 UUID)的临时文件 - 调用
Files.move(tempPath, targetPath, StandardCopyOption.REPLACE_EXISTING) - 确保临时文件和目标文件在同一文件系统(否则 move 可能退化为复制+删除,失去原子性)
正确代码示例
注意:必须捕获异常并清理临时文件
Path target = Paths.get("config.json");
Path temp = Files.createTempFile(target.getParent(), "config-", ".tmp");
try {
Files.copy(sourceInputStream, temp, StandardCopyOption.REPLACE_EXISTING);
// 原子替换:move 是原子的(前提是同文件系统)
Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
try {
Files.deleteIfExists(temp); // 清理失败的临时文件
} catch (IOException ignore) {}
throw e;
}
额外健壮性建议
- 写临时文件前,可先用
Files.getFileStore(target)检查temp.getParent()是否与target.getParent()属于同一FileStore,避免跨设备 move 失败 - 临时文件名尽量唯一(如含时间戳 + 随机数),防止并发写入冲突
- 如果目标文件被其他进程长期打开(如 Windows 上被记事本占用),
move可能失败,需合理处理AccessDeniedException











