Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

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

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

  • Print
  • Feedback

Page 4 of 7

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.

Listing 2. CondDemo.java

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CondDemo
{
   public static void main(String[] args)
   {
      Shared s = new Shared();
      new Producer(s).start();
      new Consumer(s).start();
   }
}

class Shared
{
   // Fields c and available are volatile so that writes to them are visible to 
   // the various threads. Fields lock and condition are final so that they're
   // initial values are visible to the various threads. (The Java memory model 
   // promises that, after a final field has been initialized, any thread will 
   // see the same [correct] value.)

   private volatile char c;
   private volatile boolean available;
   private final Lock lock;
   private final Condition condition;

   Shared()
   {
      c = '\u0000';
      available = false;
      lock = new ReentrantLock();
      condition = lock.newCondition();
   }

   Lock getLock()
   {
      return lock;
   }

   char getSharedChar()
   {
      lock.lock();
      try
      {
         while (!available)
            try
            {
               condition.await();
            }
            catch (InterruptedException ie)
            {
               ie.printStackTrace();
            }
         available = false;
         condition.signal();
      }
      finally
      {
         lock.unlock();
         return c;
      }
   }

   void setSharedChar(char c)
   {
      lock.lock();
      try
      {
         while (available)
            try
            {
               condition.await();
            }
            catch (InterruptedException ie)
            {
               ie.printStackTrace();
            }
         this.c = c;
         available = true;
         condition.signal();
      }
      finally
      {
         lock.unlock();
      }
   }
}

class Producer extends Thread
{
   // l is final because it's initialized on the main thread and accessed on the
   // producer thread.

   private final Lock l;

   // s is final because it's initialized on the main thread and accessed on the
   // producer thread.

   private final Shared s;
   
   Producer(Shared s)
   {
      this.s = s;
      l = s.getLock();
   }

   @Override
   public void run()
   {
      for (char ch = 'A'; ch <= 'Z'; ch++)
      {
         l.lock();
         s.setSharedChar(ch);
         System.out.println(ch + " produced by producer.");
         l.unlock();
      }
   }
}
class Consumer extends Thread
{
   // l is final because it's initialized on the main thread and accessed on the
   // consumer thread.

   private final Lock l;

   // s is final because it's initialized on the main thread and accessed on the
   // consumer thread.

   private final Shared s;

   Consumer(Shared s)
   {
      this.s = s;
      l = s.getLock();
   }

   @Override
   public void run()
   {
      char ch;
      do
      {
         l.lock();
         ch = s.getSharedChar();
         System.out.println(ch + " consumed by consumer.");
         l.unlock();
      }
      while (ch != 'Z');
   }
}

Listing 2 presents four classes: CondDemo, Shared, Producer, and Consumer. CondDemo drives the application, Shared encapsulates the logic for setting and getting a shared variable's value, Producer describes the producer thread, and Consumer describes the consumer thread.

  • Print
  • Feedback

Resources

Previous articles in Java 101: The next generation

Concurrency tutorials on JavaWorld:

  • Modern threading for not-quite-beginners (Cameron Laird, JavaWorld, January 2013): Get an overview of callable and runnable, learn more about synchronized blocks, and find out how you can use java.util.concurrent to work around deadlock and similar threading pitfalls.
  • Multicore processing for client-side Java applications (Kirill Grouchnikov, JavaWorld, September 2007): Get a hands-on introduction to collection sorting using the CountDownLatch and Executors.newFixedThreadPool concurrency utilities.
  • Java concurrency with thread gates (Obi Ezechukwu, JavaWorld, March 2009): See the Java concurrency utilities at work in a realistic implementation of the Thread Gates concurrency pattern.
  • Hyper-threaded Java (Randall Scarberry, JavaWorld, November 2006): See for yourself how two java.util.concurrent classes were used to optimize thread use for faster performance in a real-world application.
  • Java Tip 144: When to use ForkJoinPool vs ExecutorService (Madalin Ilie, JavaWorld, October 2011): Demonstrates the performance impact of replacing the standard ExecutorService class with ForkJoinPool in a web crawler.