为什么 File.exists() 在多线程环境中表现不稳定?
Why is File.exists() behaving flakily in multithreaded environment?
我在 java JDK 1.7 下有一个批处理 运行ning。它 运行 在装有 RHEL 的系统上运行,2.6.18-308.el5 #1 SMP。
此过程从数据库中获取元数据对象列表。它从这个元数据中提取文件的路径。该文件可能实际存在,也可能不存在。
该进程使用 ExecutorService (Executors.newFixedThreadPool()
) 启动多个线程。每个线程 运行 都是一个 Callable,它启动一个进程,该进程读取该文件并在该输入文件存在时写入另一个文件(并记录结果),如果该文件不存在则不执行任何操作(除了记录该结果)。
我发现行为不确定。虽然每个文件的实际存在始终是不变的,运行宁这个过程并没有给出一致的结果。它通常会给出正确的结果,但偶尔会发现一些确实存在的文件却不存在。如果我再次 运行 相同的过程,它会发现它之前说不存在的文件。
为什么会发生这种情况,是否有更可靠的替代方法?当其他线程试图读取目录时,在多线程进程中写入文件是错误的吗?较小的线程池会有帮助吗(目前是 30 个)?
更新:
这是在这种情况下工作线程调用的 unix 进程的实际代码:
public int convertOutputFile(String inputFile, String outputFile)
throws IOException
{
List<String> args = new LinkedList<String>();
args.add("sox");
args.add(inputFile);
args.add(outputFile);
args.addAll(2, this.outputArguments);
args.addAll(1, this.inputArguments);
long pStart = System.currentTimeMillis();
int status = -1;
Process soxProcess = new ProcessBuilder(args).start();
try {
// if we don't wait for the process to complete, player won't
// find the converted file.
status = soxProcess.waitFor();
if (status == 0) {
logger.debug(String.format("SoX conversion process took %d ms.",
System.currentTimeMillis() - pStart));
} else {
logger.error("SoX conversion process returned an error status of " + status);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return status;
}
更新#2:
我已经尝试过从 java.io.File.exists() 切换到 java.nio.Files.exists() 的实验,这似乎提供了更高的可靠性。我还没有看到多次尝试的失败情况,与以前一样,它发生的概率大约为 10%。所以我想我想知道 nio 版本在处理底层文件系统的方式上是否更健壮。 这一发现后来被证明是错误的。 nio 在这里没有帮助。
更新#3:
经过进一步审查,我仍然发现发生了相同的故障情况。所以转用nio并不是万能的。通过将执行程序服务的线程池大小减少到 1,我获得了更好的结果。这似乎更可靠,并且这样一来,一个线程就没有机会读取目录,而另一个线程正在启动一个写入相同的进程目录。
另一种我尚未调查的可能性是,将输出文件放在与输入文件不同的目录中是否会更好。我将它们放在同一个目录中,因为这样更容易编码,但这可能会造成混淆,因为输出文件的创建会影响与输入目录扫描相同的目录。
更新#4:
重新编码以便将输出文件写入与输入文件(正在检查其存在)不同的目录并没有特别的帮助。 唯一有帮助的更改是 ExecutorService 线程池大小为 1,换句话说,不是多线程此操作。
您的应用程序可能是正确的多线程,无论何时您访问文件系统,它都有限制。
在你的情况下,我敢打赌有太多线程同时访问它,结果是 FS 用完了文件句柄。文件实例没有办法告诉你,因为 exists()
不会抛出异常,所以它们只是 return false
,即使目录存在。
这里真正的问题是你为什么调用它?
- 你必须构造一个
FileInputStream
或FileReader
来读取文件,如果文件无法打开,它们将抛出一个FileNotFoundException
,绝对可靠。
- 无论如何你都必须捕获异常。
- 操作系统必须检查文件是否存在。
- 不需要检查两次
- 存在可以在检查它和打开文件之间改变。
所以,不要检查它两次。让打开文件完成所有工作。
Is it a mistake to be writing files in a multithreaded process
我不会说这是一个错误,但它毫无意义。磁盘不是 multi-threaded。
Would a smaller Thread Pool help (currently 30)?
无论如何我肯定会把它减少到四个左右,不是为了解决这个问题而是为了减少抖动并几乎肯定会提高吞吐量。
我已将@Olivier 的回答标记为"the" 回答,但我在这里提供我自己的回答,以总结我的实验结果。我称它为 "the" 比任何人都更接近真相的答案,尽管他对文件句柄的猜测似乎并不明显正确,尽管我也无法反驳。他的简单陈述“您的应用程序可能是正确的多线程,无论何时访问文件系统,它都有局限性。”这与我的发现一致。如果有人能进一步阐明,我可能会改变这个。
- 这是我代码中的错误吗?
高度怀疑。 运行 相同的进程在相同的文件列表上重复随机显示一些文件,当它们确实存在时显示为 non-existent。 运行 再次处理,发现这些相同的文件存在。这些文件的存在在此期间发生变化的可能性为零。
- 使用
java.nio.Files.exists()
而不是 java.io.File.exists()
有帮助吗?
没有。文件系统的底层接口似乎没有什么不同。 nio 在这方面的改进似乎仅限于 nio 中链接的处理,这不是这里的问题。但我不能肯定地说,因为这是本机代码。
- 是否将输入文件和输出文件放在不同的目录中,这样我的存在性检查就不会读取写入输出文件的同一目录,帮助?
没有。导致问题的似乎不是 目录 上的两次同时点击,而是 文件系统 上的两次同时点击。
- 减少池中的线程数有帮助吗?
只有将它减少到 1 才能使其可靠,换句话说,只有完全取消多线程方法才有帮助。这个操作似乎不是 100% 可靠的,至少对于这个 OS 和 JDK,多线程。
如果重新设计 sox 以便为输入文件中未找到的文件提供明确的错误代码,这可能会使上述@EJP 的答案变得可行。
我在 java JDK 1.7 下有一个批处理 运行ning。它 运行 在装有 RHEL 的系统上运行,2.6.18-308.el5 #1 SMP。
此过程从数据库中获取元数据对象列表。它从这个元数据中提取文件的路径。该文件可能实际存在,也可能不存在。
该进程使用 ExecutorService (Executors.newFixedThreadPool()
) 启动多个线程。每个线程 运行 都是一个 Callable,它启动一个进程,该进程读取该文件并在该输入文件存在时写入另一个文件(并记录结果),如果该文件不存在则不执行任何操作(除了记录该结果)。
我发现行为不确定。虽然每个文件的实际存在始终是不变的,运行宁这个过程并没有给出一致的结果。它通常会给出正确的结果,但偶尔会发现一些确实存在的文件却不存在。如果我再次 运行 相同的过程,它会发现它之前说不存在的文件。
为什么会发生这种情况,是否有更可靠的替代方法?当其他线程试图读取目录时,在多线程进程中写入文件是错误的吗?较小的线程池会有帮助吗(目前是 30 个)?
更新: 这是在这种情况下工作线程调用的 unix 进程的实际代码:
public int convertOutputFile(String inputFile, String outputFile)
throws IOException
{
List<String> args = new LinkedList<String>();
args.add("sox");
args.add(inputFile);
args.add(outputFile);
args.addAll(2, this.outputArguments);
args.addAll(1, this.inputArguments);
long pStart = System.currentTimeMillis();
int status = -1;
Process soxProcess = new ProcessBuilder(args).start();
try {
// if we don't wait for the process to complete, player won't
// find the converted file.
status = soxProcess.waitFor();
if (status == 0) {
logger.debug(String.format("SoX conversion process took %d ms.",
System.currentTimeMillis() - pStart));
} else {
logger.error("SoX conversion process returned an error status of " + status);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return status;
}
更新#2:
我已经尝试过从 java.io.File.exists() 切换到 java.nio.Files.exists() 的实验,这似乎提供了更高的可靠性。我还没有看到多次尝试的失败情况,与以前一样,它发生的概率大约为 10%。所以我想我想知道 nio 版本在处理底层文件系统的方式上是否更健壮。 这一发现后来被证明是错误的。 nio 在这里没有帮助。
更新#3: 经过进一步审查,我仍然发现发生了相同的故障情况。所以转用nio并不是万能的。通过将执行程序服务的线程池大小减少到 1,我获得了更好的结果。这似乎更可靠,并且这样一来,一个线程就没有机会读取目录,而另一个线程正在启动一个写入相同的进程目录。
另一种我尚未调查的可能性是,将输出文件放在与输入文件不同的目录中是否会更好。我将它们放在同一个目录中,因为这样更容易编码,但这可能会造成混淆,因为输出文件的创建会影响与输入目录扫描相同的目录。
更新#4: 重新编码以便将输出文件写入与输入文件(正在检查其存在)不同的目录并没有特别的帮助。 唯一有帮助的更改是 ExecutorService 线程池大小为 1,换句话说,不是多线程此操作。
您的应用程序可能是正确的多线程,无论何时您访问文件系统,它都有限制。
在你的情况下,我敢打赌有太多线程同时访问它,结果是 FS 用完了文件句柄。文件实例没有办法告诉你,因为 exists()
不会抛出异常,所以它们只是 return false
,即使目录存在。
这里真正的问题是你为什么调用它?
- 你必须构造一个
FileInputStream
或FileReader
来读取文件,如果文件无法打开,它们将抛出一个FileNotFoundException
,绝对可靠。 - 无论如何你都必须捕获异常。
- 操作系统必须检查文件是否存在。
- 不需要检查两次
- 存在可以在检查它和打开文件之间改变。
所以,不要检查它两次。让打开文件完成所有工作。
Is it a mistake to be writing files in a multithreaded process
我不会说这是一个错误,但它毫无意义。磁盘不是 multi-threaded。
Would a smaller Thread Pool help (currently 30)?
无论如何我肯定会把它减少到四个左右,不是为了解决这个问题而是为了减少抖动并几乎肯定会提高吞吐量。
我已将@Olivier 的回答标记为"the" 回答,但我在这里提供我自己的回答,以总结我的实验结果。我称它为 "the" 比任何人都更接近真相的答案,尽管他对文件句柄的猜测似乎并不明显正确,尽管我也无法反驳。他的简单陈述“您的应用程序可能是正确的多线程,无论何时访问文件系统,它都有局限性。”这与我的发现一致。如果有人能进一步阐明,我可能会改变这个。
- 这是我代码中的错误吗?
高度怀疑。 运行 相同的进程在相同的文件列表上重复随机显示一些文件,当它们确实存在时显示为 non-existent。 运行 再次处理,发现这些相同的文件存在。这些文件的存在在此期间发生变化的可能性为零。
- 使用
java.nio.Files.exists()
而不是java.io.File.exists()
有帮助吗?
没有。文件系统的底层接口似乎没有什么不同。 nio 在这方面的改进似乎仅限于 nio 中链接的处理,这不是这里的问题。但我不能肯定地说,因为这是本机代码。
- 是否将输入文件和输出文件放在不同的目录中,这样我的存在性检查就不会读取写入输出文件的同一目录,帮助?
没有。导致问题的似乎不是 目录 上的两次同时点击,而是 文件系统 上的两次同时点击。
- 减少池中的线程数有帮助吗?
只有将它减少到 1 才能使其可靠,换句话说,只有完全取消多线程方法才有帮助。这个操作似乎不是 100% 可靠的,至少对于这个 OS 和 JDK,多线程。
如果重新设计 sox 以便为输入文件中未找到的文件提供明确的错误代码,这可能会使上述@EJP 的答案变得可行。