10.12 Locking Versus Synchronization
The final topic I want to address is the
new Lock interface that Tiger provides, along with its companion interface, Condition. If you're happy using the synchronized keyword, then this section might not really interest you. However, if you find synchronized limiting, using Lock might solve your problems.
10.12.1 How do I do that?
Tiger introduces the java.util.concurrent.locks package to add more
flexible and extensive locking than available with the synchronized keyword.
At its simplest, the Lock class can be made to emulate a
synchronized block by calling lock( ), and then unlock( ) when done. However, it's in going beyond these basics that things get interesting.Lock provides a lockInterruptibly( ) method, which obtains a lock but
allows for interruptionsthis is something a synchronized block can't
offer. There are also tryLock( ) methods that attempt to get a lock, but
will not wait (or will wait for a specified duration)another feature not
available through use of a synchronized block. If a thread is waiting for a
lock on a synchronized method or code block, it will happily (and quietly)
wait forever.To add to the fun, the java.util.concurrent.locks.Condition interface
provides for multiple wait-sets per object. This allows conditions to keep
threads waiting, and for releasing threads from a wait state based on
specific (and even multiple) conditions. So a thread waiting to write, and
only then if a value is changed, can be handled differently than a thread
that is waiting to write without needing a value to be changed.NOTECan you tell David Flanagan helped out a lot on this chapter? Thanks,
David!Example 10-7 (borrowed from Java in a Nutshell, Fifth Edition (O'Reilly)) demonstrates the most common use of explicit locking, something called hand-over-hand locking. In
this scenario, a linked list is used: a lock is
obtained on one node, and then the next node, and traversed one node
at a time. However, at each stage, the prior node is released, so only two
nodes (at most) in the list are ever locked at once. There's simply no way
to simulate this functionality without explicit locking.NOTEAs David points out in Java in a Nutshell, this is a pretty useless list,
functionally, although it's a great example.
Example 10-7. Contrived linked list example
NOTEIf you can use synchronized blocks or methods instead of an explicit Lock
package com.oreilly.tiger.ch10;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LinkList<E> {
// The value of this node
E value;
// The rest of the list
LinkList<E> rest;
// A lock for this node
Lock lock;
// Signals when the value of this node changes
Condition valueChanged;
// Signals when the node this is connected to changes
Condition linkChanged;
public LinkList(E value) {
this.value = value;
rest = null;
lock = new ReentrantLock( );
valueChanged = lock.newCondition( );
linkChanged = lock.newCondition( );
}
public void setValue(E value) {
lock.lock( );
try {
this.value = value;
// Let waiting threads that the value has changed
valueChanged.signalAll( );
} finally {
lock.unlock( );
}
}
public void executeOnValue(E desiredValue, Runnable task)
throws InterruptedException {
lock.lock( );
try {
// Checks the value against the desired value
while (!value.equals(desiredValue)) {
// This will wait until the value changes
valueChanged.await( );
}
// When we get here, the value is correct -- Run the task
task.run( );
} finally {
lock.unlock( );
}
}
public void append(E value) {
// Start the pointer at this node
LinkList<E> node = this;
node.lock.lock( );
while (node.rest != null) {
LinkList<E> next = node.rest;
// Here's the hand-over-hand locking
try {
// Lock the next node
next.lock.lock( );
} finally {
// unlock the current node
node.lock.unlock( );
}
// Traverse
node = next;
}
// We're at the final node, so append and then unlock
try {
node.rest = new LinkList<E>(value);
// Let any waiting threads know that this node's link has changed
node.linkChanged.signalAll( );
} finally {
node.lock.unlock( );
}
}
public void printUntilInterrupted(String prefix) {
// Start the pointer at this node
LinkList<E> node = this;
node.lock.lock( );
while (true) {
LinkList<E> next;
try {
System.out.println(prefix + ": " + node.value);
// Wait for the next node if not available
while (node.rest == null) {
node.linkChanged.await( );
}
// Get the next node
next = node.rest;
// Lock it - more hand-to-hand locking
next.lock.lock( );
} catch (InterruptedException e) {
// reset the interrupt status
Thread.currentThread( ).interrupt( );
return;
} finally {
node.lock.unlock( );
}
// Traverse
node = next;
}
}
}
object, you don't have to worry about unlocking; Java takes care
of it for you.Take special notice that you never see a call to lock( ) without an immediate try/finally bock in which unlock( ) is called. This is something you need to lock (no pun intended) into your own headotherwise you'll
eventually make a mistake somewhere, and leave an object infinitely
locked.The rest of the code turns out to be pretty straightforward. Walk through
it slowly, and I trust you'll have a good overview of both the Lock and
Condition interface.
10.12.2 What about...
...other types of locks? Tiger provides ReentrantLock (used in this code),
which most closely approximates a synchronized block, albeit with the
extra features of a Lock. Tiger also defines a ReadWriteLock, which maintains a separate lock for reading than for writing. Multiple threads may
hold the read lock, as reading is typically a safe concurrent operation,
but only one thread may hold the write lock. Implementations of this
class (such as ReentrantReadWriteLock) are best used for large sets of
data, where reading happens often and writing occurs for small sections
of data.