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

Posted

技术标签:

【中文标题】Files.copy 是 Java 中的线程安全函数吗?【英文标题】:Is Files.copy a thread-safe function in Java? 【发布时间】:2021-12-16 03:40:27 【问题描述】:

我有一个函数,其目的是创建一个目录并将一个 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
            
        
    

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

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

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

这是我在关注 cmets 之后现在所拥有的(我仍然需要使用 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();
        
    

【问题讨论】:

它可能因此而失败,但这并不意味着该函数不是线程安全的。 java.nio 更好地处理多线程访问;方法尽可能地工作或抛出异常。将dest.exists() 替换为Files.exists(dest.toPath()) 是明智的。事实上,明智的做法是删除对 j​​ava.io.File 的所有使用。更大的问题是你有一个竞争条件,因为你的存在检查和你的创建检查是两个独立的操作;两个线程可以同时观察创建文件的需要。 也可能因操作系统而异。如果您查看代码,它使用 FileSystemProvider 来实际完成工作。 所以我们都同意这个函数可能会失败,所以文件甚至不会被复制一次?只要它被复制到任何线程中,我就可以了 在 Windows 中,它可能只会被复制一次。在任何其他操作系统中,当一个线程覆盖另一个线程正在执行的操作时,您可能会丢失一些数据。我个人会使用锁。 【参考方案1】:

不是。

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

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

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

你确实写信给foo.csv。相反,您写信给foo.csv.23498124908.tmp,该数字是随机生成的。因为这不是任何人都在寻找的实际 CSV 文件,所以您可以花费世界上所有的时间来正确完成它。完成后,您就可以使用魔术了:

您将foo.csv.23498124908.tmp 重命名为foo.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.file API 中甚至有一个原语。 CREATE_NEW 标志可以在创建文件时使用,这意味着:原子地创建它,如果文件已经存在并发保证则失败(如果多个进程/线程同时运行,一个成功而其他所有失败,保证)。但是,CREATE_NEW 只能原子创建。它不能原子地写入,没有任何东西可以(因此上面的整个“重命名到位”技巧)。

这种锁的问题有两个:

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

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


要专门回答您的代码 sn-p,不,这是损坏的:有可能 2 个线程同时运行并且在运行 dest.exists() 时都得到 false,因此都进入复制块,然后它们在复制时相互摔倒 - 取决于文件系统,通常一个线程最终“获胜”,他们的复制操作成功,而另一个线程似乎丢失了以太(大多数文件系统是 ref/node基于,意思是,文件被写入磁盘,但它的“指针”立即被覆盖,文件系统或多或少认为它是垃圾)。

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

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

【讨论】:

很好的答案。谢谢你。我将使用 nio 代替。并且要明确reentrantLock 将解决此问题? 这是一个可以用来解决这个问题的工具 - 锁(可以是 synchronizedj.u.c 包的 Lock 类之一)可用于编写一个系统,使不同的线程一起工作以确保敏感代码路径一次只能由一个线程执行。很明显,您可以如何使用它来确保没有 2 个线程同时写入同一个文件。但这仅适用于一个 JVM,因此如果您担心多个应用程序,则必须使用基于文件系统的锁定。 那对我来说就好了。感谢您的帮助。 > There's no way to know when it is done. There is. 为此订阅磁盘监视服务根本不是一个好计划。这些监视服务高度依赖于底层操作系统和磁盘功能,并且往往会减慢一切。对于你造成的影响,那绝对是用火箭筒击杀蚊子的情况。

以上是关于Files.copy 是 Java 中的线程安全函数吗?的主要内容,如果未能解决你的问题,请参考以下文章

来自 Java NIO2 中的临时文件的 Files.copy 上的 AccessDeniedException

如何在 Java 中取消 Files.copy()?

Java 6 中 Files#copy() 的替代方法 [重复]

使用Drive.Files.copy进行复制是PDF格式,而不是Google文档格式

java中的线程安全

java中的++i是线程安全的吗?