【问题标题】:Why is File.exists() behaving flakily in multithreaded environment?为什么 File.exists() 在多线程环境中表现异常?
【发布时间】:2016-01-06 22:37:14
【问题描述】:

我有一个在 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 版本在处理底层文件系统的方式上是否更健壮。 这一发现后来被证明是错误的。 nio 在这里没有帮助。

更新 #3: 经过进一步审查,我仍然发现发生相同的故障情况。所以切换到nio并不是万能的。通过将执行程序服务的线程池大小减少到 1,我获得了更好的结果。这似乎更可靠,并且没有机会一个线程读取目录,而另一个线程正在启​​动一个写入相同的进程目录。

我尚未调查的另一种可能性是将输出文件放在与输入文件不同的目录中是否会更好。我将它们放在同一个目录中是因为它更容易编码,但这可能会造成混淆,因为输出文件的创建会影响与输入目录扫描相同的目录。

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

【问题讨论】:

  • 你的问题很不清楚。请问可以发MCVE吗?
  • 我无法轻松发布 MCVE。对不起。尽管如此,我已经指出,工作线程的每个调用都会启动一个进程(一个 unix 进程),并且该进程读取文件并写入另一个文件(到同一目录中),如果考虑到多线程环境,File.exists() 会给出不好的结果其中目录的内容正在发生变化,nio.Files.exists() 可能会给出更可靠的结果?
  • "寻求调试帮助的问题(“为什么这段代码不起作用?”)必须包括所需的行为、特定问题或错误以及在问题本身中重现它所需的最短代码。没有明确的问题陈述对其他读者没有用处。”
  • @SteveCohen 不。您的代码中很可能存在错误。但是很难说是什么,我们不知道你的哪些工作做了什么。以哪个顺序和哪个线程,或者是否有任何竞争条件等。
  • 另外,我怀疑您的程序正在产生新的 unix 进程。它可能只是产生本地线程,这与另一个包含 JVM 的整个进程完全不同。如果你是ps --forest aux | grep java,不会有 30 个 jvm 进程,只有一个。

标签: java multithreading nio java-io jdk1.7


【解决方案1】:

我已将@Olivier 的答案标记为“the”答案,但我在这里提供我自己的答案,以总结我的实验结果。我称其为比其他任何人都更接近真相的“答案”,尽管他对文件句柄的猜测似乎并不明显正确,尽管我也无法反驳它。真正正确的是他的简单陈述“您的应用程序可能是适当的多线程,无论何时访问文件系统,它都有限制。”这与我的发现一致。如果有人可以进一步阐明,我可能会改变这一点。

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

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

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

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

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

没有。导致问题的似乎不是 目录 上的两个同时点击,而是 文件系统 上的两个同时点击。

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

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

如果重新设计 sox 以便在输入文件上为 File Not Found 提供不同的错误代码,这可能会使上述@EJP 的答案变得可行。

【讨论】:

    【解决方案2】:

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

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

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

    在多线程进程中写入文件是不是一个错误

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

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

    无论如何,我肯定会把这个减少到四个左右,不是为了解决这个问题,而是为了减少抖动并几乎肯定会提高吞吐量。

    【讨论】:

    • 嗯,不。不会抛出异常,但生成的进程会失败,并且该进程(启动可执行 SoX)不会产生有意义的错误代码,这将使我能够确定失败的原因是否是 FileNotFound,这恰好是重要信息这个案例。我可以试试你减少线程数的建议。
    • 虽然我已经通过从 java.io.File.exists() 切换到 java.nio.Files.exists() 提高了可靠性,但我会报告并支持你的答案,因为你建议减少线程数确实提高了吞吐量。谢谢。
    • 有多少线程同时运行?关于 Files.exists(),看看docs.oracle.com/javase/tutorial/essential/io/check.html:它并不比 File.exists() 安全多少,所以观察到的改进可能根本不相关。
    • 30。我根据@EJB 将它减少到 6,它确实提高了吞吐量。
    【解决方案3】:

    您的应用程序可能是正确的多线程,每当您访问文件系统时,它都有限制。 在你的情况下,我敢打赌,太多的线程同时访问它,结果是 FS 用完了文件句柄。文件实例无法告诉你,因为exists() 不会抛出异常,所以即使目录存在,它们也只会返回false

    【讨论】:

    • 虽然驾车投票确实很糟糕,但您的回答更像是评论。
    • @Olivier - 我赞成你,因为你至少得到了我试图回答的问题,即使不是权威的,这也是 File.exists() 给定机器的可靠性许多读取和写入同时发生。我不相信文件句柄是这里的问题。但是,暂时忘记java中的多线程,想象一个服务器,其中有几个独立的进程正在执行磁盘写入。如果这些写入有时会写入同一个目录,那么调用 File.exists() 的 java 应用程序会偶尔失败吗?
    • 感谢您的支持,我更愿意尝试回答而不是去 meta 并大喊“MCVE!”;) File.exist() 本质上容易出错,正如我在回答中解释的那样,它如果一切顺利并且目录存在,则返回 true,或者在任何其他情况下返回 false,基本上意味着“我无法确认它存在”而不是“它不存在”。这适用于大多数 File 方法。
    • 我的哲学和你的相似。有些问题不能简单而简洁地陈述,它们引发的讨论可能是有用的,即使不会导致快速、简洁的解决方案。您能否评论我的更新,该更新报告 java.nio.Files.exists() 似乎比 java.io.File.exists() 更可靠?
    猜你喜欢
    • 1970-01-01
    • 2013-07-27
    • 2017-04-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-29
    • 2013-12-06
    相关资源
    最近更新 更多