【问题标题】:How to create-then-atomically-rename file in Java on Windows?如何在 Windows 上的 Java 中创建然后原子重命名文件?
【发布时间】:2015-07-07 12:22:29
【问题描述】:

我正在尝试在 Windows 上正确使用 Java 实现“写入临时文件并重命名”

How to atomically rename a file in Java, even if the dest file already exists? 建议重命名文件是“原子操作”(无论“原子”实际上是什么意思)。 https://stackoverflow.com/a/20570968/65458 建议编写 tmp 文件并重命名是跨平台的,并确保最终文件不存在或可以被其他进程处理。

所以我尝试实际实施这种方法。以下是我尝试的总结。对于实际问题 - 跳到底部。

编写方法

我尝试了各种写入和重命名文件的方式(contentcharset 分别是 StringCharset):

使用java.nio.file.Files

Files.copy(new ByteArrayInputStream(content.getBytes(charset)), tmpFile);
Files.move(tmpFile, finalFile, StandardCopyOption.ATOMIC_MOVE);

使用 Guava (14) 和 java.io.File:

com.google.common.io.Files.write(content, tmpFile, charset);
tmpFile.renameTo(finalFile);

或者更晦涩的方法:

try (OutputStream os = new FileOutputStream(tmpFile);
        Writer writer = new OutputStreamWriter(os, charset)) {
    writer.write(content);
}
Runtime.getRuntime().exec(
        new String[] { "cmd.exe", "/C", "move " + tmpFile + " " + finalFile }).waitFor();

读取方法

现在假设另一个线程(线程因为我在测试中,在现实生活中它可能是另一个进程)正在执行以下代码版本之一:

具有常用功能:

void waitUntilExists() throws InterruptedException {
    while (!java.nio.file.Files.exists(finalFile)) {
        NANOSECONDS.sleep(1);
    }
}

使用java.nio.file.Files

waitUntilExists();
return new String(Files.readAllBytes(finalFile), charset);

使用番石榴 (14):

waitUntilExists();
return new String(com.google.common.io.Files.toByteArray(finalFile.toFile()), charset);

或者更晦涩的方法:

waitUntilExists();
StringBuilder sb = new StringBuilder();
try (InputStream is = new FileInputStream(finalFile.toFile())) {
    byte[] buf = new byte[8192];
    int n;
    while ((n = is.read(buf)) > 0) {
        sb.append(new String(buf, 0, n, charset));
    }
}
return sb.toString();

结果

如果我使用“java.nio.file.Files 方法”阅读,一切正常。

如果我在 Linux 上运行这段代码(我知道,这超出了这个问题的范围),一切正常。

但是,如果我使用 Guava 或 FileInputStream 实现 read,则测试失败的可能性高于 0.5% (0.005)

java.io.FileNotFoundException: 进程无法访问该文件,因为它正被另一个进程使用

(消息由我自己翻译,因为我的 Windows 不是英文;提及“另一个进程”具有误导性,因为即使这是同一个进程,Windows 也会告诉这一点是正常的,我通过显式阻塞进行了验证。)

问题

如何在 Windows 上使用 Java 实现 create-then-rename,以便最终文件自动显示,即不存在或可以读取?

由于我确实可以控制进程而不是拾取文件,因此我不能假设正在使用任何特定的读取方法,甚至不能假设它们是在 Java 中的。因此,该解决方案应该适用于上面列出的所有读取方法。

【问题讨论】:

  • 对于writeread(Guava 和 FileInputStream)的哪种方式失败和成功(NIO)?
  • read 使用Guava 或FileInputStream 实现时,无论使用write 方法,它都会随机失败。当read用NIO实现时,不管write使用什么方法,它总是成功的。 (实际上我认为不同的write 方法在底层可能并没有什么不同,因为这一切都归结为文件重命名。或者——Windows API 中可以有不同的重命名方法?)
  • 你的意思是独立于你使用哪种重命名方式,NIO读取总是成功而Guave/FileInputStream时不时失败?
  • 没错。但我不能只说“让我们用 NIO 阅读”,因为我只控制 writer。
  • 将这两种方法都放在procmon 下可能会很有趣,以查看它们各自执行的系统调用以及它们花费了多长时间。这会让您了解它们在功能上是否不同,是否更快,或者是什么。

标签: java windows file-io atomic


【解决方案1】:

这似乎正是 Windows/NTFS 的行为方式。

此外,使用旧 IO 和 NIO 读取之间的行为差​​异可能是因为它们使用不同的 Windows API。

Wikipedia on File locking

对于在 Windows 中使用文件读/写 API 的应用程序, 字节范围锁由以下方式强制执行(也称为强制锁) 在 Windows 中执行的文件系统。对于应用程序 在 Windows 中使用文件映射 API,字节范围锁不是 强制执行(也称为咨询锁。)

虽然 Wikipedia 不是 Windows 的文档,但它仍然提供了一些启示。

(我提出这个答案只是为了让其他有同样想法的人不必写这个。真实的答案,参考文档或报告的错误,非常感谢。)

【讨论】:

  • 此处针对 Windows 的讨论:stackoverflow.com/questions/167414/…
  • 也许以下关于 TxF - Transactional NTFS 的链接可以证明您的假设是特定于 NTFS 的行为。
  • @AndrewJanke,我之前看过链接,但我不明白。这不是更多关于 rename-with-overwrite 的内容吗?
  • 如果你想看看JVM在做什么,你可以在Procmon下运行你的测试程序,它会告诉你它在做什么系统调用。但这只会告诉你这个特定的 JVM 实现在做什么,而不一定是 API 规范所保证的。 ReplaceFile 可能级别太高而无法显示,但您仍然可能会得到一些有趣的信息。
【解决方案2】:

JDK 中的 java.io.File.renameTo() 函数存在错误报告,该函数在 Windows 上不是原子的,该函数已以 Won't fix:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4017593 关闭。 所以可能没有干净的方法来解决你的问题。

【讨论】:

  • 当目标文件已经存在时,该错误涵盖了“非原子重命名”问题。我的问题是关于更简单情况下的“原子重命名”,即已知目标文件尚不存在。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-15
  • 2011-07-09
相关资源
最近更新 更多