Threads
Overview
Threads run within a program. Threads themselves are not programs and cannot run on their own.
Some texts use the name lightweight process instead of thread. A thread is similar to a real process in that a thread and a running program are both a single sequential flow of control. However, a thread is considered lightweight because it runs within the context of a full-blown program and takes advantage of the resources allocated for that program and the program's environment.
As a sequential flow of control, a thread must carve out some of its own resources within a running program. (It must have its own execution stack and program counter for example.) The code running within the thread works only within that context. Thus, some other texts use execution context as a synonym for thread.
Below are three sorting algorithms.
Bi-Directional Bubble Sort Bubble Sort Quick Sort
Using the Timer and TimerTask Classes
In version 1.3, support for timers was added to the java.util package. The Timer class in that package schedules instances of a class called TimerTask. If you’re writing a program with a graphical user interface (GUI), you might want to use the javax.swing.Timer class (added in version 1.2) instead of java.util.Timer. Another utility class, SwingWorker, helps you with another common job: performing a task in a background thread, optionally updating the GUI when the task completes. Here's an example, Reminder.java, of using a timer to perform a task after a delay.import java.util.Timer; import java.util.TimerTask; /** * Simple demo that uses java.util.Timer to schedule a task to execute * once 5 seconds have passed. */ public class Reminder { Timer timer; public Reminder(int seconds) { timer = new Timer(); timer.schedule(new RemindTask(), seconds*1000); } class RemindTask extends TimerTask { public void run() { System.out.println("Time's up!"); timer.cancel(); //Terminate the timer thread } } public static void main(String args[]) { System.out.println("About to schedule task."); new Reminder(5); System.out.println("Task scheduled."); } }When you run the example, you first see this:
Task scheduled.Five seconds later, you see this:
Time's up!This simple program illustrates the basic parts of implementing and scheduling a task to be executed by a timer thread.
- Implement a custom subclass of TimerTask. The run method contains the code that performs the task. In this example, the subclass is named RemindTask.
- Create a thread by instantiating the Timer class.
- Instantiate the timer task object (new RemindTask()).
- Schedule the timer task for execution. This example uses the schedule method, with the timer task as the first argument and the delay in milliseconds (5000) as the second argument. Another way of scheduling a task is to specify the time when the task should execute. For example, the following code schedules a task for execution at 11:01 p.m.:
//Get the Date corresponding to 11:01:00 pm today. Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, 23); calendar.set(Calendar.MINUTE, 1); calendar.set(Calendar.SECOND, 0); Date time = calendar.getTime(); timer = new Timer(); timer.schedule(new RemindTask(), time);
Stopping Timer Threads
By default, a program keeps running as long as its timer threads are running. You can terminate a timer thread in four ways.The Reminder example uses the first scheme, invoking the cancel method from the timer task’s run method. Making the timer thread a daemon wouldn’t work, because the program needs to keep running until the timer’s task executes.
- Invoke cancel on the timer. You can do this from anywhere in the program, such as from a timer task’s run method.
- Make the timer's thread a “daemon” by creating the timer like this: new Timer(true). If the only threads left in the program are daemon threads, the program exits.
- After all the timer’s scheduled tasks have finished executing, remove all references to the Timer object. Eventually, the timer’s thread will terminate.
- Invoke the System.exit method, which makes the entire program (and all its threads) exit.
Sometimes, timer threads aren’t the only threads that can prevent a program from exiting when expected. For example, if you use the AWT at all—even if only to make beeps—the AWT automatically creates a nondaemon thread that keeps the program alive. The following modification of Reminder adds beeping, which requires us to also add a call to the System.exit method to make the program exit. Significant changes are in boldface. You can find the source code in ReminderBeep.java.
... public class ReminderBeep { ... public ReminderBeep(int seconds) { toolkit = Toolkit.getDefaultToolkit(); timer = new Timer(); timer.schedule(new RemindTask(), seconds*1000); } class RemindTask extends TimerTask { public void run() { System.out.println("Time's up!"); toolkit.beep(); //timer.cancel(); //Not necessary because we call System.exit System.exit(0); //Stops the AWT thread (and everything else) } } ... }
Performing a Task Repeatedly
Here’s an example of using a timer to perform a task once per second.public class AnnoyingBeep { Toolkit toolkit; Timer timer; public AnnoyingBeep() { toolkit = Toolkit.getDefaultToolkit(); timer = new Timer(); timer.schedule(new RemindTask(), 0, //initial delay 1*1000); //subsequent rate } class RemindTask extends TimerTask { int numWarningBeeps = 3; public void run() { if (numWarningBeeps > 0) { toolkit.beep(); System.out.println("Beep!"); numWarningBeeps--; } else { toolkit.beep(); System.out.println("Time's up!"); //timer.cancel(); //Not necessary because we call System.exit System.exit(0); //Stops the AWT thread (and everything else) } } } ... }You can find the entire program in AnnoyingBeep.java. When you execute it, you see the following output (our comments about timing are shown in italics):
The AnnoyingBeep program uses a three-argument version of the schedule method to specify that its task should execute once a second, beginning immediately. Here are all the Timer methods you can use to schedule repeated executions of tasks:Task scheduled. Beep! Beep! //one second after the first beep Beep! //one second after the second beep Time's up! //one second after the third beep
- schedule TimerTask(task, long delay, long period)
- schedule TimerTask(task, Date time, long period)
- scheduleAtFixedRate TimerTask(task, long delay, long period)
- scheduleAtFixedRate TimerTask(task, Date firstTime, long period)
When scheduling a task for repeated execution, you should use one of the schedule methods when smoothness is important and a scheduleAtFixedRate method when time synchronization is more important. For example, the AnnoyingBeep program uses the schedule method, which means that the annoying beeps will all be at least 1 second apart. If one beep is late for any reason, all subsequent beeps will be delayed. If we decide that the AnnoyingBeep program should exit exactly 3 seconds after the first beep—even if it means that two beeps might occur close together if a beep is delayed for any reason—we should use the scheduleAtFixedRate method instead.
More Information about Timers
The timer tasks we've shown have been very simple. They do almost nothing and refer only to data that either can be safely accessed from multiple threads or is private to the timer task. As long as your timer task uses only API designed to be thread-safe—such as the methods in the Timer class—implementing timers is relatively straightforward. However, if your timer implementation depends on shared resources, such as data used by other places in your program, you need to be careful.
For further information about timers, see
- The API documentation for Timer and TimerTask
- Using Timers in Swing Applications, an article in the online magazine The Swing Connection.
- The May 30, 2000, edition of the Java Developer Connection "Tech Tips".
Customizing a Thread's run Method
The run method gives a thread something to do. Its code implements the thread's running behavior. It can do anything that can be encoded in Java statements: compute a list of prime's, sort some data, perform some animation.The Thread class implements a generic thread that, by default, does nothing. That is, the implementation of its run method is empty. This is not particularly useful, so the Thread class defines API that lets a Runnable object provide a more interesting run method for a thread.
There are two techniques for providing a run method for a thread:
The Life Cycle of a Thread
Now that you've seen how to give a thread something to do, we'll review some details that were glossed over in the previous section. In particular, we look at the life cycle of a thread: how to create and start a thread, some of the special things it can do while it's running, and how to stop it.The following diagram shows the states that a Java thread can be in during its life. It also illustrates which method calls cause a transition to another state. This figure is not a complete finite state diagram, but rather an overview of the more interesting and common facets of a thread's life. The remainder of this section uses the Clock applet previously introduced to discuss a thread's life cycle in terms of its state.
Creating a Thread
The application in which an applet is running calls the applet's start method when the user visits the applet's page. The Clock applet creates a Thread, clockThread, in its start with the bold code shown here:public void start() { if (clockThread == null) { clockThread = new Thread(this, "Clock"); clockThread.start(); } }After the bold statement has been executed, clockThread is in the New Thread state. When a thread is a New Thread, it is merely an empty Thread object; no system resources have been allocated for it yet. When a thread is in this state, you can only start the thread. Calling any method besides start when a thread is in this state makes no sense and causes an IllegalThreadStateException. (In fact, the runtime system throws an IllegalThreadStateException any time a method is called on a thread and that thread's state does not allow for that method call.)
Notice that this--the Clock instance-- is the first argument to the thread constructor. The first argument to this thread constructor must implement the Runnable interface and provides the thread with its run method. The second argument is just a name for the thread.
Starting a Thread
Now consider the next line of code in Clock's start method shown here in bold:public void start() { if (clockThread == null) { clockThread = new Thread(this, "Clock"); clockThread.start(); } }The start method creates the system resources necessary to run the thread, schedules the thread to run, and calls the thread's run method. clockThread's run method is the one defined in the Clock class.
After the start method has returned, the thread is "running". Yet, it's somewhat more complex than that. As the previous figure shows, a thread that has been started is actually in the Runnable state. Many computers have a single processor, thus making it impossible to run all "running" threads at the same time. The Java runtime system must implement a scheduling scheme that shares the processor between all "running" threads. (See Understanding Thread Priority for more information about scheduling.) So at any given time, a "running" thread actually may be waiting for its turn in the CPU.
Here's another look at Clock's run method:
public void run() { Thread myThread = Thread.currentThread(); while (clockThread == myThread) { repaint(); try { Thread.sleep(1000); } catch (InterruptedException e){ // the VM doesn't want us to sleep anymore, // so get back to work } } }Clock's run method loops while the condition clockThread == myThread is true. This exit condition is explained in more detail in Stopping a Thread. However, for now, know that it allows the thread, and thus the applet, to exit gracefully.
Within the loop, the applet repaints itself and then tells the thread to sleep for one second (1000 milliseconds). An applet's repaint method ultimately calls the applet's paint method, which does the actual update of the applet's display area. The Clock paint method gets the current time, formats, and displays it:
public void paint Graphics(g) { // get the time and convert it to a date Calendar cal = Calendar.getInstance(); Date date = cal.getTime(); // format it and display it DateFormat dateFormatter = DateFormat.getTimeInstance(); g.drawString(dateFormatter.format(date), 5, 10); }
Making a Thread Not Runnable
A thread becomes Not Runnable when one of these events occurs:The clockThread in the Clock applet becomes Not Runnable when the run method calls sleep on the current thread:
- Its sleep method is invoked.
- The thread calls the wait method to wait for a specific condition to be satisifed.
- The thread is blocking on I/O.
public void run() { Thread myThread = Thread.currentThread(); while (clockThread == myThread) { repaint(); try { Thread.sleep(1000); } catch (InterruptedException e){ // the VM doesn't want us to sleep anymore, // so get back to work } } }During the second that the clockThread is asleep, the thread does not run, even if the processor becomes available. After the second has elapsed, the thread becomes Runnable again and, if the processor becomes available, the thread begins running again.
For each entrance into the Not Runnable state, there is a specific and distinct escape route that returns the thread to the Runnable state. An escape route works only for its corresponding entrance. For example, if a thread has been put to sleep, then the specified number of milliseconds must elapse before the thread becomes Runnable again. The following list describes the escape route for every entrance into the Not Runnable state:
- If a thread has been put to sleep, then the specified number of milliseconds must elapse.
- If a thread is waiting for a condition, then another object must notify the waiting thread of a change in condition by calling notify or notifyAll.
- If a thread is blocked on I/O, then the I/O must complete.
Stopping a Thread
A program doesn't stop a thread like it stops an applet (by calling a method). Rather, a thread arranges for its own death by having a run method that terminates naturally. For example, the while loop in this run method is a finite loop-- it will iterate 100 times and then exit:public void run() { int i = 0; while (i < 100) { i++; System.out.println("i = " + i); } }A thread with this run method dies naturally when the loop completes and the run method exits.
Let's look at how the Clock applet thread arranges for its own death. You might want to use this technique with your applets. Recall Clock's run method:
public void run() { Thread myThread = Thread.currentThread(); while (clockThread == myThread) { repaint(); try { Thread.sleep(1000); } catch (InterruptedException e){ // the VM doesn't want us to sleep anymore, // so get back to work } } }The exit condition for this run method is the exit condition for the while loop because there is no code after the while loop:
while (clockThread == myThread) {This condition indicates that the loop will exit when the currently executing thread is not equal to clockThread. When would this ever be the case?
When you leave the page, the application in which the applet is running calls the applet's stop method. This method then sets the clockThread to null, thereby telling the main loop in the run method to terminate:
public void stop() { // applets' stop method clockThread = null; }If you revisit the page, the start method is called again and the clock starts up again with a new thread. Even if you stop and start the applet faster than one iteration of the loop, clockThread will be a different thread than myThread and the loop will still terminate.
The isAlive Method
The API for the Thread class includes a method called isAlive. The isAlive method returns true if the thread has been started and not stopped. If the isAlive method returns false, you know that the thread either is a New Thread or is Dead. If the isAlive method returns true, you know that the thread is either Runnable or Not Runnable. You cannot differentiate between a New Thread or a Dead thread. Nor can you differentiate between a Runnable thread and a Not Runnable thread.
Understanding Thread Priority
Previously, this lesson claimed that threads run concurrently. While conceptually this is true, in practice it usually isn't. Most computer configurations have a single CPU, so threads actually run one at a time in such a way as to provide an illusion of concurrency. Execution of multiple threads on a single CPU, in some order, is called scheduling. The Java runtime supports a very simple, deterministic scheduling algorithm known as fixed priority scheduling. This algorithm schedules threads based on their priority relative to other runnable threads.When a Java thread is created, it inherits its priority from the thread that created it. You can also modify a thread's priority at any time after its creation using the setPriority method. Thread priorities are integers ranging between MIN_PRIORITY and MAX_PRIORITY (constants defined in the Thread class). The higher the integer, the higher the priority. At any given time, when multiple threads are ready to be executed, the runtime system chooses the runnable thread with the highest priority for execution. Only when that thread stops, yields, or becomes not runnable for some reason will a lower priority thread start executing. If two threads of the same priority are waiting for the CPU, the scheduler chooses one of them to run in a round-robin fashion. The chosen thread will run until one of the following conditions is true:
Then the second thread is given a chance to run, and so on, until the interpreter exits.
- A higher priority thread becomes runnable.
- It yields, or its run method exits.
- On systems that support time-slicing, its time allotment has expired.
The Java runtime system's thread scheduling algorithm is also preemptive. If at any time a thread with a higher priority than all other runnable threads becomes runnable, the runtime system chooses the new higher priority thread for execution. The new higher priority thread is said to preempt the other threads.
At any given time, the highest priority thread is running. However, this is not guaranteed. The thread scheduler may choose to run a lower priority thread to avoid starvation. For this reason, use priority only to affect scheduling policy for efficiency purposes. Do not rely on thread priority for algorithm correctness.
A Thread Race
RaceApplet is an applet that animates a race between two "runner" threads with different priorities. When you click the mouse down over the applet, it starts the two runners. The top runner, labelled "2", has a priority of 2. The second runner, labelled "3", has a priority of 3.Try this: Click the applet below to start the race.
This is a picture of the applet's GUI. To run the applet, click the picture. The applet will appear in a new browser window.This is the run method for both runners.
public int tick = 1; public void run() { while (tick < 10000000) tick++; }This run method simply counts from 1 to 10,000,000. The instance variable tick is public because the applet uses this value to determine how far the runner has progressed (how long its line is).
In addition to the two runner threads, this applet also has a third thread that handles the drawing. The drawing thread's run method contains an infinite loop; during each iteration of the loop it draws a line for each runner (whose length is computed from the runner's tick variable), and then sleeps for 3 milliseconds. The drawing thread has a thread priority of 4--higher than either runner. So, whenever the drawing thread wakes up after 3 milliseconds, it becomes the highest priority thread, preempting whichever runner is currently running, and draws the lines. You can see the lines inch their way across the page
This is not a fair race because one runner has a higher priority than the other. Each time the drawing thread yields the CPU by going to sleep for 3 milliseconds, the scheduler chooses the highest priority runnable thread to run; in this case, it's always the runner with a priority of 3. Here is another version of the applet that implements a fair race, that is, both of the runners have the same priority and they have an equal chance of being chosen to run.
Try this: Click the mouse to start the race.
In this race, each time the drawing thread yields the CPU by going to sleep, there are two runnable threads of equal priority--the runners--waiting for the CPU; the scheduler must choose one of the threads to run. In this situation, the scheduler chooses the next thread to run in a round-robin fashion.
This is a picture of the applet's GUI. To run the applet, click the picture. The applet will appear in a new browser window.
Selfish Threads
The Runner class used in the races above actually implements "socially-impaired" thread behavior. Recall the run method from the Runner class used in the races above:public int tick = 1; public void run() { while (tick < 10000000) tick++; }The while loop in the run method is in a tight loop. Once the scheduler chooses a thread with this thread body for execution, the thread never voluntarily relinquishes control of the CPU--the thread continues to run until the while loop terminates naturally or until the thread is preempted by a higher priority thread. This thread is called a selfish thread.
In some situations, having selfish threads doesn't cause any problems because a higher priority thread preempts the selfish one (just as the drawing thread in the RaceApplet preempts the selfish runners). However, in other situations, threads with CPU-greedy run methods, such as the Runner class, can take over the CPU and cause other threads to wait for a long time before getting a chance to run.
Time-Slicing
Some systems, such as Windows 95/NT, fight selfish thread behavior with a strategy known as time-slicing. Time-slicing comes into play when there are multiple "Runnable" threads of equal priority and those threads are the highest priority threads competing for the CPU. For example, this stand-alone Java program (which is based on the RaceApplet above) creates two equal priority selfish threads that have the following run method.public void run() { while (tick < 400000) { tick++; if ((tick % 50000) == 0) System.out.println("Thread #" + num + ", tick = " + tick); } }This run contains a tight loop that increments the integer tick and every 50,000 ticks prints out the thread's identifier and its tick count.
When running this program on a time-sliced system, you will see messages from both threads intermingled with one another. Like this:
Thread #1, tick = 50000 Thread #0, tick = 50000 Thread #0, tick = 100000 Thread #1, tick = 100000 Thread #1, tick = 150000 Thread #1, tick = 200000 Thread #0, tick = 150000 Thread #0, tick = 200000 Thread #1, tick = 250000 Thread #0, tick = 250000 Thread #0, tick = 300000 Thread #1, tick = 300000 Thread #1, tick = 350000 Thread #0, tick = 350000 Thread #0, tick = 400000 Thread #1, tick = 400000This output is produced because a time-sliced system divides the CPU into time slots and iteratively gives each of the equal-and-highest priority threads a time slot in which to run. The time-sliced system iterates through the equal-and-highest priority threads, allowing each one a bit of time to run, until one or more of them finishes or until a higher priority thread preempts them. Notice that time-slicing makes no guarantees as to how often or in what order threads are scheduled to run.
When running this program on a non-time-sliced system, however, you will see messages from one thread finish printing before the other thread ever gets a chance to print one message. Like this:
Thread #0, tick = 50000 Thread #0, tick = 100000 Thread #0, tick = 150000 Thread #0, tick = 200000 Thread #0, tick = 250000 Thread #0, tick = 300000 Thread #0, tick = 350000 Thread #0, tick = 400000 Thread #1, tick = 50000 Thread #1, tick = 100000 Thread #1, tick = 150000 Thread #1, tick = 200000 Thread #1, tick = 250000 Thread #1, tick = 300000 Thread #1, tick = 350000 Thread #1, tick = 400000This is because a non-time-sliced system chooses one of the equal-and-highest priority threads to run and allows that thread to run until it relinquishes the CPU (by sleeping, yielding, finishing its job) or until a higher priority preempts it.
The Java runtime does not implement (and therefore does not guarantee) time-slicing. However, some systems on which you can run Java do support time-slicing. Your Java programs should not rely on time-slicing as it may produce different results on different systems.
Compile and run the RaceDemo and SelfishRunner classes on your computer. Can you tell if you have a time-sliced system?
As you can imagine, writing CPU-intensive code can have negative repercussions on other threads running in the same process. In general, you should try to write "well-behaved" threads that voluntarily relinquish the CPU periodically and give other threads an opportunity to run. In particular, you should never write Java code that relies on time-sharing--this will practically guarantee that your program will give different results on different computer systems.
A thread can voluntarily yield the CPU without going to sleep or some other drastic means by calling the yield method. The yield method gives other threads of the same priority a chance to run. If there are no equal priority threads that are runnable, then the yield is ignored.
Rewrite the SelfishRunner class to be a PoliteRunner by calling the yield method from the run method. Be sure to modify the main program to create PoliteRunners instead of SelfishRunners. Compile and run the new classes on your computer. Now isn't that better?
Summary
- Most computers have only one CPU, so threads must share the CPU with other threads. The execution of multiple threads on a single CPU, in some order, is called scheduling. The Java runtime supports a very simple, deterministic scheduling algorithm known as fixed priority scheduling.
- Each Java thread is given a numeric priority between MIN_PRIORITY and MAX_PRIORITY (constants defined in the Thread class). At any given time, when multiple threads are ready to be executed, the thread with the highest priority is chosen for execution. Only when that thread stops, or is suspended for some reason, will a lower priority thread start executing.
- Scheduling of the CPU is fully preemptive. If a thread with a higher priority than the currently executing thread needs to execute, the higher priority thread is immediately scheduled.
- The Java runtime will not preempt the currently running thread for another thread of the same priority. In other words, the Java runtime does not time-slice. However, the system implementation of threads underlying the Java Thread class may support time-slicing. Do not write code that relies on time-slicing.
- In addition, a given thread may, at any time, give up its right to execute by calling the yield method. Threads can only yield the CPU to other threads of the same priority--attempts to yield to a lower priority thread are ignored.
- When all the runnable threads in the system have the same priority, the scheduler chooses the next thread to run in a simple, non-preemptive, round-robin scheduling order.
Synchronizing Threads
So far, this lesson has contained examples with independent, asynchronous threads. That is, each thread contained all of the data and methods required for its execution and didn't require any outside resources or methods. In addition, the threads in those examples ran at their own pace without concern over the state or activities of any other concurrently running threads.However, there are many interesting situations where separate, concurrently running threads do share data and must consider the state and activities of other threads. One such set of programming situations are known as producer/consumer scenarios where the producer generates a stream of data which then is consumed by a consumer.
For example, imagine a Java application where one thread (the producer) writes data to a file while a second thread (the consumer) reads data from the same file. Or, as you type characters on the keyboard, the producer thread places key events in an event queue and the consumer thread reads the events from the same queue. Both of these examples use concurrent threads that share a common resource: the first shares a file, the second shares an event queue. Because the threads share a common resource, they must be synchronized in some way.
Grouping Threads
Every Java thread is a member of a thread group. Thread groups provide a mechanism for collecting multiple threads into a single object and manipulating those threads all at once, rather than individually. For example, you can start or suspend all the threads within a group with a single method call. Java thread groups are implemented by the ThreadGroup class in the java.lang package.The runtime system puts a thread into a thread group during thread construction. When you create a thread, you can either allow the runtime system to put the new thread in some reasonable default group or you can explicitly set the new thread's group. The thread is a permanent member of whatever thread group it joins upon its creation--you cannot move a thread to a new group after the thread has been created.
The Default Thread Group
If you create a new Thread without specifying its group in the constructor, the runtime system automatically places the new thread in the same group as the thread that created it (known as the current thread group and the current thread, respectively). So, if you leave the thread group unspecified when you create your thread, what group contains your thread?When a Java application first starts up, the Java runtime system creates a ThreadGroup named main. Unless specified otherwise, all new threads that you create become members of the main thread group.
If you create a thread within an applet, the new thread's group may be something other than main, depending on the browser or viewer that the applet is running in.
Many Java programmers ignore thread groups altogether and allow the runtime system to handle all of the details regarding thread groups. However, if your program creates a lot of threads that should be manipulated as a group, or if you are implementing a custom security manager, you will likely want more control over thread groups. Continue reading for more details!
Creating a Thread Explicitly in a Group
As mentioned previously, a thread is a permanent member of whatever thread group it joins when its created--you cannot move a thread to a new group after the thread has been created. Thus, if you wish to put your new thread in a thread group other than the default, you must specify the thread group explicitly when you create the thread. The Thread class has three constructors that let you set a new thread's group:public Thread(ThreadGroup group, Runnable runnable) public Thread(ThreadGroup group, String name) public Thread(ThreadGroup group, Runnable runnable, String name)Each of these constructors creates a new thread, initializes it based on the Runnable and String parameters, and makes the new thread a member of the specified group. For example, the following code sample creates a thread group (myThreadGroup) and then creates a thread (myThread) in that group.
ThreadGroup myThreadGroup = new ThreadGroup( "My Group of Threads"); Thread myThread = new Thread(myThreadGroup, "a thread for my group");The ThreadGroup passed into a Thread constructor does not necessarily have to be a group that you create--it can be a group created by the Java runtime system, or a group created by the application in which your applet is running.
Getting a Thread's Group
To find out what group a thread is in, you can call its getThreadGroup method:theGroup = myThread.getThreadGroup();
Package Support of Threads
- java.lang.Thread.
- In the Java development enviroment, threads are objects that derive from java.lang's Thread class. The Thread class defines and implements Java threads. You can subclass the Thread class to provide your own thread implementations or you can use the Runnable interface.
- java.lang.Runnable
- The Java language library also defines the Runnable interface, which allows any class to provide the body (the run method) for a thread.
- java.lang.Object
- The root class, Object, defines three methods you can use to synchronize methods around a condition variable: wait, notify, and notifyAll.
- java.lang.ThreadGroup
- All threads belong to a thread group, which typically contains related threads. The ThreadGroup class in the java.lang package implements groups of threads.
- java.lang.ThreadDeath
- A thread is normally killed by throwing a ThreadDeath object at it. Rarely, a thread might need to catch ThreadDeath to clean up before it dies.
Language Support of Threads
The Java language has two keywords related to the synchronization of threads: volatile (which is not implemented in JDK 1.0) and synchronized. Both of these language features help ensure the integrity of data that is shared between two concurrently running threads. Multithreaded Programs discusses thread synchronization issues.
Runtime Support of Threads
The Java runtime system contains the scheduler, which is responsible for running all the existing threads. The Java scheduler uses a fixed priority scheduling algorithm which boils down to this simple rule of thumb:At any given time, the highest priority thread is running. However, this is not guaranteed. The thread scheduler may choose to run a lower priority thread to avoid starvation. For this reason, use priority only to affect scheduling policy for efficiency purposes. Do not rely on thread priority for algorithm correctness.
The Producer/Consumer Example
The Producer generates an integer between 0 and 9 (inclusive), stores it in a CubbyHole object, and prints the generated number. To make the synchronization problem more interesting, the Producer sleeps for a random amount of time between 0 and 100 milliseconds before repeating the number generating cycle:
public class Producer extends Thread { private CubbyHole cubbyhole; private int number; public Producer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { for (int i = 0; i < 10; i++) { cubbyhole.put(i); System.out.println("Producer #" + this.number + " put: " + i); try { sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { } } } }The Consumer, being ravenous, consumes all integers from the CubbyHole (the exact same object into which the Producer put the integers in the first place) as quickly as they become available.
public class Consumer extends Thread { private CubbyHole cubbyhole; private int number; public Consumer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { int value = 0; for (int i = 0; i < 10; i++) { value = cubbyhole.get(); System.out.println("Consumer #" + this.number + " got: " + value); } } }The Producer and Consumer in this example share data through a common CubbyHole object. And you will note that neither the Producer nor the Consumer makes any effort whatsoever to ensure that the Consumer is getting each value produced once and only once. The synchronization between these two threads actually occurs at a lower level, within the get and put methods of the CubbyHole object. However, let's assume for a moment that these two threads make no arrangements for synchronization and talk about the potential problems that might arise in that situation.
One problem arises when the Producer is quicker than the Consumer and generates two numbers before the Consumer has a chance to consume the first one. Thus the Consumer would skip a number. Part of the output might look like this:
Another problem that might arise is when the Consumer is quicker than the Producer and consumes the same value twice. In this situation, the Consumer would print the same value twice and might produce output that looked like this:. . . Consumer #1 got: 3 Producer #1 put: 4 Producer #1 put: 5 Consumer #1 got: 5 . . .. . . Producer #1 put: 4 Consumer #1 got: 4 Consumer #1 got: 4 Producer #1 put: 5 . . .Either way, the result is wrong. You want the Consumer to get each integer produced by the Producer exactly once. Problems such as those just described are called race conditions. They arise from multiple, asynchronously executing threads trying to access a single object at the same time and getting the wrong result.
Race conditions in the producer/consumer example are prevented by having the storage of a new integer into the CubbyHole by the Producer be synchronized with the retrieval of an integer from the CubbyHole by the Consumer. The Consumer must consume each integer exactly once.
The activities of the Producer and Consumer must be synchronized in two ways. First, the two threads must not simultaneously access the CubbyHole. A Java thread can prevent this from happening by locking an object. When an object is locked by one thread and another thread tries to call a synchronized method on the same object, the second thread will block until the object is unlocked.
And second, the two threads must do some simple coordination. That is, the Producer must have some way to indicate to the Consumer that the value is ready and the Consumer must have some way to indicate that the value has been retrieved. The Thread class provides a collection of methods--wait, notify, and notifyAll--to help threads wait for a condition and notify other threads of when that condition changes.
The Main Program
Here's a small stand-alone Java application that creates a CubbyHole object, a Producer, a Consumer, and then starts both the Producer and the Consumer.
public class ProducerConsumerTest { public static void main(String[] args) { CubbyHole c = new CubbyHole(); Producer p1 = new Producer(c, 1); Consumer c1 = new Consumer(c, 1); p1.start(); c1.start(); } }
The Output
Here's the output of ProducerConsumerTest.
Producer #1 put: 0 Consumer #1 got: 0 Producer #1 put: 1 Consumer #1 got: 1 Producer #1 put: 2 Consumer #1 got: 2 Producer #1 put: 3 Consumer #1 got: 3 Producer #1 put: 4 Consumer #1 got: 4 Producer #1 put: 5 Consumer #1 got: 5 Producer #1 put: 6 Consumer #1 got: 6 Producer #1 put: 7 Consumer #1 got: 7 Producer #1 put: 8 Consumer #1 got: 8 Producer #1 put: 9 Consumer #1 got: 9
Locking an Object
The code segments within a program that access the same object from separate, concurrent threads are called critical sections. In the Java language, a critical section can be a block or a method and are identified with the synchronized keyword. The Java platform then associates a lock with every object that has synchronized code.
In the producer/consumer example, the put and get methods of the CubbyHole are the critical sections. The Consumer should not access the CubbyHole when the Producer is changing it, and the Producer should not modify it when the Consumer is getting the value. So put and get in the CubbyHole class should be marked with the synchronized keyword.
Here's a code skeleton for the CubbyHole class:
public class CubbyHole { private int contents; private boolean available = false; public synchronized int get() { ... } public synchronized void put(int value) { ... } }Note that the method declarations for both put and get contain the synchronized keyword. Hence, the system associates a unique lock with every instance of CubbyHole (including the one shared by the Producer and the Consumer). Whenever control enters a synchronized method, the thread that called the method locks the object whose method has been called. Other threads cannot call a synchronized method on the same object until the object is unlocked. So, when the Producer calls CubbyHole's put method, it locks the CubbyHole, thereby preventing the Consumer from calling the CubbyHole's get method:
public synchronized void put(int value) { // CubbyHole locked by the Producer .. // CubbyHole unlocked by the Producer }When the put method returns, the Producer unlocks the CubbyHole. Similarly, when the Consumer calls CubbyHole's get method, it locks the CubbyHole, thereby preventing the Producer from calling put:
public synchronized int get() { // CubbyHole locked by the Consumer ... // CubbyHole unlocked by the Consumer }The acquisition and release of a lock is done automatically and atomically by the Java runtime system. This ensures that race conditions cannot occur in the underlying implementation of the threads, thus ensuring data integrity. Synchronization isn't the whole story. The two threads must also be able to notify one another when they've done their job. Learn more about that after a brief foray into reentrant locks.
Using the notifyAll and wait Methods
The CubbyHole stores its value in a private member variable called contents. CubbyHole has another private member variable, available, that is a boolean. available is true when the value has just been put but not yet gotten and is false when the value has been gotten but not yet put. So, here's one possible implementation for the put and get methods:
public synchronized int get() { // won't work! if (available == true) { available = false; return contents; } } public synchronized void put(int value) { // won't work! if (available == false) { available = true; contents = value; } }As implemented, these two methods won't work. Look at the get method. What happens if the Producer hasn't put anything in the CubbyHole and available isn't true? get does nothing. Similarly, if the Producer calls put before the Consumer got the value, put doesn't do anything.
You really want the Consumer to wait until the Producer puts something in the CubbyHole and the Producer must notify the Consumer when it's done so. Similarly, the Producer must wait until the Consumer takes a value (and notifies the Producer of its activities) before replacing it with a new value. The two threads must coordinate more fully and can use Object's wait and notifyAll methods to do so.
Here are the new implementations of get and put that wait on and notify each other of their activities:
public synchronized int get() { while (available == false) { try { // wait for Producer to put value wait(); } catch (InterruptedException e) { } } available = false; // notify Producer that value has been retrieved notifyAll(); return contents; } public synchronized void put(int value) { while (available == true) { try { // wait for Consumer to get value wait(); } catch (InterruptedException e) { } } contents = value; available = true; // notify Consumer that value has been set notifyAll(); }The code in the get method loops until the Producer has produced a new value. Each time through the loop, get calls the wait method. The wait method relinquishes the lock held by the Consumer on the CubbyHole (thereby allowing the Producer to get the lock and update the CubbyHole) and then waits for notification from the Producer. When the Producer puts something in the CubbyHole, it notifies the Consumer by calling notifyAll. The Consumer then comes out of the wait state, available is now true, the loop exits, and the get method returns the value in the CubbyHole.
The put method works in a similar fashion, waiting for the Consumer thread to consume the current value before allowing the Producer to produce a new one.
The notifyAll method wakes up all threads waiting on the object in question (in this case, the CubbyHole). The awakened threads compete for the lock. One thread gets it, and the others go back to waiting. The Object class also defines the notify method, which arbitrarily wakes up one of the threads waiting on this object.
The Object class contains not only the version of wait that is used in the producer/consumer example and which waits indefinitely for notification, but also two other versions of the wait method:
wait(long timeout) Waits for notification or until the timeout period has elapsed. timeout is measured in milliseconds. wait(long timeout, int nanos) Waits for notification or until timeout milliseconds plus nanos nanoseconds have elapsed. Besides using these timed wait methods to synchronize threads, you also can use them in place of sleep. Both wait and sleep delay for the requested amount of time, but you can easily wake up wait with a notify but a sleeping thread cannot be awakened prematurely. This doesn't matter too much for threads that don't sleep for long, but it could be important for threads that sleep for minutes at a time.
Grouping Threads
Every Java thread is a member of a thread group. Thread groups provide a mechanism for collecting multiple threads into a single object and manipulating those threads all at once, rather than individually. For example, you can start or suspend all the threads within a group with a single method call. Java thread groups are implemented by the ThreadGroup class in the java.lang package.The runtime system puts a thread into a thread group during thread construction. When you create a thread, you can either allow the runtime system to put the new thread in some reasonable default group or you can explicitly set the new thread's group. The thread is a permanent member of whatever thread group it joins upon its creation--you cannot move a thread to a new group after the thread has been created.
The Default Thread Group
If you create a new Thread without specifying its group in the constructor, the runtime system automatically places the new thread in the same group as the thread that created it (known as the current thread group and the current thread, respectively). So, if you leave the thread group unspecified when you create your thread, what group contains your thread?
When a Java application first starts up, the Java runtime system creates a ThreadGroup named main. Unless specified otherwise, all new threads that you create become members of the main thread group.
If you create a thread within an applet, the new thread's group may be something other than main, depending on the browser or viewer that the applet is running in. Refer to Threads in Appletsfor information about thread groups in applets.
Many Java programmers ignore thread groups altogether and allow the runtime system to handle all of the details regarding thread groups. However, if your program creates a lot of threads that should be manipulated as a group, or if you are implementing a custom security manager, you will likely want more control over thread groups. Continue reading for more details!
Creating a Thread Explicitly in a Group
As mentioned previously, a thread is a permanent member of whatever thread group it joins when its created--you cannot move a thread to a new group after the thread has been created. Thus, if you wish to put your new thread in a thread group other than the default, you must specify the thread group explicitly when you create the thread. The Thread class has three constructors that let you set a new thread's group:
public Thread(ThreadGroup group, Runnable runnable) public Thread(ThreadGroup group, String name) public Thread(ThreadGroup group, Runnable runnable, String name)Each of these constructors creates a new thread, initializes it based on the Runnable and String parameters, and makes the new thread a member of the specified group. For example, the following code sample creates a thread group (myThreadGroup) and then creates a thread (myThread) in that group.
ThreadGroup myThreadGroup = new ThreadGroup( "My Group of Threads"); Thread myThread = new Thread(myThreadGroup, "a thread for my group");The ThreadGroup passed into a Thread constructor does not necessarily have to be a group that you create--it can be a group created by the Java runtime system, or a group created by the application in which your applet is running.
Getting a Thread's Group
To find out what group a thread is in, you can call its getThreadGroup method:theGroup = myThread.getThreadGroup();
The ThreadGroup Class
Once you've obtained a thread's ThreadGroup, you can query the group for information, such as what other threads are in the group. You can also modify the threads in that group, such as suspending, resuming, or stopping them, with a single method invocation.
Other Thread Information
- Threads in Applets
- When you write applets that use threads, you may have to make special provisions, such as ensuring that your applet is well-behaved. Also, some browsers impose security restrictions for applets based on which thread group a thread is in.
- Concurrent Programming in Java, Second Edition
- For further information about threads programming, see Doug Lea's highly acclaimed book written for intermediate and advanced threads programmers. The second edition is available from amazon.com.
![]()