Random access file FileLock: java.io vs. java.nio
Asked Answered
F

1

8

I've noticed that java.io and java.nio implementations of random access files differ slightly with respect to how FileLocks are handled.

It appears as though (on Windows) java.io gives you a mandatory file lock and java.nio gives you an advisory file lock upon requesting it respectively. A mandatory file lock means that the lock holds for all processes and an advisory holds for well behaving processes that follow the same locking protocol.

If I run the following example, I'm able to delete the *.nio file manually, while *.io file refuses to be deleted.

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();
    }

}

Is there a reason for this? Are these two even considered as alternatives or are they meant to coexist indefinitely?

Fiertz answered 2/9, 2016 at 13:5 Comment(2)
Would adding SYNC, DSYNC to the nio version make a difference? Then it would have been a performance consideration.Librate
adding SYNC, DSYNC makes no differenceEskridge
E
8

TL;DR: It's not about locks, it's about the way files are opened. What you see in io is the best Windows could do prior to Windows 2000, and it did that even when files were opened for reading only -- it wasn't possible to delete that file. What you see in nio is an improvement that uses a new capability introduced since Windows 2000, but you still can have your old behavior in nio if you choose to. It was decided not to port that capability into what io does.

Full story: I removed all the code related to locking (see below) as well as writing to the files and it works absolutely the same as your code does; however, I also found that if you specify the ExtendedOpenOption.NOSHARE_DELETE when opening the "nio" channel, then behavior when you try to delete any of the two files is the same (uncomment it in the code and try). Also found this question and it has some explanation, not sure if it will help.

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);
    }
}

Update 1: Actually, I still don't know the rationale behind this, but the mechanics are clear: RandomAccessFile constructor eventually calls the following native code from method winFileHandleOpen from io_util_md.c:

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);
    ...
}

So, FILE_SHARE_DELETE flag is not set and there's nothing you can do to set it. On the other hand, calling java.nio.channels.FileChannel.open(Path, OpenOption...) eventually takes this flag into account:

    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);
        ...
    }

Update 2: From JDK-6607535:

The suggestion to change the sharing mode is RFE 6357433. It would be great to do that but it may break existing applications (since the current behavior has existed since jdk1.0). Adding a special mode to RandomAccessFile to work around a Windows issues is probably not the right approach either. Also, there is some suggestion that this can help with the issue of deleting files that have a file mapping. This is not so, as a file mapping still prevents a file from being deleted. Anyway, for jdk7 we are working on a new file system API that will address some of the requirements here. The Windows implementation does the right thing so that opened files can be deleted.

Also see JDK-6357433 referenced from there:

A customer has pointed out on the java.net forums that opening a file on Windows using a FileInputStream causes other processes to be unable to delete the file. This is intended behavior as the code is currently written, as the sharing flags specified during the opening of the file are FILE_SHARE_READ and FILE_SHARE_WRITE. See io_util_md.c, fileOpen. However, Windows 2000 supplies a new flag, FILE_SHARE_DELETE, which allows another process to delete the file while it is still open.

and finally

WORK AROUND

For jdk7, the workaround is to use new file system API. So instead of new FileInputStream(f) and new FileOutputStream(f), use f.toPath().newInputStream() and f.toPath().newOutputStream().

Eskridge answered 2/9, 2016 at 18:26 Comment(1)
Great analysis. I never even considered locks not causing this.Fiertz

© 2022 - 2024 — McMap. All rights reserved.