【发布时间】: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 文件并重命名是跨平台的,并确保最终文件不存在或可以被其他进程处理。
所以我尝试实际实施这种方法。以下是我尝试的总结。对于实际问题 - 跳到底部。
编写方法
我尝试了各种写入和重命名文件的方式(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);
使用番石榴 (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 中的。因此,该解决方案应该适用于上面列出的所有读取方法。
【问题讨论】:
-
对于
write和read(Guava 和 FileInputStream)的哪种方式失败和成功(NIO)? -
当
read使用Guava 或FileInputStream实现时,无论使用write方法,它都会随机失败。当read用NIO实现时,不管write使用什么方法,它总是成功的。 (实际上我认为不同的write方法在底层可能并没有什么不同,因为这一切都归结为文件重命名。或者——Windows API 中可以有不同的重命名方法?) -
你的意思是独立于你使用哪种重命名方式,NIO读取总是成功而Guave/FileInputStream时不时失败?
-
没错。但我不能只说“让我们用 NIO 阅读”,因为我只控制 writer。
-
将这两种方法都放在
procmon下可能会很有趣,以查看它们各自执行的系统调用以及它们花费了多长时间。这会让您了解它们在功能上是否不同,是否更快,或者是什么。
标签: java windows file-io atomic