如何在Windows上用Java创建然后自动重命名文件?

如何在Windows上用Java创建然后自动重命名文件?,java,windows,file-io,atomicity,Java,Windows,File Io,Atomicity,我正试图在Windows上正确地使用Java实现“编写临时文件并重命名” 建议重命名文件是“原子操作”(不管“原子”实际上是什么意思)。 建议编写tmp文件和重命名是跨平台的,并确保最终文件不存在或可由其他进程处理 所以我尝试实际实施这种方法。下面是我尝试的总结。对于实际问题,跳到底部 写方法 我尝试了各种写入和重命名文件的方法(content和charset分别是String和charset): 使用java.nio.file.Files: Files.copy(new ByteArrayIn

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

建议重命名文件是“原子操作”(不管“原子”实际上是什么意思)。 建议编写tmp文件和重命名是跨平台的,并确保最终文件不存在或可由其他进程处理

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

写方法 我尝试了各种写入和重命名文件的方法(
content
charset
分别是
String
charset
):

使用
java.nio.file.Files

Files.copy(new ByteArrayInputStream(content.getBytes(charset)), tmpFile);
Files.move(tmpFile, finalFile, StandardCopyOption.ATOMIC_MOVE);
waitUntilExists();
return new String(Files.readAllBytes(finalFile), charset);
使用番石榴(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();
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();
读取方法 现在假设另一个线程(线程,因为我在测试中,在现实生活中可能是另一个进程)正在执行以下版本的代码之一:

具有通用功能:

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

Files.copy(new ByteArrayInputStream(content.getBytes(charset)), tmpFile);
Files.move(tmpFile, finalFile, StandardCopyOption.ATOMIC_MOVE);
waitUntilExists();
return new String(Files.readAllBytes(finalFile), charset);
使用番石榴(14):

或者更模糊的方法:

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();
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
approach”阅读,一切都正常

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

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

java.io.FileNotFoundException:进程无法访问该文件,因为其他进程正在使用该文件

(我自己翻译的消息导致我的windows不是英语;提及“另一个进程”是误导性的,因为windows告诉我这一点是正常的,即使这是同一个进程,我用显式阻止进行了验证。)

问题: 如何在Windows上使用Java实现创建然后重命名,以便最终文件以原子方式显示,即不存在或可以读取


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

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

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

对于在Windows中使用文件读/写API的应用程序, 字节范围锁由 在Windows中执行的文件系统。适用于 在Windows中使用文件映射API时,字节范围锁不可用 强制(也称为建议锁。)

虽然Wikipedia不是Windows的文档,但这仍然有一些启示


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

JDK中的java.io.File.renameTo()函数存在错误报告,该函数在Windows上不是原子函数,已关闭,但不会修复:。
因此,可能没有干净的方法来解决您的问题。

对于
写入
的哪种方式,
读取
(Guava和FileInputStream)失败并成功(NIO)?当
读取
使用Guava或
FileInputStream
实现时,无论使用的
写入
方法如何,它都会随机失败。当用NIO实现
read
时,无论使用的
write
方法如何,它都会成功。(实际上,我认为不同的
write
方法在幕后可能并没有真正的不同,因为归根结底都是文件重命名。或者——在Windows API中可能有不同的重命名方法。)您的意思是独立于您使用的重命名方式,NIO读取总是成功的,而Guave/FileInputStream不时会失败?没错。但我不能只说“让我们用NIO阅读”,因为我只控制writer。将这两个方法都放在
procmon
下可能会很有趣,以查看它们各自在做什么系统调用,以及它们花费了多长时间。这会让你知道它们在功能上是不同的,只是速度更快,还是什么。这里的Windows特定讨论:也许下面的链接可以证明你的假设,即行为是NTFS特定的。也许这一个也是@AndrewJanke,我以前看到过链接,但我不明白。如果你想了解JVM在做什么,你可以在下面运行你的测试程序,它会告诉你它在做什么系统调用。但这只是告诉您这个特定的JVM实现在做什么,而不一定是API规范所保证的
ReplaceFile
的级别可能太高,无法自动显示,但您仍然可能会得到一些有趣的信息。该错误涵盖了目标文件已经存在时的“非原子重命名”问题。我的问题是在更简单的情况下,当已知目标文件不存在时,关于“原子重命名”的问题。