Files.move()跨文件系统操作天然非原子,因需复制+删除两阶段;应通过FileStore比对判断是否同文件系统,并采用复制校验→原子重命名→删除源的三步容错流程。

Files.move() 在跨文件系统操作时完全不能保证原子性,这是由操作系统底层限制决定的,Java 无法绕过。
为什么跨文件系统移动天然是非原子的
原子重命名依赖的是文件系统的 rename(2) 系统调用(Linux/macOS)或类似机制(Windows 同卷),它只在同一文件系统内有效。跨文件系统(例如从 /home 移到 /mnt/usb,或 Windows 的 C: → D:)时:
- 操作系统必须实际读取源文件内容、写入目标位置——这是耗时、可中断的 I/O 过程
- Files.move() 会自动退化为“复制 + 删除”两阶段操作
- 若在复制中途崩溃、断电或磁盘满,可能出现:源文件已被删除,而目标文件不完整或根本不存在
- ATOMIC_MOVE 选项在这种情况下被静默忽略,不会报错也不会警告
如何判断是否跨文件系统
不能靠路径字符串猜测,必须通过文件系统对象比对:
- 用
Files.getFileStore(path)获取源和目标父目录的FileStore - 比较两者是否相等:
Files.getFileStore(source).equals(Files.getFileStore(target.getParent())) - 更轻量方式(仅限同主机本地路径):
source.toRealPath().getRoot().equals(target.toRealPath().getRoot())
跨文件系统时的安全替代方案
放弃“一步原子”,改用可控的三步容错流程:
- 先用
Files.copy(source, tempTarget, REPLACE_EXISTING)复制到目标卷上的临时路径(如target.tmp) - 校验复制结果:至少检查
Files.size(tempTarget) == Files.size(source);高要求场景加哈希比对 - 校验通过后,再执行
Files.move(tempTarget, target, REPLACE_EXISTING)——这一步在同一卷内是原子的 - 最后才调
Files.delete(source);若失败,保留源文件,人工介入清理
REPLACE_EXISTING 和 ATOMIC_MOVE 不是默认行为
这两个选项都必须显式传入,否则行为与直觉相反:
- 不加
REPLACE_EXISTING,目标已存在时直接抛FileAlreadyExistsException - 不加
ATOMIC_MOVE,即使同文件系统,也不承诺原子性(只是大概率快且安全) - 同时使用两者,才能在同卷下实现“覆盖式原子移动”
本质上,原子性不是 Java API 赋予的能力,而是你主动确认环境、选择合适路径、并分层控制每一步的结果。

















