Chapter 7. Locking and multi-threaded access

Table of Contents

Locks
Locking strategies
Lock commands
Custom locks
Interrupting threads
Troubleshooting

EntityFS entities are designed to be used concurrently by several threads. To avoid simultaneous, conflicting operations by different threads, entities can be locked for exclusive access by the thread holding the lock.

Locking is supported by read/write file system, but by default locking is disabled. It can be enabled by calling enableLocking on the FileSystemBuilder.

Locks are either read/write locks (the same semantics as Java's ReadWriteLock) or single locks (the same semantics as Java's synchronized keyword). Locks can also be fair (first come, first served for threads wanting to lock the entity, but with a performance penalty), or non-fair. All lock types are reentrant, so the same lock can safely be locked several times by the thread that owns it.

Every file system uses one lock type and one lock acquiring strategy for all of its entities. The type of locks is determined by the file system's LockAdapterFactory.


How a file system's locks are acquired is determined by its LockAcquiringStrategy:


Entity locks, regardless of their type, implement the EntityLock interface, which is an extension of Java's Lock interface. An EntityLock is aware of the Lockable object that it protects.

The EntityLocks utility class contains static utility methods for working with entity locks.

Entity locks should always be released in a finally clause of a try-finally block. In this way, the locks are always release even in the case of an error happening in the try block.

Example 7.1. Using try-finally for locks

// d is a directory
EntityLock rl = d.lockForReading();
try
{
  d.listEntities();
}
finally
{
  // This will always be called.
  rl.unlock();
}

Example 7.2. Releasing several locks in finally

Collection<EntityLock> locks = new ArrayList<EntityLock>();
try
{
  // d1 and d2 are directories
  locks.add(d1.lockForReading());
  locks.add(d2.lockForWriting());
  
  // do stuff...
}
finally
{
  EntityLocks.unlockAll(locks);
}

To avoid deadlocks, every application must have a strategy for in which order entity locks are locked. (This is another strategy than the LockAcquiringStrategy discussed above.) For a discussion on locking strategies, see for instance the book Java concurrency in practice. Entities do not assume any particular locking strategy. Utility classes, however, use a strategy where:

  1. Parent entities are locked before child entities.

  2. Entities in the same directory are locked in hash code order.

LockCommand:s are command objects for locking entities. Several commands may be run by calling the LockCommandExecutor's execute method. The executor makes sure that locks are acquired in a sequence that follows a the lock acquiring strategy that it implements.


A locking FileSystem has its own LockCommandExecutor instance that clients may use to execute LockCommand:s in an order that is compliant with that file system's locking strategy. The default implementation, LockCommandExecutorImpl, implements the utility classes' locking strategy that were described above.

The example below shows how a LockCommandExecutor can be used to lock all locks necessary for moving and then writing to an entity when using an AccessController that require locked entities. (See Chapter 13, Access controls.)

Example 7.3. Using a LockCommandExecutor

// Move the file f1 from directory d1 to directory d2 and append the text
// "moved" to it.
FileSystem fs = f1.getFileSystem();
AccessController ac = fs.getAccessController();
Collection<LockCommand> cmds = 
  new ArrayList<LockCommand>();

// Add the lock commands that are needed for moving the entity.
// Keep this lock command for future reference
LockCommand f1lc = new LockForWritingCommand(f1);
cmds.add(f1lc);
cmds.add(new LockForWritingCommand(d1));
cmds.add(new LockForWritingCommand(d2));

// Add the lock commands that are needed for performing access controls.
// Keep these lock commands for future reference
Collection<? extends LockCommand> f1acmds =
  ac.getLockCommandsForAccessControl(f1);
cmds.addAll(f1acmds);
cmds.addAll(ac.getLockCommandsForAccessControl(d1));
cmds.addAll(ac.getLockCommandsForAccessControl(d2));

// Lock all locks
LockCollection lc = fs.getLockCommandExecutor().execute(cmds);
try
{
  // Move the entity
  f1.rename(d2, f1.getName());
  
  // Keep all locks required for appending text to the file in a new lock 
  // collection.
  LockCollection lc2 = new LockCollection();
  try
  {
    // The write lock on f1
    lc2.addLock(f1lc, lc.removeLock(f1lc));
    // The locks required for access controls on f1
    lc2.addLocks(lc.removeLocks(f1acmds));
    
    // Unlock all other locks
    lc.unlockAll();
    
    Writer w = new OutputStreamWriter(
      f1.openForAppend(),
      Charset.forName("utf8"));
    try
    {
      w.write("moved");
    }
    finally
    {
      w.close();
    }
  }
  finally
  {
    lc2.unlockAll();
  }
}
finally
{
  // It is safe to call unlockAll several times an a LockCollection
  lc.unlockAll();
}

An application can create new locks for an entity in addition to the standard read and write locks by calling its createLock method. This can be used as an alternative to using synchronized or any other locking method and it makes it possible for the application to use the same LockAcquiringStrategy for its own locks as for EntityFS locks.

When creating new locks for an entity, give careful thought about how the new locks relate to the standard read and write locks. For instance, an application may require that a thread must hold at least a read lock on the entity before it lets it lock the custom lock. Without a good strategy, deadlocks may occur.

The EntityFS methods that can be interrupted throw the unchecked exception UncheckedInterruptedException instead of InterruptedException. UncheckedInterruptedException:s should be handled in the same way as InterruptedException:s. See, for instance, Java concurrency in practice.

Verbose information about lock conflicts for read/write locks is written to stdout if the entityfs.locking.debug system property is set.