多线程环境下的mkdirs()函数

7

我正在创建文件和文件夹的树形结构。我正在重写为多线程。我唯一看到的弱点是创建文件夹时。现在它按深度一个一个地进行。在写入文件之前,我会检查路径是否存在。如果不存在,我使用mkdirs创建所有缺失的内容。

public void checkDir(String relativePath) {
        File file = new File(homePath + relativePath);
        if (!file.exists()) {
            if (file.mkdirs()) {
                log.info("Directory: " + homePath + relativePath + " is created!");
            } else {
                log.error("Failed to create directory: " + homePath + relativePath + " !");
            }
        }
    }

我有一个问题,当我使用两个线程时会发生什么情况。一个线程的路径是A/B/C,另一个线程的路径是A/B/D。假设我只有A文件夹存在,但没有B文件夹。因此,它们都将检查该路径不存在并想要创建它。因此,其中一个线程可能会失败,因为另一个线程速度更快。那么我该如何管理这种情况呢?

  1. 我考虑删除存在条件并让其失败,但没有已存在的异常可以捕获。
  2. 首先创建目录树(但我认为还有更好的方法?)
  3. 将目录创建作为关键部分,并使其顺序执行-不确定如何在Spring中执行此操作,但无论如何,也不确定是否必要,也不确定它是否会减慢进程速度太多。

也许我想得太多了,但理论上可能会发生这种情况。当前我使用常规Thread,但我想为此使用Spring TaskExecutor。它自己处理关键部分,但这不是共享变量或任何内容,路径也是不同的,因此我认为它不会识别它。

感谢您的建议。


如果一个更快,Java会意识到目录已经存在,不会再次创建,所以我不会失败。 - user2693587
既然这似乎不是关键性能问题,为什么不只是同步方法呢? - Zarathustra
你应该考虑Kári的回答。Atomic并不自动意味着操作是同步的。 - Antonio
请注意,使用字符串连接创建文件路径是容易出错且不稳定的。因为文件本身就应该用其他方式来表示。我建议您重写代码,从一开始就使用Path并使用Path上的方法(如relativizeresolve)来处理这些路径。最后,使用Files进行操作,因为它的错误处理要好得多。简而言之,请不要使用String表示类型数据,也不要使用File API。 - Boris the Spider
3个回答

7
File.mkdirs()方法被规定为当目录不存在时,创建该目录及其所有父目录。因此,调用exists()方法没有意义。存在性将被检查。调用exists()只是浪费时间。 mkdirs()本质上是一个原子操作:实际上并没有尝试去超越它的必要。
请注意,返回false值不一定表示失败。它可能仅表示路径中所有目录都已存在。
基本上,您问题的前提是错误的。

1
谢谢,那是非常简单的解决方案 :) - Elis.jane
请参阅JDK-4742723(已修复),该问题描述了多线程目录创建失败的情况。 - Marcono1234

5
没有一个答案能够解决mkdirs()是否线程安全的问题。有一个答案表明,mkdirs()是原子性的,但可能存在失败的情况。该函数主要涉及文件系统,因此可能需要调用操作系统的系统调用。如果您不知道将要使用应用程序的目标系统,确定这些系统调用是否实际上是线程安全的可能是不可能的。
例如,即使mkdirs()在创建文件夹结构之前检查存在性,在以下情况下会发生什么?
线程1调用mkdirs(),它固有地检查文件夹结构的存在性并确定它不存在。这时,线程1被抢占了。
线程2调用mkdirs(),它固有地检查文件夹结构的存在性,并确定它不存在,随后继续创建文件夹结构。
线程1重新启动并继续尝试创建文件夹结构,以前已经确定了它不存在。
那里会发生什么?我不知道,这一系列事件很难测试,特别是考虑到创建文件夹系统调用在操作系统之间的差异。为了确保线程安全并避免引入潜在难以跟踪和调试的错误,最好在代码的关键部分实现一定的互斥。
我想采取一种天真的方法声明一个单一的“全局”变量,两个线程都可以访问,例如一个布尔变量b,并在临界区域周围添加以下代码:
synchronized(b) {
     // Your critical section here
}

这将确保如果一个线程已经锁定了b,那么它将独自访问临界区而另一个线程等待,从而确保mkdir()不会被两个线程调用。但是,如果您想更深入地了解多线程以及如何在较低层次上实现互斥,在这种情况下,我建议您查看信号量以及如何实现它们来解决此问题。

我同意这个解释。Atomic并不自动意味着操作是同步的。 - Antonio
你不知道它是否本质上检查了存在性。如果我在实现它,我不会检查任何东西。我只会尝试创建所有相关的目录,从父目录到子目录,每个目录在内核中都是原子的,然后仅当所有创建都成功时返回true。因此,不可能出现任何多线程问题。而且我认为Sun的人也看到了这一点。 - user207421

1
正如EJP所指出的那样,false 的返回值可能意味着许多不同的事情,有些是错误,有些则不是。如果您想记录它实际上无法创建目录的事实,您应该在此之后检查其存在性。
public final class DirectoryHelper {
   private DirectoryHelper(){}

   public static boolean createDirectories(File path) {
      if (path.mkdirs()) return true; //definitely has new dir
      // if false, just look afterwards for the existence of the directory
      // also opportunity to throw own exceptions if you prefer
      return path.exists() && path.isDirectory();
   }
}

我在这里编写了一种新方法,只有在之后目录不存在时才返回false。我不关心它是刚刚创建还是已经存在。由于新的顺序,我也不需要一个synchronized块。
那么你的代码看起来就像这样:
public void checkDir(String relativePath) {
    File file = new File(new File(homePath), relativePath);
    if (!file.exists()) { // just to prevent logging of existing dirs
        if (DirectoryHelper.createDirectories(file)) {
            log.info("Directory: " + file + " is created!");
        } else {
            log.error("Failed to create directory: " + file + " !");
        }
    }
};

或者你可以直接使用更好规范的“Path” API。 - Boris the Spider
这适用于我的回答的哪一部分? - weston
检查返回值并尝试找出问题所在的逻辑。 - Boris the Spider
1
但这就是整个答案。也许你可以把你的意思发表为一个答案。 - weston

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接