Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

When reading or writing files, we need to make sure proper file-locking mechanisms are in place. This ensures data integrity in concurrent I/O based applications.

In this tutorial, we'll take a look at various approaches to achieve this using the Java NIO library.

2. Introduction to File Locks

In general, there are two types of locks:

    • Exclusive locks — also known as write locks
    • Shared locks — also referred to as read locks

Put simply, an exclusive lock prevents all other operations – including reads – while a write operation completes.

In contrast, a shared lock allows more than one process to read at the same time. The point of a read lock is to prevent the acquisition of a write lock by another process. Typically, a file in a consistent state should indeed be readable by any process.

In the next section, we'll see how Java handles these types of locks.

3. File Locks in Java

The Java NIO library enables locking files at the OS level. The lock() and tryLock() methods of a FileChannel are for that purpose.

We can create a FileChannel through either a FileInputStream, a FileOutputStream, or a RandomAccessFile. All three have a getChannel() method that returns a FileChannel.

Alternatively, we can create a FileChannel directly via the static open method:

try (FileChannel channel = FileChannel.open(path, openOptions)) {
  // write to the channel
}

Next, we'll review different options for getting exclusive and shared locks in Java. To learn more about file channels, check out our Guide to Java FileChannel tutorial.

4. Exclusive Locks

As we've already learned, while writing to a file, we can prevent other processes from reading or writing to it by using an exclusive lock.

We get exclusive locks by calling lock() or tryLock() on the FileChannel class. We can also use their overloaded methods:

  • lock(long position, long size, boolean shared)
  • tryLock(long position, long size, boolean shared)

In those cases, the shared parameter has to be set to false.

To get an exclusive lock, we must use a writable FileChannel. We can create it through the getChannel() methods of a FileOutputStream or a RandomAccessFile. Alternatively, as previously mentioned, we can use the static open method of the FileChannel class.  All we need is to set the second argument to StandardOpenOption.APPEND:

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.APPEND)) { 
    // write to channel
}

4.1. Exclusive Locks Using a FileOutputStream

FileChannel created from a FileOutputStream is writable. We can, therefore, acquire an exclusive lock:

try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/testfile.txt");
     FileChannel channel = fileOutputStream.getChannel();
     FileLock lock = channel.lock()) { 
    // write to the channel
}

Here, channel.lock() will either block until it obtains a lock, or it will throw an exception. For example, if the region specified is already locked, an OverlappingFileLockException is thrown. See the Javadoc for a complete list of possible exceptions.

We can also perform a non-blocking lock using channel.tryLock(). If it fails to get a lock because another program holds an overlapping one, then it returns null. If it fails to do so for any other reason, then an appropriate exception is thrown.

4.2. Exclusive Locks Using a RandomAccessFile

With a RandomAccessFile, we need to set flags on the second parameter of the constructor.

Here, we're going to open the file with read and write permissions:

try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "rw");
      FileChannel channel = file.getChannel();
      FileLock lock = channel.lock()) {
    // write to the channel
}

If we open the file in read-only mode and try to write to its channel it will throw a NonWritableChannelException.

4.3. Exclusive Locks Require a Writable FileChannel

As mentioned before, exclusive locks need a writable channel. Therefore, we can’t get an exclusive lock through a FileChannel created from a FileInputStream:

Path path = Files.createTempFile("foo","txt");
Logger log = LoggerFactory.getLogger(this.getClass());
try (FileInputStream fis = new FileInputStream(path.toFile()); 
    FileLock lock = fis.getChannel().lock()) {
    // unreachable code
} catch (NonWritableChannelException e) {
    // handle exception
}

In the example above, the lock() method will throw a NonWritableChannelException. Indeed, this is because we're invoking getChannel on a FileInputStream, which creates a read-only channel.

This example is just to demonstrate that we can't write to a non-writable channel. In a real-world scenario, we wouldn't catch and rethrow the exception.

5. Shared Locks

Remember, shared locks are also called read locks. Hence, to get a read lock, we must use a readable FileChannel.

Such a FileChannel can be obtained by calling the getChannel() method on a FileInputStream or a RandomAccessFile. Again, another option is to use the static open method of the FileChannel class. In that case, we set the second argument to StandardOpenOption.READ:

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
    FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
    // read from the channel
}

One thing to note here is that we chose to lock the entire file by calling lock(0, Long.MAX_VALUE, true). We could also have locked only a specific region of the file by changing the first two parameters to different values. The third parameter has to be set to true in the case of a shared lock.

To keep things simple, we’ll be locking the whole file in all the examples below, but keep in mind we can always lock a specific region of a file.

5.1. Shared Locks Using a FileInputStream

FileChannel obtained from a FileInputStream is readable. We can, therefore, get a shared lock:

try (FileInputStream fileInputStream = new FileInputStream("/tmp/testfile.txt");
    FileChannel channel = fileInputStream.getChannel();
    FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
    // read from the channel
}

In the snippet above, the call to lock() on the channel will succeed. That's because a shared lock only requires that the channel is readable. It's the case here since we created it from a FileInputStream.

5.2. Shared Locks Using a RandomAccessFile

This time, we can open the file with just read permissions:

try (RandomAccessFile file = new RandomAccessFile("/tmp/testfile.txt", "r"); 
     FileChannel channel = file.getChannel();
     FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
     // read from the channel
}

In this example, we created a RandomAccessFile with read permissions. We can create a readable channel from it and, thus, create a shared lock.

5.3. Shared Locks Require a Readable FileChannel

For that reason, we can't acquire a shared lock through a FileChannel created from a FileOutputStream:

Path path = Files.createTempFile("foo","txt");
try (FileOutputStream fis = new FileOutputStream(path.toFile()); 
    FileLock lock = fis.getChannel().lock(0, Long.MAX_VALUE, true)) {
    // unreachable code
} catch (NonWritableChannelException e) { 
    // handle exception
}

In this example, the call to lock() tries to get a shared lock on a channel created from a FileOutputStream. Such a channel is write-only. It doesn't fulfil the need that the channel has to be readable. This will trigger a NonWritableChannelException.

Again, this snippet is just to demonstrate that we can't read from a non-readable channel.

6. Things to Consider

In practice, using file locks is difficult; the locking mechanisms aren’t portable. We’ll need to craft our locking logic with this in mind.

In POSIX systems, locks are advisory. Different processes reading or writing to a given file must agree on a locking protocol. This will ensure the file’s integrity. The OS itself won’t enforce any locking.

On Windows, locks will be exclusive unless sharing is allowed. Discussing the benefits or drawbacks of OS-specific mechanisms is outside the scope of this article. Yet, it's important to know these nuances when implementing a locking mechanism.

7. Conclusion

In this tutorial, we've reviewed several different options for obtaining file locks in Java.

First, we started by understanding the two main locking mechanisms and how the Java NIO library facilitates locking files. Then we walked through a series of simple examples showing we can obtain exclusive and shared locks in our applications. We also took a look at the types of typical exceptions we might encounter when working with file locks.

As always, the source code for the examples is available over on GitHub.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!