Java 101: The next generation: Java concurrency without the pain, Part 2

Locking, atomic variables, Fork/Join, and what to expect in Java 8

The Java Concurrency Utilities are high-level concurrency types that facilitate threading tasks especially on multicore systems. Part 1 of this introduction featured java.util.concurrent's Executor framework, synchronizer types, and Java Concurrent Collections package. In Part 2, learn how the Java Concurrency Utilities handle locking, atomic variables, and fork/join operations. Then prepare for the future with an overview of seven anticipated changes to the Java Concurrency Utilities coming in Java 8.

Java's low-level threading capabilities are famously hard to use and error-prone, which is why they're frequently associated with deadlock, thread starvation, race conditions, and other concurrency bugs. One alternative to losing your sleep and peace of mind is the Java Concurrency Utilities, introduced in JDK 5. The two articles in this short series are dedicated to exploring how Java developers use the packages and libraries in java.util.concurrent to work around common threading bugs and write cleaner, simpler programs.

In Part 1, I explored the Executor framework, synchronizer utilities, and the Java Concurrent Collections package. Part 2 is an in-depth look at the mechanics of java.util.concurrent's advanced locking mechanisms and atomic variables, as well as a short tutorial on the Fork/Join framework. I also discuss the new features and performance improvements coming to the Java Concurrency Utilities with Java 8.

Related on JavaWorld

See "Modern threading for not-quite-beginners" (January 2013) for an intermediate primer on multithreaded programming and "Java Tip 144: When to use ForkJoinPool vs ExecutorService" (October 2011) for a quick tip on using the Fork/Join framework.

The Locking framework

The Java language lets threads use synchronization to update shared variables safely and ensure that one thread's updates are visible to other threads. In the Java language, you call synchronization via the synchronized keyword. The Java virtual machine (JVM) supports this mechanism via monitors and the associated monitorenter and monitorexit instructions.

Each Java object is associated with a monitor, which is a mutual exclusion mechanism that prevents multiple threads from concurrently executing in a critical section. Before a thread can enter this section, it must lock the monitor. If the monitor is already locked, the thread blocks until the monitor is unlocked.

Monitors also address the vagaries of memory caching and compiler optimizations that might otherwise prevent one thread from observing another thread's update of a shared variable. Before a thread leaves the critical section, the monitor ensures that the thread's updates are immediately visible, so that another thread about to enter the critical section will see those updates.

Synchronization vs volatile

Synchronization supports mutual exclusion and visibility. In contrast, the volatile keyword only supports visibility.

Although adequate for simple applications, Java's low-level synchronization mechanism can be inconvenient for advanced applications that require additional capabilities such as timed waits and lock polling.

The Locking framework, which is found in java.util.concurrent.locks, addresses these limitations.

Locks

The Lock interface provides more extensive locking operations than it's possible to obtain via synchronized methods and statements. For instance, you could use Lock to immediately back out of a lock-acquisition attempt if a lock wasn't available, or you could wait indefinitely to acquire a lock and back out only if interrupted.

Lock declares the following methods:

  • void lock() acquires a lock, disabling the current thread when the lock is not available. The thread remains dormant until the lock becomes available.
  • void lockInterruptibly() is similar to void lock() but allows the disabled thread to be interrupted and resume execution through a thrown java.lang.InterruptedException. (Note that interrupting lock acquisition is not supported in all cases.)
  • Condition newCondition() returns a new Condition instance that's bound to a given Lock instance. If the Lock implementation doesn't support conditions, java.lang.UnsupportedOperationException is thrown. (I discuss conditions later in this article.)
  • void lock() acquires a lock, disabling the current thread when the lock is not available. The thread remains dormant until the lock becomes available.
  • boolean tryLock() acquires a lock only when it's free at the time of invocation. This method returns true when the lock is acquired; otherwise, it returns false.
  • boolean tryLock(long time, TimeUnit unit) is similar to boolean tryLock(); however, it lets you specify an amount of time to wait for the lock to become available. Pass the magnitude of the delay to time and the units represented by this delay to unit. For example, you might pass 2 to time and TimeUnit.SECONDS to unit. (The java.util.concurrent.TimeUnit enum also offers DAYS, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, and NANOSECONDS.) This method throws InterruptedException when the current thread is interrupted while acquiring the lock (in cases where interrupting lock acquisition is supported).
  • void unlock() releases the lock.

It's important to always release a held lock. The Javadoc for the Lock interface presents the following idiom for locking a lock and ensuring that the lock is always unlocked:

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();
}

Lock's Javadoc also discusses the memory synchronization semantics that are expected of Lock implementations. Essentially, Lock implementations must behave as the built-in monitor lock, enforcing mutual exclusion and visibility.

Working with locks

The ReentrantLock class implements Lock and describes a reentrant mutual exclusion lock. The lock is associated with an acquisition count. When a thread holds the lock and re-acquires the lock, the acquisition count is incremented and the lock must be released twice.

ReentrantLock offers the same concurrency and memory semantics as the implicit monitor lock normally accessed using synchronized methods and statements. However, it has extended capabilities and offers better performance under high thread contention (that is, when threads are frequently asking to acquire a lock that is 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.

ReentrantLock or synchronized?

ReentrantLock behaves like synchronized and you might wonder when it's appropriate to use one or the other. Use ReentrantLock when you need timed or interruptible lock waits, non-block-structured locks (obtain a lock in one method; return the lock in another), multiple condition variables, or lock polling. Furthermore, ReentrantLock supports scalability and is useful where there is high contention among threads. If none of these factors come into play, use synchronized.

ReentrantLock declares the following constructors:

  • ReentrantLock() creates a reentrant lock.
  • ReentrantLock(boolean fair) creates a reentrant lock with the given fairness policy. Passing true to fair results in a lock that uses a fair ordering policy, which means that under contention, the lock favors granting access to the longest-waiting thread. The former constructor invokes this constructor, passing false to fair.

ReentrantLock implements Lock's methods: its implementation of unlock() throws java.lang.IllegalMonitorStateException when the calling thread doesn't hold the lock. Additionally, ReentrantLock provides its own methods, including the following trio:

  • int getHoldCount() returns the number of holds on this lock by the current thread: a thread has a hold on a lock for each lock action that isn't matched by an unlock action. When the lock() method is called and the current thread already holds the lock, the hold count is incremented by one and the method returns immediately.
  • boolean isFair() returns the fairness setting.
  • boolean isHeldByCurrentThread() queries if this lock is held by the current thread, returning true when this is the case. This method is often used for debugging and testing.

Listing 1 is a simple demonstration of ReentrantLock.

Listing 1. LockDemo.java

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo
{
   public static void main(String[] args)
   {
      ExecutorService executor = Executors.newFixedThreadPool(2);
      final ReentrantLock rl = new ReentrantLock();
 
      class Worker implements Runnable
      {
         private String name;

         Worker(String name)
         {
            this.name = name;
         }

         @Override
         public void run()
         {
           rl.lock();
           try
           {
              if (rl.isHeldByCurrentThread())
                System.out.printf("Thread %s has entered its critical section.%n", 
                                  name);
              System.out.printf("Thread %s is performing work for 2 seconds.%n", name);
              try
              {
                 Thread.sleep(2000);
              }
              catch (InterruptedException ie)
              {
                 ie.printStackTrace();
              }
              System.out.printf("Thread %s has finished working.%n", name);
           }
           finally
           {
              rl.unlock(); 
           }
         }
      }

      executor.execute(new Worker("A"));
      executor.execute(new Worker("B"));

      try
      {
         executor.awaitTermination(5, TimeUnit.SECONDS);
      }
      catch (InterruptedException ie)
      {
         ie.printStackTrace();
      }
      executor.shutdownNow();
   }
}

Listing 1 creates two worker threads. Each thread first acquires a lock to ensure that it has complete access to the critical section. It then outputs some messages and sleeps for two seconds to simulate work. After outputting another message, it releases the lock.

Compile LockDemo.java and run the application. You should observe output similar to the following:

Thread A has entered its critical section.
Thread A is performing work for 2 seconds.
Thread A has finished working.
Thread B has entered its critical section.
Thread B is performing work for 2 seconds.
Thread B has finished working.

Comment out rl.lock(); and rl.unlock(); and you should observe interleaved output like what is shown below:

Thread A is performing work for 2 seconds.
Thread B is performing work for 2 seconds.
Thread A has finished working.
Thread B has finished working.

Conditions

The Condition interface factors out the java.lang.Object monitor methods (wait(), notify(), and notifyAll()) into distinct 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 statements, Condition replaces Object monitor methods.

Condition declares the following methods:

  • void await() forces the current thread to wait until it's signalled or interrupted.
  • boolean await(long time, TimeUnit unit) forces the current thread to wait until it's signalled or interrupted, or the specified waiting time elapses.
  • long awaitNanos(long nanosTimeout) forces the current thread to wait until it's signalled or interrupted, or the specified waiting time elapses.
  • void awaitUninterruptibly() forces the current thread to wait until it's signalled.
  • boolean awaitUntil(Date deadline) forces the current thread to wait until it's signalled or interrupted, or the specified deadline elapses.
  • void signal() wakes up one waiting thread.
  • void signalAll() wakes up all waiting threads.

Working with conditions

The classic producer-consumer example nicely demonstrates conditions. In this example, a producer thread repeatedly produces items for consumption by a consumer thread.

The producer thread must not produce a new item until the previously produced item has been consumed. Similarly, the consumer thread must not consume an item that hasn't been produced. This is known as lockstep synchronization.

Listing 2 demonstrates conditions (and locks) in a producer-consumer context.

1 2 3 4 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more