Table of Contents
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.
Table 7.1. Lock types
Lock type | LockAdapterFactory |
---|---|
Fair read/write locks | FairReadWriteEntityLockAdapterFactory |
Fair single locks | FairSingleEntityLockAdapterFactory |
Non-fair read/write locks | NonFairReadWriteEntityLockAdapterFactory |
Non-fair single locks | NonFairSingleEntityLockAdapterFactory |
How a file system's locks are acquired is determined by its LockAcquiringStrategy:
Table 7.2. Lock acquiring strategies
Lock acquiring strategy | Description |
---|---|
SimpleLockAcquiringStrategy | Locks are acquired by calling their lock
method. Threads can wait indefinitely for a lock. Deadlocks will freeze the
affected threads. This is the default strategy. |
TimeoutLockAcquiringStrategy | Locks are acquired by calling their timed lock
method, which times out, generating a
LockTimeoutException, if a thread waits too long for a lock. |
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:
Parent entities are locked before child entities.
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.
Table 7.3. Lock commands
Lock command | Description |
---|---|
LockForReadingCommand | Lock an entity for reading. |
LockForWritingCommand | Lock an entity for writing. |
LockParentForReadingCommand | Lock the parent directory of an entity for reading. |
LockParentForWritingCommand | Lock the parent directory of an entity for writing. |
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.