随机访问文件FileLock:java.io vs. java.nio

8
我注意到 java.iojava.nio 中的随机访问文件实现在处理 FileLocks 方面略有不同。
看起来(在 Windows 上),java.io 给你一个强制性文件锁,而 java.nio 在请求时给你一个建议性文件锁。强制性文件锁意味着锁适用于所有进程,而建议性锁适用于遵循相同锁定协议的良好行为进程。
如果我运行以下示例,则可以手动删除 *.nio 文件,而 *.io 文件则无法被删除。
import java.io.*;
import java.lang.management.ManagementFactory;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;

public class NioIoLock {

    public static void main(String[] args) throws IOException, InterruptedException {
        String workDir = System.getProperty("user.dir");

        FileChannel channelIo, channelNio;
        FileLock lockIo, lockNio;

        // use io
        {
            String fileName = workDir
                    + File.separator
                    + ManagementFactory.getRuntimeMXBean().getName()
                    + ".io";
            File lockFile = new File(fileName);
            lockFile.deleteOnExit();
            RandomAccessFile file = new RandomAccessFile(lockFile, "rw");            

            channelIo = file.getChannel();
            lockIo = channelIo.tryLock();
            if (lockIo != null) {                   
                channelIo.write(ByteBuffer.wrap("foobar".getBytes("UTF-8")));
            }

        }

        // use nio
        {
            Path workDirPath = Paths.get(workDir);
            Path file = workDirPath.resolve(
                    Paths.get(ManagementFactory.getRuntimeMXBean().getName() + ".nio"));

            // open/create test file
            channelNio = FileChannel.open(
                    file, StandardOpenOption.READ, StandardOpenOption.WRITE,
                    StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE);

            // lock file
            lockNio = channelNio.tryLock();
            if (lockNio != null) {
                channelNio.write(ByteBuffer.wrap("foobar".getBytes("UTF-8")));
            }

        }

        // do not release locks for some time
        Thread.sleep(10000);

        // release io lock and channel
        if (lockIo != null && lockIo.isValid()) {
            lockIo.release();
        }
        channelIo.close();

        // release nio lock and channel
        if (lockNio != null && lockNio.isValid()) {
            lockNio.release();
        }
        channelNio.close();
    }

}

这是有原因的吗?这两者是否被视为替代品,还是它们将无限期共存?


SYNC, DSYNC 添加到nio版本中是否会有所不同?那就需要考虑性能问题了。 - Joop Eggen
添加 SYNC, DSYNC 没有任何区别。 - starikoff
1个回答

8
TL;DR: 这不是关于锁的问题,而是文件打开方式的问题。在 Windows 2000 之前,io 中看到的是 Windows 最好的表现,即使只读取文件,也无法删除该文件。而在 nio 中看到的是一种改进,它使用了自 Windows 2000 以来引入的新功能,但如果您选择,仍然可以保留旧的行为。决定不将该功能移植到 io 中。

完整故事: 我删除了所有与锁定相关的代码(见下文),以及写入文件的代码,它的工作方式与您的代码完全相同;但是,我还发现,如果在打开“nio”通道时指定 ExtendedOpenOption.NOSHARE_DELETE,则尝试删除任何两个文件时的行为相同(请取消代码中的注释并尝试)。还发现了this question,其中有一些解释,不确定是否有帮助。

import java.io.*;
import java.lang.management.ManagementFactory;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;

public class NioIoLock {

    public static void main(String[] args)
            throws IOException, InterruptedException {
        String workDir = System.getProperty("user.dir");

        FileChannel channelIo, channelNio;
        FileLock lockIo, lockNio;

        // use io
        {
            String fileName = workDir + File.separator
                + ManagementFactory.getRuntimeMXBean().getName() + ".io";
            File lockFile = new File(fileName);
            lockFile.deleteOnExit();
            RandomAccessFile file = new RandomAccessFile(lockFile, "rw");
            channelIo = file.getChannel();
        }

        // use nio
        {
            Path workDirPath = Paths.get(workDir);
            Path file = workDirPath.resolve(Paths
                .get(ManagementFactory.getRuntimeMXBean().getName() + ".nio"));

            // open/create test file
            channelNio = FileChannel.open(file, StandardOpenOption.READ,
                StandardOpenOption.WRITE, StandardOpenOption.CREATE,
                StandardOpenOption.DELETE_ON_CLOSE
                /*, com.sun.nio.file.ExtendedOpenOption.NOSHARE_DELETE*/);
        }

        // do not release locks for some time
        Thread.sleep(10000);
    }
}
更新1: 实际上,我仍然不知道其中的理由,但机制是清楚的:RandomAccessFile 构造函数最终调用来自 io_util_md.c 的 method winFileHandleOpen 的以下本地代码:
FD
winFileHandleOpen(JNIEnv *env, jstring path, int flags)
{
    ...
    const DWORD sharing =
        FILE_SHARE_READ | FILE_SHARE_WRITE;
    ... // "sharing" not updated anymore
    h = CreateFileW(
        pathbuf,            /* Wide char path name */
        access,             /* Read and/or write permission */
        sharing,            /* File sharing flags */
        NULL,               /* Security attributes */
        disposition,        /* creation disposition */
        flagsAndAttributes, /* flags and attributes */
        NULL);
    ...
}

所以,未设置FILE_SHARE_DELETE标志,你无法设置它。另一方面,调用java.nio.channels.FileChannel.open(Path, OpenOption...) eventually会考虑到此标志:
    private static FileDescriptor open(String pathForWindows,
                                       String pathToCheck,
                                       Flags flags,
                                       long pSecurityDescriptor)
        throws WindowsException
    {
        ...
        int dwShareMode = 0;
        if (flags.shareRead)
            dwShareMode |= FILE_SHARE_READ;
        if (flags.shareWrite)
            dwShareMode |= FILE_SHARE_WRITE;
        if (flags.shareDelete)
            dwShareMode |= FILE_SHARE_DELETE;
        ...
        // open file
        long handle = CreateFile(pathForWindows,
                                 dwDesiredAccess,
                                 dwShareMode,
                                 pSecurityDescriptor,
                                 dwCreationDisposition,
                                 dwFlagsAndAttributes);
        ...
    }

更新2:来自JDK-6607535

更改共享模式的建议是RFE 6357433。这样做可能很好,但会破坏现有应用程序(因为当前行为存在于jdk1.0以来)。在RandomAccessFile中添加特殊模式来解决Windows问题也可能不是正确的方法。此外,有一些建议称,这可以帮助解决具有文件映射的删除文件的问题。这并不是这样的,因为文件映射仍然会防止文件被删除。无论如何,对于jdk7,我们正在开发一个新的文件系统API,它将解决这里的某些要求。Windows实现做得很好,因此可以删除打开的文件。

另请参见从那里引用的JDK-6357433

一位客户在java.net论坛上指出,使用FileInputStream在Windows上打开文件会导致其他进程无法删除该文件。根据目前代码的编写方式,这是预期行为,因为在文件打开时指定了FILE_SHARE_READ和FILE_SHARE_WRITE共享标志。请参见io_util_md.c、fileOpen。然而,Windows 2000提供了一个新的标志FILE_SHARE_DELETE,允许在文件仍然打开时另一个进程删除该文件。
解决方法:对于jdk7,解决方法是使用新的文件系统API。因此,不要使用new FileInputStream(f)和new FileOutputStream(f),而应使用f.toPath().newInputStream()和f.toPath().newOutputStream()。

1
很棒的分析。我甚至没有考虑过锁不会导致这种情况。 - predi

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