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

Final concepts for improving Java application programming with Java threads

This month's Java 101 concludes the thread series by focusing on thread groups, volatility, thread-local variables, timers, and the ThreadDeath class.

Thread groups

In a network server program, one thread waits for and accepts requests from client programs to execute, for example, database transactions or complex calculations. The thread usually creates a new thread to handle the request. Depending on the request volume, many different threads might be simultaneously present, complicating thread management. To simplify thread management, programs organize their threads with thread groupsjava.lang.ThreadGroup objects that group related threads' Thread (and Thread subclass) objects. For example, your program can use ThreadGroup to group all printing threads into one group.

Note: To keep the discussion simple, I refer to thread groups as if they organize threads. In reality, thread groups organize Thread (and Thread subclass) objects associated with threads.

Java requires every thread and every thread group—save the root thread group, system—to join some other thread group. That arrangement leads to a hierarchical thread-group structure, which the figure below illustrates in an application context.

Figure 1. An application's hierarchical thread-group structure begins with a main thread group just below the system thread group

At the top of the figure's structure is the system thread group. The JVM-created system group organizes JVM threads that deal with object finalization and other system tasks, and serves as the root thread group of an application's hierarchical thread-group structure. Just below system is the JVM-created main thread group, which is system's subthread group (subgroup, for short). main contains at least one thread—the JVM-created main thread that executes byte-code instructions in the main() method.

Below the main group reside the subgroup 1 and subgroup 2 subgroups, application-created subgroups (which the figure's application creates). Furthermore, subgroup 1 groups three application-created threads: thread 1, thread 2, and thread 3. In contrast, subgroup 2 groups one application-created thread: my thread.

Now that you know the basics, let's start creating thread groups.

Create thread groups and associate threads with those groups

The ThreadGroup class's SDK documentation reveals two constructors: ThreadGroup(String name) and ThreadGroup(ThreadGroup parent, String name). Both constructors create a thread group and give it a name, as the name parameter specifies. The constructors differ in their choice of what thread group serves as parent to the newly created thread group. Each thread group, except system, must have a parent thread group. For ThreadGroup(String name), the parent is the thread group of the thread that calls ThreadGroup(String name). As an example, if the main thread calls ThreadGroup(String name), the newly created thread group has the main thread's group as its parent—main. For ThreadGroup(ThreadGroup parent, String name), the parent is the group that parent references. The following code shows how to use these constructors to create a pair of thread groups:

public static void main (String [] args)
   ThreadGroup tg1 = new ThreadGroup ("A");
   ThreadGroup tg2 = new ThreadGroup (tg1, "B");

In the code above, the main thread creates two thread groups: A and B. First, the main thread creates A by calling ThreadGroup(String name). The tg1-referenced thread group's parent is main because main is the main thread's thread group. Second, the main thread creates B by calling ThreadGroup(ThreadGroup parent, String name). The tg2-referenced thread group's parent is A because tg1's reference passes as an argument to ThreadGroup (tg1, "B") and A associates with tg1.

Tip: Once you no longer need a hierarchy of ThreadGroup objects, call ThreadGroup's void destroy() method via a reference to the ThreadGroup object at the top of that hierarchy. If the top ThreadGroup object and all subgroup objects lack thread objects, destroy() prepares those thread group objects for garbage collection. Otherwise, destroy() throws an IllegalThreadStateException object. However, until you nullify the reference to the top ThreadGroup object (assuming a field variable contains that reference), the garbage collector cannot collect that object. Referencing the top object, you can determine if a previous call was made to the destroy() method by calling ThreadGroup's boolean isDestroyed() method. That method returns true if the thread group hierarchy was destroyed.

By themselves, thread groups are useless. To be of any use, they must group threads. You group threads into thread groups by passing ThreadGroup references to appropriate Thread constructors:

ThreadGroup tg = new ThreadGroup ("subgroup 2");
Thread t = new Thread (tg, "my thread");

The code above first creates a subgroup 2 group with main as the parent group. (I assume the main thread executes the code.) The code next creates a my thread Thread object in the subgroup 2 group.

Now, let's create an application that produces our figure's hierarchical thread-group structure:

Listing 1.

class ThreadGroupDemo
   public static void main (String [] args)
      ThreadGroup tg = new ThreadGroup ("subgroup 1");
      Thread t1 = new Thread (tg, "thread 1");
      Thread t2 = new Thread (tg, "thread 2");
      Thread t3 = new Thread (tg, "thread 3");
      tg = new ThreadGroup ("subgroup 2");
      Thread t4 = new Thread (tg, "my thread");
      tg = Thread.currentThread ().getThreadGroup ();
      int agc = tg.activeGroupCount ();
      System.out.println ("Active thread groups in " + tg.getName () +
                          " thread group: " + agc);
      tg.list ();

ThreadGroupDemo creates the appropriate thread group and thread objects to mirror what you see in the figure above. To prove that the subgroup 1 and subgroup 2 groups are main's only subgroups, ThreadGroupDemo does the following:

  1. Retrieves a reference to the main thread's ThreadGroup object by calling Thread's static currentThread() method (which returns a reference to the main thread's Thread object) followed by Thread's ThreadGroup getThreadGroup() method.
  2. Calls ThreadGroup's int activeGroupCount() method on the just-returned ThreadGroup reference to return an estimate of active groups within the main thread's thread group.
  3. Calls ThreadGroup's String getName () method to return the main thread's thread group name.
  4. Calls ThreadGroup's void list () method to print on the standard output device details on the main thread's thread group and all subgroups.

When run, ThreadGroupDemo displays the following output:

Active thread groups in main thread group: 2
    java.lang.ThreadGroup[name=subgroup 1,maxpri=10]
        Thread[thread 1,5,subgroup 1]
        Thread[thread 2,5,subgroup 1]
        Thread[thread 3,5,subgroup 1]
    java.lang.ThreadGroup[name=subgroup 2,maxpri=10]
        Thread[my thread,5,subgroup 2]

Output that begins with Thread results from list()'s internal calls to Thread's toString() method, an output format I described in Part 1. Along with that output, you see output beginning with java.lang.ThreadGroup. That output identifies the thread group's name followed by its maximum priority.

Priority and thread groups

A thread group's maximum priority is the highest priority any of its threads can attain. Consider the aforementioned network server program. Within that program, a thread waits for and accepts requests from client programs. Before doing that, the wait-for/accept-request thread might first create a thread group with a maximum priority just below that thread's priority. Later, when a request arrives, the wait-for/accept-request thread creates a new thread to respond to the client request and adds the new thread to the previously created thread group. The new thread's priority automatically lowers to the thread group's maximum. That way, the wait-for/accept-request thread responds more often to requests because it runs more often.

Java assigns a maximum priority to each thread group. When you create a group, Java obtains that priority from its parent group. Use ThreadGroup's void setMaxPriority(int priority) method to subsequently set the maximum priority. Any threads that you add to the group after setting its maximum priority cannot have a priority that exceeds the maximum. Any thread with a higher priority automatically lowers when it joins the thread group. However, if you use setMaxPriority(int priority) to lower a group's maximum priority, all threads added to the group prior to that method call keep their original priorities. For example, if you add a priority 8 thread to a maximum priority 9 group, and then lower that group's maximum priority to 7, the priority 8 thread remains at priority 8. At any time, you can determine a thread group's maximum priority by calling ThreadGroup's int getMaxPriority() method. To demonstrate priority and thread groups, I wrote MaxPriorityDemo:

Listing 2.

class MaxPriorityDemo
   public static void main (String [] args)
      ThreadGroup tg = new ThreadGroup ("A");
      System.out.println ("tg maximum priority = " + tg.getMaxPriority ());
      Thread t1 = new Thread (tg, "X");
      System.out.println ("t1 priority = " + t1.getPriority ());
      t1.setPriority (Thread.NORM_PRIORITY + 1);
      System.out.println ("t1 priority after setPriority() = " +
                          t1.getPriority ());
      tg.setMaxPriority (Thread.NORM_PRIORITY - 1);
      System.out.println ("tg maximum priority after setMaxPriority() = " +
                          tg.getMaxPriority ());
      System.out.println ("t1 priority after setMaxPriority() = " +
                          t1.getPriority ());
      Thread t2 = new Thread (tg, "Y");
      System.out.println ("t2 priority = " + t2.getPriority ());
      t2.setPriority (Thread.NORM_PRIORITY);
      System.out.println ("t2 priority after setPriority() = " +
                          t2.getPriority ());

When run, MaxPriorityDemo produces the following output:

tg maximum priority = 10
t1 priority = 5
t1 priority after setPriority() = 6
tg maximum priority after setMaxPriority() = 4
t1 priority after setMaxPriority() = 6
t2 priority = 4
t2 priority after setPriority() = 4

Thread group A (which tg references) starts with the highest priority (10) as its maximum. Thread X, whose Thread object t1 references, joins the group and receives 5 as its priority. We change that thread's priority to 6, which succeeds because 6 is less than 10. Subsequently, we call setMaxPriority(int priority) to reduce the group's maximum priority to 4. Although thread X remains at priority 6, a newly-added Y thread receives 4 as its priority. Finally, an attempt to increase thread Y's priority to 5 fails, because 5 is greater than 4.

Note: setMaxPriority(int priority) automatically adjusts the maximum priority of a thread group's subgroups.

In addition to using thread groups to limit thread priority, you can accomplish other tasks by calling various ThreadGroup methods that apply to each group's thread. Methods include void suspend(), void resume(), void stop(), and void interrupt(). Because Sun Microsystems has deprecated the first three methods (they are unsafe), we examine only interrupt().

Interrupt a thread group

ThreadGroup's interrupt() method allows a thread to interrupt a specific thread group's threads and subgroups. This technique would prove appropriate in the following scenario: Your application's main thread creates multiple threads that each perform a unit of work. Because all threads must complete their respective work units before any thread can examine the results, each thread waits after completing its work unit. The main thread monitors the work state. Once all other threads are waiting, the main thread calls interrupt() to interrupt the other threads' waits. Then those threads can examine and process the results. Listing 3 demonstrates thread group interruption:

Listing 3.

class InterruptThreadGroup
   public static void main (String [] args)
      MyThread mt = new MyThread ();
      mt.setName ("A");
      mt.start ();
      mt = new MyThread ();
      mt.setName ("B");
      mt.start ();
         Thread.sleep (2000); // Wait 2 seconds
      catch (InterruptedException e)
      // Interrupt all methods in the same thread group as the main 
      // thread
      Thread.currentThread ().getThreadGroup ().interrupt ();
class MyThread extends Thread
   public void run ()
      synchronized ("A")
         System.out.println (getName () + " about to wait.");
            "A".wait ();
         catch (InterruptedException e)
            System.out.println (getName () + " interrupted.");
         System.out.println (getName () + " terminating.");
1 2 3 4 Page 1
Page 1 of 4
How to choose a low-code development platform