10

在我的一个项目中,我对一个 JRE 中的一个文件具有并发写访问权限,并且希望通过首先写入一个临时文件然后使用原子移动将该临时文件移动到目标来处理这个问题。我不关心写访问的顺序之类的,我只需要保证单个文件在任何给定时间都是可用的。我已经知道 Files.move 等,我的问题是我查看了该方法的至少一个实现,它对实现是否真的保证原子移动提出了一些疑问。请看下面的代码:

Files.move on GrepCode for OpenJDK

1342        FileSystemProvider provider = provider(source);
1343        if (provider(target) == provider) {
1344            // same provider
1345            provider.move(source, target, options);
1346        } else {
1347            // different providers
1348            CopyMoveHelper.moveToForeignTarget(source, target, options);
1349        }

问题是选项 ATOMIC_MOVE 并非在所有情况下都被考虑,但源和目标路径的位置是唯一重要的事情。这不是我想要的,也不是我理解文档的方式:

如果移动不能作为原子文件系统操作执行,则抛出 AtomicMoveNotSupportedException。例如,当目标位置位于不同的 FileStore 上并且需要复制文件时,或者目标位置与该对象的不同提供程序相关联时,就会出现这种情况。

上面的代码显然违反了该文档,因为它在根本不识别 ATOMIC_MOVE 的情况下退回到复制删除策略。在我的情况下,一个例外是完全可以的,因为我们服务的主机可以更改他的设置以仅使用一个支持原子移动的文件系统,因为这就是我们在系统要求中所期望的。我不想处理的事情只是因为实现使用了复制删除策略而静默失败,这可能会导致目标文件中的数据损坏。因此,据我了解,依靠 Files.move 进行原子操作根本不安全,因为如果不支持这些操作,它并不总是会失败,但实现可能会退回到复制删除策略。

这种行为是否是实现中的错误并需要归档,或者文档是否允许这种行为并且我理解错了?如果我现在已经知道在外面使用了这样的可能损坏的实现,这有什么不同吗?在这种情况下,我需要自己同步写访问权限......

4

3 回答 3

12

你看错地方了。当文件系统提供者不相同时,操作将被委派给moveToForeignTarget您在您发布的代码片段中看到的。然而,该方法moveToForeignTarget将使用该方法convertMoveToCopyOptions(注意口语名称……)来获取翻译操作所需的复制选项。如果遇到该选项,convertMoveToCopyOptions将抛出一个,因为无法将该移动选项转换为有效的复制选项。AtomicMoveNotSupportedExceptionATOMIC_MOVE

所以没有理由担心,一般来说,建议避免因为看到不到十行代码而仓促得出结论(尤其是在没有尝试过单个测试的情况下)......</p>

于 2014-08-11T10:10:23.700 回答
2

标准 Java 库并未提供在所有情况下都执行原子移动的方法。

Files.move()不保证原子移动。您可以作为选项传递ATOMIC_MOVE,但如果移动不能作为原子操作执行,AtomicMoveNotSupportedException则会被抛出(当目标位置位于不同的 FileStore 上并且需要复制文件时就是这种情况)。

如果你真的需要,你必须自己实现它。一种解决方案是先捕获AtomicMoveNotSupportedException然后执行此操作:尝试在没有ATOMIC_MOVE选项的情况下移动文件,但如果在复制过程中发生错误,则捕获异常并删除目标。

于 2014-08-11T08:01:37.560 回答
0

我遇到了类似的问题要解决:

  • 一个进程经常通过“保存到临时文件->将临时文件移动到最终文件”来更新文件,使用Files.move(tmp, out, ATOMIC_MOVE, REPLACE_EXISTING);
  • 另一个或多个进程读取该文件 - 完全、一次性并立即关闭。文件相当小 - 不到 50k。

而且它只是不能可靠地工作,至少在 Windows 上是这样。在重负载下阅读器偶尔会得到NoSuchFileException- 这意味着即使在同一个文件系统上Files.move也不是这样:(ATOMIC

我的环境:Windows 10 + java 11.0.12

这是要玩的代码:

import org.junit.Test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Locale.US;

public class SomeTest {

    static int nWrite = 0;
    static int nRead = 0;
    static int cErrors = 0;
    static boolean writeFinished;
    static boolean useFileChannels = true;
    static String filePath = "c:/temp/test.out";

    @Test
    public void testParallelFileAccess() throws Exception {
        new Writer().start();
        new Reader().start();

        while( !writeFinished ) {
            Thread.sleep(10);
        }

        System.out.println("cErrors: " + cErrors);
    }

    static class Writer extends Thread {

        public Writer() {
            setDaemon(true);
        }

        @Override
        public void run() {
            File outFile = new File("c:/temp/test.out");
            File outFileTmp = new File(filePath + "tmp");
            byte[] bytes = "test".getBytes(UTF_8);

            for( nWrite = 1; nWrite <= 100000; nWrite++ ) {
                if( (nWrite % 1000) == 0 )
                    System.out.println("nWrite: " + nWrite + ", cReads: " + nRead);

                try( FileOutputStream fos = new FileOutputStream(outFileTmp) ) {
                    fos.write(bytes);
                }
                catch( Exception e ) {
                    logException("write", e);
                }

                int maxAttemps = 10;
                for( int i = 0; i <= maxAttemps; i++ ) {
                    try {
                        Files.move(outFileTmp.toPath(), outFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
                        break;
                    }
                    catch( IOException e ) {
                        try {
                            Thread.sleep(1);
                        }
                        catch( InterruptedException ex ) {
                            break;
                        }
                        if( i == maxAttemps )
                            logException("move", e);
                    }
                }
            }

            System.out.println("Write finished ...");
            writeFinished = true;
        }
    }

    static class Reader extends Thread {

        public Reader() {
            setDaemon(true);
        }

        @Override
        public void run() {
            File inFile = new File(filePath);
            Path inPath = inFile.toPath();
            byte[] bytes = new byte[100];
            ByteBuffer buffer = ByteBuffer.allocateDirect(100);

            try { Thread.sleep(100); } catch( InterruptedException e ) { }

            for( nRead = 0; !writeFinished; nRead++ ) {
                if( useFileChannels ) {
                    try ( ByteChannel channel = Files.newByteChannel(inPath, Set.of()) ) {
                        channel.read(buffer);
                    }
                    catch( Exception e ) {
                        logException("read", e);
                    }
                }
                else {
                    try( InputStream fis = Files.newInputStream(inFile.toPath()) ) {
                        fis.read(bytes);
                    }
                    catch( Exception e ) {
                        logException("read", e);
                    }
                }
            }
        }
    }

    private static void logException(String action, Exception e) {
        cErrors++;
        System.err.printf(US, "%s: %s - wr=%s, rd=%s:, %s%n", cErrors, action, nWrite, nRead, e);
    }
}
于 2021-11-02T21:53:53.103 回答