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

Is Files.copy a thread-safe function in Java?

我有一个功能,目的是创建一个目录并将 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
            }
        }
    }

总结一下。从这个意义上说,这个函数是线程安全的吗,不同的线程都可以 运行 这个函数并行而不中断?我想是的,但任何想法都会有所帮助!

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

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

这是我在关注评论后得到的(我仍然需要使用 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();
        }
    }

不是。

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

大概您不只是希望 'only one' 线程赢得制作此文件的竞赛,您 希望该文件要么完美,要么不存在完全:您不希望 anybody 能够观察到处于半生不熟状态的 CSV 文件,并且您肯定不希望在生成 CSV 文件的过程中发生崩溃意味着文件在那里,半生不熟,但它的存在意味着它阻止了任何正确写出它的尝试。您不能使用 finally 块或异常捕获来解决此问题;有人可能会被电源线绊倒。

那么,你是如何解决所有这些问题的?

不会写入foo.csv。相反,您写入 foo.csv.23498124908.tmp 随机生成的数字。因为这不是任何人正在寻找的实际 CSV 文件,所以您可以花很多时间来正确完成它。完成后,你就可以施展魔法了:

您将 foo.csv.23498124908.tmp 重命名为 foo.csv,并原子地 - 一个瞬间 foo.csv 不存在,下一个瞬间它及时完成并且具有完整的内容。此外,只有在文件之前不存在时重命名才会成功:两个单独的线程不可能同时将它们的 foo.csv.23481498.tmp 文件重命名为 foo.csv。如果您要尝试并获得完美的时机,其中一个(任意一个)'wins',另一个得到 IOException 并且不重命名任何东西。

执行此操作的方法是使用 Files.move(from, to, StandardCopyOptions.ATOMIC_MOVE)。如果 OS/filesystem 组合根本不支持 ATOMIC_MOVE(尽管他们几乎都支持),ATOMIC_MOVE 甚至会拒绝执行。

第二个优点是,即使您有多个完全不同的应用程序,这种锁定机制也能正常工作 运行ning。如果他们都使用 ATOMIC_MOVE 或该语言 API 中的等价物,则只有一个可以获胜,无论我们在谈论 'threads in a JVM' 还是 'apps on a system'。

如果您想避免这样的想法,即多个线程都同时在做这个 CSV 文件的工作,即使只有一个应该这样做,其余的应该 'wait' 直到第一个线程完成,文件系统锁 不是答案 - 你可以尝试(创建一个空文件,它的存在是其他线程正在处理它的标志) - 甚至还有一个原语 java 的 java.nio.file API。 CREATE_NEW 标志可以在创建文件时使用,这意味着:原子地创建它,如果文件已经存在并发保证则失败(如果多个 processes/threads 所有 运行 同时,一个成功和所有其他人都失败了,保证)。但是,CREATE_NEW只能原子创建。它不能原子地写,什么都不能(因此上面的整个 'rename it into place' 技巧)。

这种锁的问题有两个:

  • 如果 JVM 崩溃,该文件不会消失。曾经启动过 linux 守护进程,例如 postgresd,它告诉你 'the pid file is still there, if there is no postgres running please delete it'?是啊,那个问题。
  • 除了每隔几毫秒重新检查该文件是否存在之外,无法知道它何时完成。如果您等待几毫秒,您可能会破坏磁盘(希望您的 OS 和磁盘缓存算法做得不错)。如果你等了很多,你可能会无缘无故地等待很长时间。

因此你不应该做这些事情,而只是在进程中使用锁。使用 synchronized 或创建一个新的 java.util.concurrent.ReentrantLock 或诸如此类的东西。


要具体回答您的代码片段,没有损坏:2 个线程可以同时 运行 并且都得到 false 运行s dest.exists(),因此都进入了复制块,然后在复制时互相倒下 - 取决于文件系统,通常一个线程结束 'winning',他们的复制操作成功,另一个线程似乎迷失了以太(大多数文件系统都是基于 ref/node 的,这意味着文件已写入磁盘但其 'pointer' 立即被覆盖,文件系统认为它是垃圾,更多或更少)。

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

注意:API 你在用什么? Files.copy(instanceOfJavaIoFile, anotherInstanceOfJavaIoFile) 不是 java。有 java.nio.file.Files.copy(instanceOfjnfPath, anotherInstanceOfjnfPath) - 这就是您想要的。也许您拥有的这个 Files 来自 apache commons?我强烈建议你不要使用那些东西;那些 APIs 通常已经过时(java 本身有更好的 APIs 来做同样的事情),而且设计得很糟糕。弃用 java.io.File,它已经过时 API。请改用 java.nio.file。旧的 API 没有 ATOMIC_MOVE 或 CREATE_NEW,并且在出现问题时不会抛出异常 - 它只是 returns false,这很容易被忽略并且没有空间解释出了什么问题。因此,为什么你不应该使用它。 apache 库的一个主要问题是它使用了将大量静态实用方法堆积到一个巨大容器中的反模式。不幸的是,java 本身 (java.nio.file) 中的第二个文件内容同样是愚蠢的 API 设计。我猜在java的世界里,第三次就是魅力。无论如何,一个具有高级功能的坏核心 java API 仍然比一个坏的 apache 实用程序 API 更好,它环绕着旧的 API,它根本不暴露您在这里需要的各种功能。