CHAPTER 7

image

The Locking Framework

The java.util.concurrent.locks package provides a framework of interfaces and classes for locking and waiting for conditions in a manner that’s distinct from an object’s intrinsic lock-based synchronization and java.lang.Object’s wait/notification mechanism. The concurrency utilities include the Locking Framework that improves on intrinsic synchronization and wait/notification by offering lock polling, timed waits, and more.

SYNCHRONIZED AND LOW-LEVEL LOCKING

Java supports synchronization so that threads can safely update shared variables and ensure that a thread’s updates are visible to other threads. You leverage synchronization in your code by marking methods or code blocks with the synchronized keyword. These code sequences are known as critical sections. The Java virtual machine (JVM) supports synchronization via monitors and the monitorenter and monitorexit JVM instructions.

Every Java object is associated with a monitor, which is a mutual exclusion (letting only one thread at a time execute in a critical section) construct that prevents multiple threads from concurrently executing in a critical section. Before a thread can enter a critical section, it’s required to lock the monitor. If the monitor is already locked, the thread blocks until the monitor is unlocked (by another thread leaving the critical section).

When a thread locks a monitor in a multicore/multiprocessor environment, the values of shared variables that are stored in main memory are read into the copies of these variables that are stored in a thread’s working memory (also known as local memory or cache memory). This action ensures that the thread will work with the most recent values of these variables and not stale values, and is known as visibility. The thread proceeds to work with its copies of these shared variables. When the thread unlocks the monitor while leaving the critical section, the values in its copies of shared variables are written back to main memory, which lets the next thread that enters the critical section access the most recent values of these variables. (The volatile keyword addresses visibility only.)

The Locking Framework includes the often-used Lock, ReentrantLock, Condition, ReadWriteLock, and ReentrantReadWriteLock types, which I explore in this chapter. I also briefly introduce you to the StampedLock class, which was introduced by Java 8.

Lock

The Lock interface offers more extensive locking operations than can be obtained via the locks associated with monitors. For example, you can immediately back out of a lock-acquisition attempt when a lock isn’t available. This interface declares the following methods:

  • void lock(): Acquire the lock. When the lock isn’t available, the calling thread is forced to wait until it becomes available.
  • void lockInterruptibly(): Acquire the lock unless the calling thread is interrupted. When the lock isn’t available, the calling thread is forced to wait until it becomes available or the thread is interrupted, which results in this method throwing java.lang.InterruptedException.
  • Condition newCondition(): Return a new Condition instance that’s bound to this Lock instance. This method throws java.lang.UnsupportedOperationException when the Lock implementation class doesn’t support conditions.
  • boolean tryLock(): Acquire the lock when it’s available at the time this method is invoked. The method returns true when the lock is acquired and false when the lock isn’t acquired.
  • boolean tryLock(long time, TimeUnit unit): Acquire the lock when it’s available within the specified waiting time, measured in unit java.util.concurrent.TimeUnit units (seconds, milliseconds, and so on), and the calling thread isn’t interrupted. When the lock isn’t available, the calling thread is forced to wait until it becomes available within the waiting time or the thread is interrupted, which results in this method throwing InterruptedException. When the lock is acquired, true is returned; otherwise, false returns.
  • void unlock(): Release the lock.

Acquired locks must be released. In the context of synchronized methods and blocks and the implicit monitor lock associated with every object, all lock acquisition and release occurs in a block-structured manner. When multiple locks are acquired, they’re released in the opposite order and all locks are released in the same lexical scope in which they were acquired.

Lock acquisition and release in the context of Lock interface implementations can be more flexible. For example, some algorithms for traversing concurrently accessed data structures require the use of “hand-over-hand” or “chain locking”: you acquire the lock of node A, then node B, then release A and acquire C, then release B and acquire D, and so on. Implementations of the Lock interface enable the use of such techniques by allowing a lock to be acquired and released in different scopes, and by allowing multiple locks to be acquired and released in any order.

With this increased flexibility comes additional responsibility. The absence of block-structured locking removes the automatic release of locks that occurs with synchronized methods and blocks. As a result, you should typically employ the following idiom for lock acquisition and release:

Lock l = ...; // ... is a placeholder for code that obtains the lock
l.lock();
try
{
  // access the resource protected by this lock
}
catch (Exception ex)
{
  // restore invariants
}
finally
{
   l.unlock();
}

This idiom ensures that an acquired lock will always be released.

Image Note  All Lock implementations are required to enforce the same memory synchronization semantics as provided by the built-in monitor lock.

ReentrantLock

Lock is implemented by the ReentrantLock class, which describes a reentrant mutual exclusion lock. This lock is associated with a hold count. When a thread holds the lock and reacquires the lock by invoking lock(), lockUninterruptibly(), or one of the tryLock() methods, the hold count is increased by 1. When the thread invokes unlock(), the hold count is decremented by 1. The lock is released when this count reaches 0.

ReentrantLock offers the same concurrency and memory semantics as the implicit monitor lock that’s accessed via synchronized methods and blocks. However, it has extended capabilities and offers better performance under high thread contention (threads frequently asking to acquire a lock that’s already held by another thread). When many threads attempt to access a shared resource, the JVM spends less time scheduling these threads and more time executing them.

You initialize a ReentrantLock instance by invoking either of the following constructors:

  • ReentrantLock(): Create an instance of ReentrantLock. This constructor is equivalent to ReentrantLock(false).
  • ReentrantLock(boolean fair): Create an instance of ReentrantLock with the specified fairness policy. Pass true to fair when this lock should use a fair ordering policy: under contention, the lock would favor granting access to the longest-waiting thread.

ReentrantLock implements Lock’s methods. However, its implementation of unlock() throws java.lang.IllegalMonitorStateException when the calling thread doesn’t hold the lock. Also, ReentrantLock provides its own methods. For example, boolean isFair() returns the fairness policy and boolean isHeldByCurrentThread() returns true when the lock is held by the current thread. Listing 7-1 demonstrates ReentrantLock.

Listing 7-1 describes an application whose default main thread creates a pair of worker threads that enter, simulate working in, and leave critical sections. They use ReentrantLock’s lock() and unlock() methods to obtain and release a reentrant lock. When a thread calls lock() and the lock is unavailable, the thread is disabled (and cannot be scheduled) until the lock becomes available.

Compile Listing 7-1 as follows:

javac RLDemo.java

Run the resulting application as follows:

java RLDemo

You should discover output that’s similar to the following (message order may differ somewhat):

Thread ThdA entered critical section.
Thread ThdA performing work.
Thread ThdA finished working.
Thread ThdB entered critical section.
Thread ThdB performing work.
Thread ThdB finished working.

Condition

The Condition interface factors out Object’s wait and notification methods (wait(), notify(), and notifyAll()) into distinct condition objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary Lock implementations. Where Lock replaces synchronized methods and blocks, Condition replaces Object’s wait/notification methods.

Image Note  A Condition instance is intrinsically bound to a lock. To obtain a Condition instance for a certain Lock instance, use Lock’s newCondition() method.

Condition declares the following methods:

  • void await(): Force the calling thread to wait until it’s signaled or interrupted.
  • boolean await(long time, TimeUnit unit): Force the calling thread to wait until it’s signaled or interrupted, or until the specified waiting time elapses.
  • long awaitNanos(long nanosTimeout): Force the current thread to wait until it’s signaled or interrupted, or until the specified waiting time elapses.
  • void awaitUninterruptibly(): Force the current thread to wait until it’s signaled.
  • boolean awaitUntil(Date deadline): Force the current thread to wait until it’s signaled or interrupted, or until the specified deadline elapses.
  • void signal(): Wake up one waiting thread.
  • void signalAll(): Wake up all waiting threads.

Listing 7-2 revisits Chapter 3’s producer-consumer application (in Listing 3-2) to show you how it can be written to take advantage of conditions.

Listing 7-2 is similar to Listing 3-2’s PC application. However, it replaces synchronized and wait/notification with locks and conditions.

PC’s main() method instantiates the Shared, Producer, and Consumer classes. The Shared instance is passed to the Producer and Consumer constructors and these threads are then started.

The Producer and Consumer constructors are called on the default main thread. Because the Shared instance is also accessed by the producer and consumer threads, this instance must be visible to these threads (especially when these threads run on different cores). In each of Producer and Consumer, I accomplish this task by declaring s to be final. I could have declared this field to be volatile, but volatile suggests additional writes to the field and s shouldn’t be changed after being initialized.

Check out Shared’s constructor. Notice that it creates a lock via lock = new ReentrantLock();, and creates a condition associated with this lock via condition = lock.newCondition();. This lock is made available to the producer and consumer threads via the Lock getLock() method.

The producer thread invokes Shared’s void setSharedChar(char c) method to generate a new character and then outputs a message identifying the produced character. This method locks the previously created Lock object and enters a while loop that repeatedly tests variable available, which is true when a produced character is available for consumption.

While available is true, the producer invokes the condition’s await() method to wait for available to become false. The consumer signals the condition to wake up the producer when it has consumed the character. (I use a loop instead of an if statement because spurious wakeups are possible and available might still be true.)

After leaving its loop, the producer thread records the new character, assigns true to available to indicate that a new character is available for consumption, and signals the condition to wake up a waiting consumer. Lastly, it unlocks the lock and exits setSharedChar().

Image Note  I lock the setSharedChar()/System.out.println() block in Producer’s run() method and the getSharedChar()/System.out.println() block in Consumer’s run() method to prevent the application from outputting consuming messages before producing messages, even though characters are produced before they’re consumed.

The behavior of the consumer thread and getSharedChar() method is similar to what I’ve just described for the producer thread and setSharedChar() method.

Image Note  I didn’t use the try/finally idiom for ensuring that a lock is disposed of in Producer’s and Consumer’s run() methods because an exception isn’t thrown from this context.

Compile Listing 7-2 as follows:

javac PC.java

Run the resulting application as follows:

java PC

You should observe output that’s identical to the following prefix of the output, which indicates lockstep synchronization (the producer thread doesn’t produce an item until it’s consumed and the consumer thread doesn’t consume an item until it’s produced):

A produced by producer.
A consumed by consumer.
B produced by producer.
B consumed by consumer.
C produced by producer.
C consumed by consumer.
D produced by producer.
D consumed by consumer.

ReadWriteLock

Situations arise where data structures are read more often than they’re modified. For example, you might have created an online dictionary of word definitions that many threads will read concurrently, while a single thread might occasionally add new definitions or update existing definitions. The Locking Framework provides a read-write locking mechanism for these situations that yields greater concurrency when reading and the safety of exclusive access when writing. This mechanism is based on the ReadWriteLock interface.

ReadWriteLock maintains a pair of locks: one lock for read-only operations and one lock for write operations. The read lock may be held simultaneously by multiple reader threads as long as there are no writers. The write lock is exclusive: only a single thread can modify shared data. (The lock that’s associated with the synchronized keyword is also exclusive.)

ReadWriteLock declares the following methods:

  • Lock readLock(): Return the lock that’s used for reading.
  • Lock writeLock(): Return the lock that’s used for writing.

ReentrantReadWriteLock

ReadWriteLock is implemented by the ReentrantReadWriteLock class, which describes a reentrant read-write lock with similar semantics to ReentrantLock.

You initialize a ReentrantReadWriteLock instance by invoking either of the following constructors:

  • ReentrantReadWriteLock(): Create an instance of ReentrantReadWriteLock. This constructor is equivalent to ReentrantReadWriteLock(false).
  • ReentrantReadWriteLock(boolean fair): Create an instance of ReentrantReadWriteLock with the specified fairness policy. Pass true to fair when this lock should use a fair ordering policy.

Image Note  For the fair ordering policy, when the currently held lock is released, either the longest-waiting single writer thread will be assigned the write lock or, when there’s a group of reader threads waiting longer than all waiting writer threads, that group will be assigned the read lock.

A thread that tries to acquire a fair read lock (non-reentrantly) will block when the write lock is held or when there’s a waiting writer thread. The thread will not acquire the read lock until after the oldest currently waiting writer thread has acquired and released the write lock. If a waiting writer abandons its wait, leaving one or more reader threads as the longest waiters in the queue with the write lock free, those readers will be assigned the read lock.

A thread that tries to acquire a fair write lock (non-reentrantly) will block unless both the read lock and write lock are free (which implies no waiting threads). (The nonblocking tryLock() methods don’t honor this fair setting and will immediately acquire the lock if possible, regardless of waiting threads.)

After instantiating this class, you invoke the following methods to obtain the read and write locks:

  • ReentrantReadWriteLock.ReadLock readLock(): Return the lock used for reading.
  • ReentrantReadWriteLock.WriteLock writeLock(): Return the lock used for writing.

Each of the nested ReadLock and WriteLock classes implements the Lock interface and declares its own methods. Furthermore, ReentrantReadWriteLock declares additional methods such as the following pair:

  • int getReadHoldCount(): Return the number of reentrant read holds on this lock by the calling thread, which is 0 when the read lock isn’t held by the calling thread. A reader thread has a hold on a lock for each lock action that’s not matched by an unlock action.
  • int getWriteHoldCount(): Return the number of reentrant write holds on this lock by the calling thread, which is 0 when the write lock isn’t held by the calling thread. A writer thread has a hold on a lock for each lock action that’s not matched by an unlock action.

To demonstrate ReadWriteLock and ReentrantReadWriteLock, Listing 7-3 presents an application whose writer thread populates a dictionary of word/definition entries while a reader thread continually accesses entries at random and outputs them.

Listing 7-3’s default main thread first creates the words and definitions arrays of strings, which are declared final because they will be accessed from anonymous classes. After creating a map in which to store word/definition entries, it obtains a reentrant read/write lock and accesses the reader and writer locks.

A runnable for the writer thread is now created. Its run() method iterates over the words array. Each of the iterations locks the writer lock. When this method returns, the writer thread has the exclusive writer lock and can update the map. It does so by invoking the map’s put() method. After outputting a message to identify the added word, the writer thread releases the lock and sleeps one millisecond to give the appearance of performing other work. An executor based on a thread pool is obtained and used to invoke the writer thread’s runnable.

A runnable for the reader thread is subsequently created. Its run() method repeatedly obtains the read lock, accesses a random entry in the map, outputs this entry, and unlocks the read lock. An executor based on a thread pool is obtained and used to invoke the reader thread’s runnable.

Although I could have avoided the idiom for lock acquisition and release because an exception isn’t thrown, I specified try/finally for good form.

Compile Listing 7-3 as follows:

javac Dictionary.java

Run the resulting application as follows:

java Dictionary

You should observe output that’s similar to the following prefix of the output (the message order may differ somewhat) that I observed in one execution:

writer storing hypocalcemia entry
writer storing prolixity entry
reader accessing hypocalcemia: a deficiency of calcium in the blood entry
writer storing assiduous entry
reader accessing assiduous: showing great care, attention, and effort entry
reader accessing castellan: null entry
reader accessing hypocalcemia: a deficiency of calcium in the blood entry
reader accessing assiduous: showing great care, attention, and effort entry
reader accessing indefatigable: null entry
reader accessing hypocalcemia: a deficiency of calcium in the blood entry
reader accessing hypocalcemia: a deficiency of calcium in the blood entry
reader accessing assiduous: showing great care, attention, and effort entry
reader accessing indefatigable: null entry
reader accessing prolixity: unduly prolonged or drawn out entry
reader accessing hypocalcemia: a deficiency of calcium in the blood entry
reader accessing castellan: null entry
reader accessing assiduous: showing great care, attention, and effort entry
reader accessing hypocalcemia: a deficiency of calcium in the blood entry
reader accessing prolixity: unduly prolonged or drawn out entry
reader accessing assiduous: showing great care, attention, and effort entry
reader accessing castellan: null entry
reader accessing hypocalcemia: a deficiency of calcium in the blood entry
reader accessing indefatigable: null entry
reader accessing castellan: null entry
reader accessing prolixity: unduly prolonged or drawn out entry
reader accessing hypocalcemia: a deficiency of calcium in the blood entry
writer storing indefatigable entry
reader accessing assiduous: showing great care, attention, and effort entry
reader accessing assiduous: showing great care, attention, and effort entry

Image Note  Java 8 added StampedLock to the java.util.concurrent.locks package. According to its JDK 8 documentation, StampedLock is a capability-based lock with three modes for controlling read/write access. It differentiates between exclusive and nonexclusive locks in a manner that’s similar to ReentrantReadWriteLock, but also allows for optimistic reads, which ReentrantReadWriteLock doesn’t support. Check out Dr. Heinz Kabutz’s Phaser and StampedLock Concurrency Synchronizers video presentation (www.parleys.com/tutorial/5148922b0364bc17fc56ca4f/chapter0/about) to learn about StampedLock. Also, see this presentation’s PDF file (www.jfokus.se/jfokus13/preso/jf13_PhaserAndStampedLock.pdf).

EXERCISES

The following exercises are designed to test your understanding of Chapter 7’s content:

  1. Define lock.
  2. What is the biggest advantage that Lock objects hold over the intrinsic locks that are obtained when threads enter critical sections (controlled via the synchronized reserved word)?
  3. True or false: ReentrantLock’s unlock() method throws IllegalMonitorStateException when the calling thread doesn’t hold the lock.
  4. How do you obtain a Condition instance for use with a particular Lock instance?
  5. True or false: ReentrantReadWriteLock() creates an instance of ReentrantReadWriteLock with a fair ordering policy.
  6. Define StampedLock.
  7. The java.util.concurrent.locks package includes a LockSupport class. What is the purpose of LockSupport?
  8. Replace the following ID class with an equivalent class that uses ReentrantLock in place of synchronized:
    public class ID
    {
       private static int counter; // initialized to 0 by default

       public static synchronized int getID()
       {
          int temp = counter + 1;
          try
          {
             Thread.sleep(1);
          }
          catch (InterruptedException ie)
          {
          }
          return counter = temp;
       }
    }

Summary

The java.util.concurrent.locks package provides a framework of interfaces and classes for locking and waiting for conditions in a manner that’s distinct from an object’s intrinsic lock-based synchronization and Object’s wait/notification mechanism. The concurrency utilities include a locking framework that improves on intrinsic synchronization and wait/notification by offering lock polling, timed waits, and more.

The Locking Framework includes the often-used Lock, ReentrantLock, Condition, ReadWriteLock, and ReentrantReadWriteLock types, which I explored in this chapter. I also briefly introduced you to the StampedLock class, which was introduced in Java 8.

Chapter 8 presents additional concurrency utilities.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.12.140