Stormcloud
Stormcloud

Reputation: 2307

Java: How to hold off read/writing a file while it's locked

I have a number of separate applications (all written in Java) that need to access a file for short periods of time. Some of these processes will need read access while others will need to update the file. So that all of these applications play nicely I want to write some code that will use OS locks to gain exclusive access to the file for the time that each application requires it.

The obvious way to do this RandomAccessFile myFile = new RandomAccessFile(file, "rw"), however this will fail if another process already has the lock. What I need is the ability to back off and try again.

I was hoping to write some code that uses channel.tryLock() to test if a lock has been taken out. The trouble is I need a channel, and I don't appear able to get that channel object with out taking out a lock!

Update

I need to find a way of checking to see if there is a lock on a file. I want to do this without throwing an exception.

A simplified version of my code is:

void myMethod(File myFile) {
    try (
        RandomAccessFile myFile = new RandomAccessFile(myFile, "rw");  // Exception here
        FileChannel myChannel = myFile.getChannel();
        FileLock myLock = lockFile(myChannel )
    ) {
        // Stuff to read and/or write the file
    }
}

private FileLock lockFile(FileChannel channel) throws Exception {
    FileLock lock;

    while (lock = channel.tryLock() == null) {
        Thread.sleep(100);
    }

    return lock;
}

the problem is that if the file is locked it fails (by throwing an exception) on the highlighted line - before the point that can get a lock for the file.

Other variations for obtaining the channel such as FileChannel channel = FileChannel.open(myFile.toPath(), StandardOpenOption.WRITE, StandardOpenOption.APPEND) also throw an exception:

Exception in thread "main" java.nio.file.FileSystemException: path\to\my\file.txt: The process cannot access the file because it is being used by another process.

So how can I get a channel to test the lock with with out throwing an exception?

Upvotes: 1

Views: 1972

Answers (1)

Holger
Holger

Reputation: 298539

The standard way to use FileLock, is to open the file, e.g. via FileChannel.open, followed by tryLock. The presence of a lock does not prevent other processes from opening the file.

This can be demonstrated by the following program:

import java.io.IOException;
import java.nio.channels.*;
import java.nio.file.*;

class Locking {
    public static void main(String[] args) throws IOException, InterruptedException {
        if(args.length > 0) {
            String me = String.format("%6s ", ProcessHandle.current());
            Path p = Paths.get(args[0]);
            try(FileChannel fc = FileChannel.open(p,
                    StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {

                FileLock l = fc.tryLock();
                if(l == null) System.out.println(me + "could not acquire lock");
                else {
                    System.out.println(me + "got lock");
                    Thread.sleep(3000);
                    System.out.println(me + "releasing lock");
                    l.release();
                }
            }
        }
        else {
            Path p = Files.createTempFile("lock", "test");
            String[] command = {
                Paths.get(System.getProperty("java.home"), "bin", "java").toString(),
                "-cp", System.getProperty("java.class.path"),
                "Locking", p.toString()
            };
            ProcessBuilder b = new ProcessBuilder(command).inheritIO();
            Process p1 = b.start(), p2 = b.start(), p3 = b.start();
            p1.waitFor();
            p2.waitFor();
            p3.waitFor();
            Files.delete(p);
        }
    }
}

which prints something alike

 12116 got lock
 13948 could not acquire lock
 13384 could not acquire lock
 12116 releasing lock

which can be demonstrated online on tio.run

While this program works the same under Windows, this operating system supports opening files unshared, preventing other processes from opening. If a different process has opened the file in that way, we can’t even open it to probe the locking state.

This is not the way, Java opens the file, however, there’s a non-standard open option to replicate the behavior, com.sun.nio.file.ExtendedOpenOption.NOSHARE_WRITE. In recent JDKs, it’s in the jdk.unsupported module.

When we run the following extended test program under Windows

import java.io.IOException;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.HashSet;
import java.util.Set;

class LockingWindows {
    public static void main(String[] args) throws IOException, InterruptedException {
        if(args.length > 0) {
            String me = String.format("%6s ", ProcessHandle.current());
            Path p = Paths.get(args[0]);
            Set<OpenOption> options
                = Set.of(StandardOpenOption.WRITE, StandardOpenOption.APPEND);
            if(Boolean.parseBoolean(args[1])) options = addExclusive(options);
            try(FileChannel fc = FileChannel.open(p, options)) {
                FileLock l = fc.tryLock();
                if(l == null) System.out.println(me + "could not acquire lock");
                else {
                    System.out.println(me + "got lock");
                    Thread.sleep(3000);
                    System.out.println(me + "releasing lock");
                    l.release();
                }
            }
        }
        else {
            Path p = Files.createTempFile("lock", "test");
            String[] command = {
                Paths.get(System.getProperty("java.home"), "bin", "java").toString(),
                "-cp", System.getProperty("java.class.path"),
                "LockingWindows", p.toString(), "false"
            };
            ProcessBuilder b = new ProcessBuilder(command).inheritIO();
            for(int run = 0; run < 2; run++) {
                Process p1 = b.start(), p2 = b.start(), p3 = b.start();
                p1.waitFor();
                p2.waitFor();
                p3.waitFor();
                if(run == 0) {
                    command[command.length - 1] = "true";
                    b.command(command);
                    System.out.println("\nNow with exclusive mode");
                }
            }
            Files.delete(p);
        }
    }

    private static Set<OpenOption> addExclusive(Set<OpenOption> options) {
        OpenOption o;
        try {
            o = (OpenOption) Class.forName("com.sun.nio.file.ExtendedOpenOption")
                .getField("NOSHARE_WRITE").get(null);
            options = new HashSet<>(options);
            options.add(o);
        } catch(ReflectiveOperationException | ClassCastException ex) {
            System.err.println("opening exclusive not supported");
        }
        return options;
    }
}

we will get something like

  2356 got lock
  6412 could not acquire lock
  9824 could not acquire lock
  2356 releasing lock

Now with exclusive mode
  9160 got lock
Exception in thread "main" java.nio.file.FileSystemException: C:\Users\...\Temp\lock13936982436235244405test: The process cannot access the file because it is being used by another process
    at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:92)
    at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
    at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
    at java.base/sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:121)
    at java.base/java.nio.channels.FileChannel.open(FileChannel.java:298)
    at LockingWindows.main(LockingWindows.java:148)
Exception in thread "main" java.nio.file.FileSystemException: C:\Users\...\Temp\lock13936982436235244405test: The process cannot access the file because it is being used by another process
    at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:92)
    at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
    at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
    at java.base/sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:121)
    at java.base/java.nio.channels.FileChannel.open(FileChannel.java:298)
    at LockingWindows.main(LockingWindows.java:148)
  9160 releasing lock

The similarity to the outcome of your test suggests that the Windows program you ran concurrently to your Java program did use such a mode.

For your Java programs, no such issue should arise, as long as you don’t use that mode. Only when you have to interact with another Windows program not using the collaborative locking, you have to deal with this.

Upvotes: 4

Related Questions