我正在尝试在Windows上使用Java正确地实现“写临时文件并重命名”的操作。
如何在Java中原子地重命名文件,即使目标文件已经存在?建议重命名文件是“原子操作”(无论“原子”实际上意味着什么)。 https://stackoverflow.com/a/20570968/65458建议编写tmp文件并重命名是跨平台的,并确保最终文件不存在或可以被其他进程处理。
因此,我尝试实际实现这种方法。以下是我的尝试摘要。有关实际问题,请跳到底部。
写入方法
我尝试了各种写入和重命名文件的方式(content
和charset
分别为String
和Charset
):
使用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);
使用Guava(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实现创建后重命名,以便最终文件出现具有原子性,即要么不存在,要么可以被读取?
由于我对将拾取文件的进程具有控制权,因此不能假设使用任何特定的读取方法,甚至不能假设它们是用Java编写的。因此,解决方案应适用于上述所有读取方法。
java.io.FileInputStream
和其他java.io
类实现的,因此它可能具有相同的行为。 - Andrew JankeFileInput/OutputStream
,但我可能会错过一些细节...顺便问一下,为什么使用NIO进行读取时行为不同?它是否使用不同的Windows API函数、不同的参数或其他什么东西? - Piotr Findeisen