Untangling Java concurrency

Java 101: Understanding Java threads, Part 4: Thread groups, volatility, and thread-local variables

Final concepts for improving Java application programming with Java threads

1 2 3 4 Page 2
Page 2 of 4

The main thread creates and starts threads A and B before sleeping for 2,000 milliseconds to give A and B a chance to wait. Upon waking, the main thread assumes A and B are waiting, and executes Thread.currentThread ().getThreadGroup ().interrupt (); to interrupt those threads. Because A and B execute in a synchronized context, one thread will throw an InterruptedException object and finish its processing before the other thread does the same. When run, InterruptThreadGroup produces the following output (for one invocation):

A about to wait.
B about to wait.
A interrupted.
A terminating.
B interrupted.
B terminating.

A is interrupted and terminates before B is interrupted and terminates.

You'll occasionally want to enumerate all threads or subgroups that comprise a thread group. The next section explores that activity.

Enumerate threads and subgroups

Part 1 introduced you to Thread's activeCount() and enumerate(Thread [] thdarray) methods. activeCount() calls ThreadGroup's int activeCount() method via the current thread's group reference to return an estimate of the active threads in the current thread's group and subgroups. enumerate(Thread [] thdarray) calls ThreadGroup's int enumerate(Thread [] thdarray) method via the current thread's group reference. enumerate(Thread [] thdarray) is just one of four enumeration methods in ThreadGroup:

  1. int enumerate(Thread [] thdarray) copies into thdarray references to every active thread in the current thread group and all subgroups.
  2. int enumerate(Thread [] thdarray, boolean recurse) copies into thdarray references to every active thread in the current thread group only if recurse is false. Otherwise, this method includes active threads from subgroups.
  3. int enumerate(ThreadGroup [] tgarray) copies into tgarray references to every active subgroup in the current thread group.
  4. int enumerate(ThreadGroup [] tgarray, boolean recurse) copies into tgarray references to every active subgroup in the current thread group only if recurse is false. Otherwise, this method includes all active subgroups of active subgroups, active subgroups of active subgroups of active subgroups, and so on.

You can use ThreadGroup's activeCount and enumerate(Thread [] thdarray) methods to enumerate all program threads. First, you find the system thread group. Then you call ThreadGroup's activeCount() method to retrieve an active thread count for array-sizing purposes. Next, you call ThreadGroup's enumerate(Thread [] thdarray) method to populate that array with Thread references, as Listing 4 demonstrates:

Listing 4. EnumThreads.java

// EnumThreads.java
class EnumThreads
{
   public static void main (String [] args)
   {
      // Find system thread group
      ThreadGroup system = null;
      ThreadGroup tg = Thread.currentThread ().getThreadGroup ();
      while (tg != null)
      {
         system = tg;
         tg = tg.getParent ();
      }
      // Display a list of all system and application threads, and their
      // daemon status
      if (system != null)
      {
         Thread [] thds = new Thread [system.activeCount ()];
         int nthds = system.enumerate (thds);
         for (int i = 0; i < nthds; i++)
              System.out.println (thds [i] + " " + thds [i].isDaemon ());
      }
   }
}

When run, EnumThreads produces the following output (on my platform):

Thread[Reference Handler,10,system] true
Thread[Finalizer,8,system] true
Thread[Signal Dispatcher,10,system] true
Thread[CompileThread0,10,system] true
Thread[main,5,main] false

Apart from a main thread, all other threads belong to the system thread group.

Tip: You can easily determine a thread group's parent group by calling ThreadGroup's ThreadGroup getParent() method. For all thread groups, save system, this method returns a nonnull reference. For system, this method returns null. You can also find out if a thread group is the parent, grandparent, and so forth of another thread group by calling ThreadGroup's boolean parentOf(ThreadGroup tg) method. That method returns true if a thread, whose reference you use to call parentOf(ThreadGroup tg), is a parent (or other ancestor) of the group that tg references—or is the same group as the tg-referenced group. Otherwise, the method returns false.

Volatility

Volatility, that is, changeability, describes the situation where one thread changes a shared field variable's value and another thread sees that change. You expect other threads to always see a shared field variable's value, but that is not necessarily the case. For performance reasons, Java does not require a JVM implementation to read a value from or write a value to a shared field variable in main memory, or object heap memory. Instead, the JVM might read a shared field variable's value from a processor register or cache, collectively known as working memory. Similarly, the JVM might write a shared field variable's value to a processor register or cache. That capability affects how threads share field variables, as you will see.

Suppose a program creates a shared integer-field variable x whose initial value in main memory is 10. This program starts two threads; one thread writes to x, and the other reads x's value. Finally, this program runs on a JVM implementation that assigns each thread its own private working memory, meaning each thread has its own private copy of x. When the writing thread writes 6 to x, the writing thread only updates its private working-memory copy of x; the thread does not update the main-memory copy. Also, when the reading thread reads from x, the returned value comes from the reading thread's private copy. Hence, the reading thread returns 10 (because a shared field variable's private working-memory copies initialize to values taken from the main-memory counterpart), not 6. As a result, one thread is unaware of another's change to a shared field variable.

A thread's inability to observe another thread's modification to a shared field variable can cause serious problems. For example, last month's YieldDemo application contained a pair of shared field variables: finished and sum. For JVMs that support separate working memory for each thread, the main and main-created YieldDemo threads can have their own copies of finished and sum. As a result, the main thread's execution of finished = true; would not affect the YieldDemo thread's copy of that variable—and the YieldDemo thread would never terminate.

When you ran YieldDemo, you probably discovered that program eventually terminated. That implies your JVM implementation reads/writes main memory instead of working memory. But if you found that the program did not terminate, you probably encountered a situation where the main thread set its working memory copy of finished to true, not the equivalent main-memory copy. Also, the YieldDemo thread read its own working-memory copy of finished, and never saw true.

To fix YieldDemo's visibility problem (on those JVMs that support working memory), include Java's volatile keyword in the finished and sum declarations: static volatile boolean finished = false; and static volatile int sum = 0;. The volatile keyword ensures that when a thread writes to a volatile shared field variable, the JVM modifies the main-memory copy, not the thread's working-memory copy. Similarly, the JVM ensures that a thread always reads from the main-memory copy.

Caution: The volatile and final keywords cannot appear together in a shared field variable declaration. Any attempt to include both keywords forces the compiler to report an error.

The visibility problem does not occur when threads use synchronization to access shared field variables. When a thread acquires a lock, the thread's working-memory copies of shared field variables reload from their main-memory counterparts. Similarly, when a thread releases a lock, the working-memory copies flush back to the main-memory shared field variables. For example, in last month's ProdCons2 application, the producer and consumer threads read from/wrote to the writeable shared field variable's main-memory copy because all access to that shared field variable happened within synchronized contexts. As a result, synchronization allows threads to communicate via shared field variables.

Tip: To ensure that a read/write operation (outside a synchronized context) on either a long-integer shared field variable or a double-precision floating-point shared field variable succeeds, prefix the shared field variable's declaration with keyword volatile.

New developers sometimes think volatility replaces synchronization. Although volatility, through keyword volatile, lets you assign values to long-integer or double-precision floating-point shared field variables outside a synchronized context, volatility cannot replace synchronization. Synchronization lets you group several operations into an indivisible unit, which you cannot do with volatility. However, because volatility is faster than synchronization, use volatility in situations where multiple threads must communicate via a single shared field variable.

Thread-local variables

Sun's Java 2 Platform, Standard Edition (J2SE) SDK 1.2 introduced the java.lang.ThreadLocal class, which developers use to create thread-local variablesThreadLocal objects that store values on a per-thread basis. Each ThreadLocal object maintains a separate value (such as a user ID) for each thread that accesses the object. Furthermore, a thread manipulates its own value and can't access other values in the same thread-local variable.

ThreadLocal has three methods:

  1. Object get (): Returns the calling thread's value from the thread-local variable. Because this method is thread-safe, you can call get() from outside a synchronized context.
  2. Object initialValue (): Returns the calling thread's initial value from the thread-local variable. Each thread's first call to either get() or set(Object value) results in an indirect call to initialValue() to initialize that thread's value in the thread-local variable. Because ThreadLocal's default implementation of initialValue() returns null, you must subclass ThreadLocal and override this method to return a nonnull initial value.
  3. void set (Object value): Sets the current thread's value in the thread-local variable to value. Use this method to replace the value that initialValue() returns.

Listing 5 shows you how to use ThreadLocal:

Listing 5. ThreadLocalDemo1.java

// ThreadLocalDemo1.java
class ThreadLocalDemo1
{
   public static void main (String [] args)
   {
      MyThread mt1 = new MyThread ("A");
      MyThread mt2 = new MyThread ("B");
      MyThread mt3 = new MyThread ("C");
      mt1.start ();
      mt2.start ();
      mt3.start ();
   }
}
class MyThread extends Thread
{
   private static ThreadLocal tl =
      new ThreadLocal ()
          {
             protected synchronized Object initialValue ()
             {
                return new Integer (sernum++);
             }
          };
   private static int sernum = 100;
   MyThread (String name)
   {
      super (name);
   }
   public void run ()
   {
      for (int i = 0; i < 10; i++)
           System.out.println (getName () + " " + tl.get ());
   }
}

ThreadLocalDemo1 creates a thread-local variable. That variable associates a unique serial number with each thread that accesses the thread-local variable by calling tl.get (). When a thread first calls tl.get (), ThreadLocal's get() method calls the overridden initialValue() method in the anonymous ThreadLocal subclass. The following output results from one program invocation:

A 100
A 100
A 100
A 100
A 100
A 100
A 100
A 100
A 100
A 100
B 101
B 101
B 101
B 101
B 101
B 101
B 101
B 101
B 101
B 101
C 102
C 102
C 102
C 102
C 102
C 102
C 102
C 102
C 102
C 102

The output associates each thread name (A, B, or C) with a unique serial number. If you run this program a second time, you might see a different serial number associate with a thread name. Though the number differs, it always associates with a single thread name.

Note: To allow multiple threads access to the same ThreadLocal object, ThreadLocalDemo1 uses the static keyword. Without that keyword, each thread accesses its own ThreadLocal object, with each object containing only a value for one thread, not a separate value for each thread. Because thread-local variables store values on a per-thread basis, failing to use static in a thread-local variable declaration serves little purpose.

An alternative to overriding ThreadLocal's initialValue() method is calling that class's set(Object value) method to provide an initial value:

Listing 6. ThreadLocalDemo2.java

// ThreadLocalDemo2.java
class ThreadLocalDemo2
{
   public static void main (String [] args)
   {
      MyThread mt1 = new MyThread ("A");
      MyThread mt2 = new MyThread ("B");
      MyThread mt3 = new MyThread ("C");
      mt1.start ();
      mt2.start ();
      mt3.start ();
   }
}
class MyThread extends Thread
{
   private static ThreadLocal tl = new ThreadLocal ();
   private static int sernum = 100;
   MyThread (String name)
   {
      super (name);
   }
   public void run ()
   {
      synchronized ("A")
      {
         tl.set ("" + sernum++);
      }
      for (int i = 0; i < 10; i++)
           System.out.println (getName () + " " + tl.get ());
   }
}

ThreadLocalDemo2 is nearly identical to ThreadLocalDemo1. However, instead of overriding initialValue() to establish each thread's initial value to a unique serial number, ThreadLocalDemo2 uses a tl.set ("" + sernum++); method call. If you run this program, your output will be more or less identical (on an invocation-by-invocation basis) to ThreadLocalDemo1's output.

1 2 3 4 Page 2
Page 2 of 4