我正在运行一个长时间运行的操作,比如 10 万个作业。我想在每完成 100 个这样的作业后在文件中更新它的进度。
我正在使用 bufferedWriter 打开文件,附加模式为 false。写入它然后关闭它。每 100 个工作完成一次。所以文件打开和关闭会发生 1000 次。我可以通过只打开和关闭文件一次来进一步优化它吗?
public static void writeMetaData(String writeDir, JSONObject jsonObject) throws Exception {
String filePath = writeDir.concat("/").concat("metadata.txt");
BufferedWriter metaDataWriter = Files.newBufferedWriter(Paths.get(filePath), StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING);
metaDataWriter.write(jsonObject.toString());
IOUtils.closeQuietly(metaDataWriter);
}
for(int i =0 ; i < 100000; i++) {
// do Something;
if(i % 100 == 0) {
writeMetaData(writeDir, jsonObject);
}
}
文件应该只有一行。
100 个作业后的预期文件内容:
进度:100
200 个作业后的预期文件内容:
进度:200
这可以进一步优化吗?
最佳答案
首先,writeDir.concat("/").concat("metadata.txt")
之类的表达式会降低可读性和 性能。直接的 writeDir + "/"+ "metadata.txt"
将提供更好的性能。但是由于你只是为了构造一个 Path
而构造一个字符串,所以更直接的做法是不要在你的代码中完成 Path
的工作,而是使用 Paths.get(writeDir, "metadata.txt")
.
您不能倒回 BufferedWriter
,但可以倒回 FileChannel
。因此,要保持 channel 打开并在需要时倒带,您必须在倒带后构造一个新的编写器:
public static void writeMetaData(FileChannel ch, JSONObject jsonObj) throws IOException {
ch.position(0);
if(ch.size() > 0) ch.truncate(0);
Writer w = Channels.newWriter(ch, StandardCharsets.UTF_8.newEncoder(), 8192);
w.write(jsonObj.toString());
w.flush();
}
try(FileChannel ch = FileChannel.open(Paths.get(writeDir, "metadata.txt"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
for(int i = 0; i < 100000; i++) {
// do Something;
if(i % 100 == 0) {
writeMetaData(ch, jsonObject);
}
}
}
重要的是 Writer
的使用以 flush()
结束,以强制写入所有缓冲数据,而不是 close()
因为这也会关闭底层 channel 。请注意,此代码不会将编写器包装到 BufferedWriter
中;将文本编码为 UTF-8 已经是一种缓冲操作,通过为编码器请求更大的缓冲区,匹配 BufferedWriter
的默认缓冲区大小,我们可以获得相同的缓冲效果,而无需复制开销。
由于写作本身并不是目的,因此还有一个关于阅读方面的问题。如果读取器尝试在某些时间间隔读取数据,则存在与写入重叠的风险,从而获得不完整的数据。
你可以使用
public static void writeMetaData(FileChannel ch, JSONObject jsonObj) throws IOException {
try(FileLock lock = ch.lock()) {
ch.position(0);
if(ch.size() > 0) ch.truncate(0);
Writer w = Channels.newWriter(ch, StandardCharsets.UTF_8.newEncoder(), 8192);
w.write(jsonObj.toString());
w.flush();
}
}
在写入过程中锁定文件。但根据系统的不同,文件锁定可能不是强制性的,但只会影响同样试图获得读取锁定的读者。
当您使用 JDK 11 或更新版本时,您可以考虑使用
for(int i = 0; i < 100000; i++) {
// do Something;
if(i % 100 == 0) {
Files.writeString(Paths.get(writeDir, "metadata.txt"), jsonObject.toString());
}
}
这显然以简单性取胜(是的,这是完整的代码,不需要额外的方法)。默认选项已经包含所需的 StandardCharsets.UTF_8
和 StandardOpenOption.TRUNCATE_EXISTING
。
虽然它确实在内部打开和关闭文件,但它有一些其他的性能调整可以弥补。特别是在字符串仅由 ASCII 字符组成的可能情况下,因为实现会简单地将字符串的内部数组直接写入文件。
关于Java 只打开 BufferedWriter 一次但多次重写内容,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71386037/