为什么File.exists()在多线程环境中表现不稳定



我有一个在java JDK 1.7下运行的批处理进程。它在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版本在处理底层文件系统方面是否更健壮这一发现后来被证明是错误的。尼奥在这里帮不了什么忙

更新#3:经过进一步审查,我仍然发现同样的故障情况正在发生。因此,转投nio并不是万灵药。通过将executor服务的线程池大小减少到1,我获得了更好的结果。这似乎更可靠,这样就不会有一个线程读取目录,而另一个线程正在启动一个写入同一目录的进程。

我还没有调查的另一种可能性是,将输出文件放在与输入文件不同的目录中是否会更好地为我服务。我把它们放在同一个目录中,因为这样更容易编码,但这可能会让人困惑,因为输出文件的创建会影响到与输入目录扫描相同的目录。

更新#4:重新编码以将输出文件写入与输入文件不同的目录(正在检查输入文件的存在)并没有特别的帮助唯一有帮助的改变是将ExecutorService线程池大小设置为1,换句话说,不是对该操作进行多线程处理

我已经将@Olivier的答案标记为"the"答案,但为了总结我的实验结果,我在这里提供了自己的答案。我称之为比任何人都更接近真相的"答案",尽管他对文件句柄的猜测似乎并不明显正确,尽管我也无法反驳。他的简单语句"您的应用程序可能是正确的多线程,无论何时访问FileSystem,它都有局限性。"这与我的发现一致。如果有人能透露更多的信息,我可能会改变这一点。

  1. 这是我代码中的错误吗

高度怀疑。在相同的文件列表上重复运行相同的进程会随机显示一些文件,当它们确实存在时,显示为不存在。再次运行进程时,发现存在这些相同的文件。在此期间,这些文件的存在发生变化的可能性为零。

  1. 使用java.nio.Files.exists()而不是java.io.File.exists()有帮助吗

没有。文件系统的底层接口似乎没有什么不同。nio在这方面的改进似乎仅限于处理nio中的链接,这不是这里的问题。但我不能肯定,因为这是本机代码。

  1. 将输入和输出文件放在不同的目录中,这样我的存在性检查就不会读取写入输出文件的同一目录,这有帮助吗

没有。导致问题的似乎不是对目录的两次同时命中,而是对文件系统的两次并发命中。

  1. 减少池中的线程数量有帮助吗

只有将其减少到1才能使其可靠,换句话说,只需完全取消多线程方法,就会有所帮助。这个操作似乎不是100%可靠的,至少在这个操作系统和JDK多线程的情况下不是这样。

如果对sox进行重新设计,以便在输入文件中为"找不到文件"提供一个明显的错误代码,这可能会使@EJP的答案变得可行。

这里真正的问题是你为什么要叫它?

  • 你必须构造一个FileInputStreamFileReader来读取一个文件,如果文件无法打开,它们将抛出一个具有绝对可靠性的FileNotFoundException
  • 无论如何,您都必须捕获异常
  • 操作系统必须检查该文件是否存在
  • 没有必要检查两次
  • 存在性可能会在检查和打开文件之间发生变化

所以,不要检查两次。让打开文件来完成所有的工作。

在多线程进程中写入文件是错误的吗

我不会说这是一个错误,但这毫无意义。磁盘不是多线程的。

一个较小的线程池(目前为30)会有帮助吗?

无论如何,我肯定会把它减少到四个左右,不是为了解决这个问题,而是为了减少颠簸,几乎可以肯定地提高吞吐量。

您的应用程序可能是正确的多线程,无论何时访问FileSystem,它都有局限性。在您的情况下,我敢打赌有太多线程同时访问它,结果是FS用完了文件句柄。文件实例无法告诉您这一点,因为exists()不会抛出Exception,所以它们只是返回false,即使目录存在。

最新更新