Files.copy 是 Java 中的线程安全函数吗?

mco*_*ool 1 java multithreading asynchronous thread-safety

我有一个函数,其目的是创建一个目录并将 csv 文件复制到该目录。同一个函数会运行多次,每次都由不同线程中的对象运行。它在对象的构造函数中被调用,但我在那里有逻辑,仅在文件尚不存在时才复制该文件(这意味着,它会检查以确保并行的其他实例之一尚未创建它)。

现在,我知道我可以简单地重新排列代码,以便在并行运行对象之前创建此目录并复制文件,但这对于我的用例来说并不理想。

我想知道,下面的代码会失败吗?也就是说,由于其中一个实例正在复制文件,而另一个实例尝试开始将同一文件复制到同一位置?

    private void prepareGroupDirectory() {
        new File(outputGroupFolderPath).mkdirs();
        String map = "/path/map.csv"
        File source = new File(map);
        
        String myFile = "/path/test_map.csv";
        File dest = new File(myFile);
        
        // copy file
        if (!dest.exists()) {
            try{
                Files.copy(source, dest);
            }catch(Exception e){
                // do nothing
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

总而言之。该函数是否是线程安全的,因为不同的线程都可以并行运行该函数而不会中断?我想是的,但任何想法都会有帮助!

需要明确的是,我已经对此进行了多次测试,并且每次都有效。我问这个问题是为了确保,从理论上讲,它仍然永远不会失败。

编辑:此外,这是高度简化的,以便我可以以易于理解的格式提出问题。

这是我在遵循评论后现在所拥有的(我仍然需要使用nio),但这目前正在工作:

   private void prepareGroupDirectory() {
        new File(outputGroupFolderPath).mkdirs();
        logger.info("created group directory");

        String map = instance.getUploadedMapPath().toString();
        File source = new File(map);
        String myFile = FilenameUtils.getBaseName(map) + "." + FilenameUtils.getExtension(map);
        File dest = new File(outputGroupFolderPath + File.separator + "results_" + myFile);
        instance.setWritableMapForGroup(dest.getAbsolutePath());
        logger.info("instance details at time of preparing group folder: {} ", instance);
        final ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {
            // copy file
            if (!dest.exists()) {
                String pathToWritableMap = createCopyOfMap(source, dest);
                logger.info(pathToWritableMap);
            }
        } catch (Exception e) {
            // do nothing
            // thread-safe
        } finally {
            lock.unlock();
        }
    }
Run Code Online (Sandbox Code Playgroud)

rzw*_*oot 5

事实并非如此。

您正在寻找的是旋转到位的概念。文件操作的问题在于几乎没有一个操作是原子的。

想必您不仅仅希望“只有一个”线程赢得制作此文件的竞赛,您希望该文件要么完美,要么根本不存在:您不希望任何人能够观察该 CSV 文件在半生不熟的状态下,您肯定不希望在生成 CSV 文件的过程中发生崩溃,这意味着该文件在那里,半生不熟,但它的存在本身就意味着它会阻止任何正确写出该文件的尝试。您不能使用finally块或异常捕获来解决此问题;有人可能会被电源线绊倒。

那么,如何解决所有这些问题呢?

你不写信foo.csv. 相反,您可以写入foo.csv.23498124908.tmp随机生成该数字的位置。因为这并不是任何人都在寻找的实际 CSV 文件,所以您可能会花上很长时间才能正确完成它。完成后,你就可以表演魔术了:

您重命名foo.csv.23498124908.tmpfoo.csv,并以原子方式执行此操作- 某个时刻foo.csv不存在,下一时刻它存在并且具有完整的内容。此外,只有当文件之前不存在时,重命名才会成功:两个单独的线程不可能同时将其foo.csv.23481498.tmp文件重命名为foo.csv。如果您尝试并获得完美的时机,其中一个(任意一个)“获胜”,另一个会得到 IOException 并且不会重命名任何内容。

执行此操作的方法是使用Files.move(from, to, StandardCopyOptions.ATOMIC_MOVE). 如果操作系统/文件系统组合根本不支持 ATOMIC_MOVE(尽管它们几乎都支持),ATOMIC_MOVE 甚至会很友善地拒绝执行。

第二个优点是,即使您运行多个完全不同的应用程序,这种锁定机制也能发挥作用。如果他们都ATOMIC_MOVE在该语言的 API 中使用此功能或等效功能,那么无论我们谈论的是“JVM 中的线程”还是“系统上的应用程序”,只有一个可以获胜。

如果您想避免多个线程同时执行创建此 CSV 文件的工作,即使只有一个线程应该这样做,其余线程应该“等待”直到第一个线程完成,文件系统锁不是答案- 你可以尝试(创建一个空文件,其存在表明其他线程正在处理它) - 在 java 的java.nio.fileAPI 中甚至有一个原语。CREATE_NEW创建文件时可以使用该标志,这意味着:以原子方式创建文件,如果文件已存在且具有并发保证,则失败(如果多个进程/线程全部同时运行,则保证其中一个成功,所有其他都失败)。然而,CREATE_NEW只能原子地创建。它不能自动写入,没有任何东西可以(因此上面的整个“将其重命名到位”技巧)。

这种锁的问题有两个:

  • 如果 JVM 崩溃,该文件不会消失。您是否曾经启动过 Linux 守护进程,例如 postgresd,并且它告诉您“pid 文件仍然存在,如果没有 postgres 运行,请删除它”?是的,那个问题。
  • 除了每隔几毫秒重新检查该文件是否存在之外,没有办法知道它何时完成。如果您等待几毫秒,则可能会损坏磁盘(希望您的操作系统和磁盘缓存算法做得不错)。如果您等待时间较长,您可能会无缘无故地等待很长时间。

因此,为什么你不应该做这些事情,而只在进程中使用锁。使用synchronized或制造新的java.util.concurrent.ReentrantLock或诸如此类的东西。


要具体回答您的代码片段,没有被破坏:两个线程有​​可能同时运行,并且false在运行时都获得dest.exists(),因此都进入复制块,然后在复制时它们相互覆盖 - 取决于文件系统,通常一个线程最终“获胜”,其复制操作成功,而另一个线程似乎输给了以太(大多数文件系统都是基于引用/节点的,这意味着文件被写入磁盘,但其“指针”立即被覆盖,文件系统或多或少认为它是垃圾)。

想必您认为这是一个失败的场景,并且您的代码不保证它不会发生。

注意:您使用什么 API?Files.copy(instanceOfJavaIoFile, anotherInstanceOfJavaIoFile)不是java。那里有java.nio.file.Files.copy(instanceOfjnfPath, anotherInstanceOfjnfPath)——那就是你想要的。也许Files你的这个来自 apache commons?我强烈建议你不要使用那些东西;这些 API 通常已经过时(java 本身有更好的 API 来完成同样的事情),并且设计得很糟糕。放弃吧java.io.File,它已经过时了。java.nio.file代替使用。旧的 API 没有 ATOMIC_MOVE 或 CREATE_NEW,并且在出现问题时不会抛出异常 - 它只是返回,false这很容易被忽略,并且没有空间来解释出了什么问题。这就是为什么你不应该使用它。Apache 库的主要问题之一是它使用了将大量静态实用方法堆积到一个巨大容器中的反模式。不幸的是,Java 本身对文件内容的第二种看法(参考资料java.nio.file)同样是愚蠢的 API 设计。我想在java世界里,第三次才是魅力所在。无论如何,一个具有高级功能的糟糕的核心 java API 仍然比一个糟糕的 apache 实用程序 API 更好,后者包装了旧的 API,而旧的 API 根本不公开您在这里需要的功能。