There are other methods in the Thread class that affect scheduling. As we’ll see, these remaining methods are not always the most useful techniques with respect to Java scheduling because of the complications that arise in the various native-thread scheduling models and their use of timesliced scheduling. In addition, two of these methods have been deprecated in Java 2 and should not be used in any version of Java. But we’ll complete our look at the API relating to thread scheduling in this section.
There are two methods that can directly affect the state of a thread:
Prevents a thread from running for an indefinite amount of time.
Allows a thread to run after being suspended.
The suspend()
method moves a particular thread
from the runnable state into the blocked state. In this case, the
thread isn’t blocked waiting for a particular resource,
it’s blocked waiting for some thread to resume it. The
resume()
method moves the thread from the
blocked state to the runnable state.
In the section Section 6.1,” earlier in this
chapter, we posited the existence of four thread states. Actually,
the
suspended state is different from the
blocked state, even though there is no real conceptual difference
between them. Strictly speaking, the suspend()
method moves a thread to the suspended state from whatever state the
thread was previously in—including a blocked thread, which can
be suspended just like any other thread. Similarly, the
resume()
method moves the thread from the
suspended state to whatever state the thread was in before it was
suspended—so a thread that has been resumed may still be
blocked. But this is a subtle difference, and we’ll persist in
treating the blocked and suspended states as identical.
A common guideline is to use the suspend()
and
resume()
methods to control the threads within
an applet. This is a good idea: when the applet is not active, you
don’t want its threads to continue to run. Using this
guideline, let’s revise our fractal applet as
follows:
import java.applet.Applet; import java.awt.*; public class Fractal extends Applet implements Runnable { Thread t; public void start() { if (t == null) { t = new Thread(this); t.setPriority(Thread.currentThread().getPriority() - 1); t.start(); } else t.resume(); } public void stop() { t.suspend(); } public void run() { // Do calculations, occasionally calling repaint(). } public void paint(Graphics g) { // Paint the completed sections of the fractal. } }
This example is better than our first fractal code: in the first case, when the user revisited the page with the fractal applet, the fractal calculation would have had to begin at its very beginning and redisplay all those results to the user as they were recalculated. Now, the applet can save the information of the fractal and simply pick up the calculation from the point at which the user interrupted it.
Despite the common use of the suspend()
and
resume()
methods in this and other cases,
there’s a danger lurking in the code that has caused those
methods to become deprecated. This danger exists in all releases of
the Java virtual machine, however, so even though these methods are
not deprecated in Java 1.0 or 1.1, you should not feel comfortable
about using them in those earlier releases. In fact, the
suspend()
and resume()
methods should never actually be used. The reasoning that we’re
about to outline applies to the stop()
method as
well, which has been deprecated beginning with Java 2 and should also
be avoided in earlier releases.
The problem with using the suspend()
method is
that it can conceivably lead to cases of lock
starvation—including cases where the starvation shuts down the
entire virtual machine. If a thread is suspended while it is holding
a lock, that lock remains held by the suspended thread. As long as
that thread is suspended, no other thread can obtain the lock in
question. Depending on the lock in question, all threads may
eventually end up blocked, waiting for the lock.
You may think that with careful programming you can avoid this situation, by never suspending a thread that holds a lock. However, there are many locks internal to the Java API and the virtual machine itself that you don’t know about, so you can never ensure that a thread that you want to suspend does not hold any locks. Worse yet, consider what happens if a thread is suspended while it is in the middle of allocating an object from the heap. The suspended thread may hold a lock that protects the heap itself, meaning that no other thread will be able to allocate any object. Clearly, this is a bad situation.
This is not an insurmountable problem; it would be possible to
implement the virtual machine in such a way that a thread could not
be suspended while it held a lock, or at least not while it held
certain internal locks within the virtual machine. But Java virtual
machines are not typically written like that, and the specification
certainly does not require it. Hence, the
suspend()
method was deprecated instead. There
is no danger in the resume()
method itself, but
since the resume()
method is useful only with
the suspend()
method, it too has been
deprecated.
A similar situation occurs with the stop()
method. In this case, the danger is not that the lock will be held
indefinitely—in fact, the lock will be released when the thread
stops (details of this procedure are given in Appendix A). The danger here is that a complex data
structure may be left in an unstable state: if the thread that is
being stopped is in the midst of updating a linked list, for example,
the links in the list will be left in an inconsistent state. The
reason we needed to obtain a lock on the list in the first place was
to ensure that the list would not be found by another thread in an
inconsistent state; if we were able to interrupt a thread in the
middle of this operation, we would lose the benefit of its obtaining
the lock. So the stop()
method has been
deprecated as well.
The outcome of this is that no thread should suspend or stop another
thread: a thread should only stop itself (by returning from its
run()
method) or suspend itself (by calling the
wait()
method). It may do this in response to a
flag set by another thread, or by any other method that you may
devise.
In earlier chapters, we showed what to do instead of calling the
stop()
method. Here’s a similar technique
that we can use to avoid calling the suspend()
method:
import java.applet.Applet; import java.awt.*; public class Fractal extends Applet implements Runnable { Thread t; volatile boolean shouldRun = false; Object runLock = new Object(); int nSections; public void start() { if (t == null) { shouldRun = true; t = new Thread(this); t.setPriority(Thread.currentThread().getPriority() - 1); t.start(); } else { synchronized(runLock) { shouldRun = true; runLock.notify(); } } } public void stop() { shouldRun = false; } void doCalc(int i) { // Calculate the ith section of the fractal. } public void run() { for (int i = 0; i < nSections; i++) { doCalc(i); repaint(); synchronized(runLock) { while (shouldRun == false) try { runLock.wait(); } catch (InterruptedException ie) {} } } } public void paint(Graphics g) { // Paint the completed sections of the fractal. } }
The start()
method of the applet is still
responsible for creating and starting the calculation thread; in
addition, it now sets the shouldRun
flag to
true
so that the calculation thread can test to
see that it should calculate sections. When the
run()
method checks this flag and it is
false
, the run()
method waits
for the flag to be true
. This waiting has the same
effect as suspending the calculation thread. Similarly, the
notification provided by the start()
method has
the same effect as resuming the calculation thread.
Suspending the thread is now a two-step process: the applet’s
stop()
method sets a flag and the calculation
thread’s run()
method tests that flag.
Hence, there will be a period of time in this case when the applet is
still calculating fractal sections even though the applet is no
longer visible to the user. In general, there will always be a period
of time using this technique between when you want the thread to stop
or suspend and when the thread actually checks the flag telling it
whether it should suspend itself. But this is a safer way than using
the suspend()
method (and, of course,
there’s no guarantee that the suspend()
method will appear in future versions of the Java platform).
Why isn’t access to the shouldRun flag synchronized
in the applet’s stop() method? Remember that setting
or testing a boolean variable is already an atomic operation, so
there is no need to synchronize the stop()
method since it only needs to perform a single atomic operation. The
other methods synchronize their sections because they are performing
more than one operation on the shouldRun
flag;
in addition, they must hold a lock before they can call the
wait()
or notify()
methods.
A
final method available for affecting
which thread is the currently running thread is the
yield()
method, which is useful because it
allows threads of the same priority to be run:
Yields the current thread, allowing another thread of the same priority to be run by the Java virtual machine.
There are a few points worth noting about the
yield()
method. First, notice that it is a
static method, and as such, only affects the currently running
thread, as in the following code fragment:
public class YieldApplet extends Applet implements Runnable { Thread t; public void init() { t = new Thread(this); } public void paint(Graphics g) { t.yield(); } }
When the applet thread executes the paint()
method and calls the yield()
method, it is the
applet thread itself that yields, and not the calculation thread
t
, even though we used the object
t
to call the yield()
method.
What actually happens when a thread yields? In
terms of the state of the thread, nothing happens: the thread remains
in the runnable state. But logically, the thread is moved to the end
of its priority queue, so the Java virtual machine picks a new thread
to be the currently running thread, using the same rules it always
has. Clearly, there are no threads that are higher in priority than
the thread that has just yielded, so the new currently running thread
is selected among all the threads that have the same priority as the
thread that has just yielded. If there are no other threads in that
group, the yield()
method has no effect: the
yielding thread is immediately selected again as the currently
running thread. In this respect, calling the
yield()
method is equivalent to calling
sleep(0)
.
If there are other threads with the same priority as the yielding thread, then one of those other threads becomes the currently running thread. Thus, yielding is an appropriate technique provided you know that there are multiple threads of the same priority. However, there is no guarantee which thread will be selected: the thread that yields may still be the next one selected by the scheduler, even if there are other threads available at the same priority.
Let’s revisit our fractal example and see how it looks when we
use the yield()
method instead of priority
calls:
import java.applet.Applet;
import java.awt.*;
public class Fractal extends Applet implements Runnable {
Thread t;
volatile boolean shouldRun = false;
Object runLock = new Object();
int nSections;
public void start() {
if (t == null) {
shouldRun = true;
t = new Thread(this);
t.start();
}
else {
synchronized(runLock) {
shouldRun = true;
runLock.notify();
}
}
}
public void stop() {
shouldRun = false;
}
void doCalc(int i) {
// Calculate the ith section of the fractal.
}
public void run() {
for (int i = 0; i < nSections; i++) {
doCalc(i);
repaint();
Thread.yield();
synchronized(runLock) {
while (shouldRun == false)
try {
runLock.wait();
} catch (InterruptedException ie) {}
}
}
}
public void paint(Graphics g) {
// Paint the completed sections of the fractal.
}
}
In this example, we are no longer setting the priority of the
calculation thread to be lower than the other threads in the applet.
Now when our calculation thread has results, it merely yields. The
applet thread is in the runnable state; it was moved to that state
when the calculation thread called the repaint()
method. So the Java virtual machine chooses the applet thread to be
the currently running thread, the applet repaints itself, the applet
thread blocks, and the calculation thread again becomes the currently
running thread and calculates the next section of the fractal.
This example suffers from a few problems. First, because the applet thread is at the same priority as the calculation thread, the user is unable to interact with the applet until the calculation thread yields. If, for example, the user selects a checkbox in the GUI, the program may not take appropriate action until the calculation thread yields. On platforms with native-thread scheduling, that usually will not happen, since the applet thread and the calculation thread will timeslice, but it can be a big problem on green-thread implementations.
Second, there is a race condition in this example—and in all
examples that rely on the yield()
method. This
race condition only occurs if we’re on a native-thread platform
that timeslices between threads of the same priority, and like most
race conditions, it occurs very rarely. In our previous code example,
immediately after the calculation thread yields, it may be time for
the operating system to schedule another thread (or LWP). This means
that the calculation thread may be the next thread to run, even
though it has just yielded. The good news in this case is that the
program continues to execute, and the sections of the fractal get
painted next time the calculation thread yields (or the next time the
operating system schedules the applet thread).
In the worst case, then, a thread that yields may still be the next
thread to run. However, that scenario can only apply when the
operating system is scheduling the threads and the threads are
timeslicing, in which case there was probably no need to use the
yield()
method at all. As long as a program
treats the yield()
method as a hint to the Java
virtual machine that now might be a good time to change the currently
running thread, a program that relies on the
yield()
method will run on green-thread
implementations of the virtual machine (where the
yield()
method will always have the desired
effect) as well as on native-thread implementations.
When you need to have some control over thread scheduling, the
question of which mechanism to use—calling the
yield()
method or adjusting individual thread
priorities—tends to be somewhat subjective, since both methods
have a similar effect on the threads. As is clear from the example we
have used throughout this discussion, we prefer using the
priority-based methods to control thread scheduling. These methods
offer the most flexibility to the Java developer.
We rarely find the yield()
method to be useful.
This may come as a surprise to thread programmers on systems where
the yield()
method is the most direct one for
affecting thread scheduling. But because of the indeterminate nature
of scheduling among threads of the same priority on native-thread
Java implementations, the effect of the yield()
method cannot be guaranteed: a thread that yields may immediately be
rescheduled when the operating system timeslices threads. On the
other hand, if your threads yield often enough, this rare race
condition won’t matter in the long run, and using the
yield()
method can be an effective way to
schedule your threads. The yield()
method is
also simpler to understand than the priority-based methods, which
puts it in great favor with some developers.
The last thing that we’ll address in conjunction with thread scheduling is the issue of daemon threads. There are two types of threads in the Java system: daemon threads and user threads. The implication of these names is that daemon threads are those threads created internally by the Java API and that user threads are those you create yourself, but this is not the case. Any thread can be a daemon thread or a user thread. All threads are created initially as user threads, so all the threads we’ve looked at so far have been user threads.
Some threads that are created by the virtual machine on your behalf are daemon threads. A daemon thread is identical to a user thread in almost every way: it has a priority, it has the same methods, and it goes through the same states. In terms of scheduling, daemon threads are handled just like user threads: neither type of thread is scheduled in favor of the other. During the execution of your program, a daemon thread behaves just like a user thread.
The only time the Java virtual machine checks to see if particular threads are daemon threads is after a user thread has exited. When a user thread exits, the Java virtual machine checks to see if there are any remaining user threads left. If there are user threads remaining, then the Java virtual machine, using the rules we’ve discussed, schedules the next thread (user or daemon). If, however, there are only daemon threads remaining, then the Java virtual machine will exit and the program will terminate. Daemon threads only live to serve user threads; if there are no more user threads, there is nothing to serve and no reason to continue.
The canonical daemon thread in the reference implementation of the Java virtual machine is the garbage collection thread (on other implementations, the garbage collector need not be a separate thread). The garbage collector runs from time to time and frees those Java objects that no longer have valid references, which is why the Java programmer doesn’t need to worry about memory management. So the garbage collector is a useful thread. If we don’t have any other threads running, however, there’s nothing for the garbage collector to do: after all, garbage is not spontaneously created, at least not inside a Java program. So if the garbage collector is the only thread left running in the Java virtual machine, then clearly there’s no more work for it to do, and the Java virtual machine can exit. Hence, the garbage collector is marked as a daemon thread.
There are two methods in the Thread class that deal with daemon threads:
The setDaemon()
method can be called only after
the thread object has been created and before the thread has been
started. While the thread is running, you cannot cause a user thread
to become a daemon thread (or vice versa); attempting to do so will
generate an exception. To be completely correct, an exception is
generated any time the thread is alive and the
setDaemon()
method is called—even if
setDaemon(true)
is called on a thread that is
already a daemon thread.
By default, a thread is a user thread if it was created by a user
thread; it is a daemon thread if it was created by a daemon thread.
The setDaemon()
method is needed only if one
thread creates another thread that should have a different daemon
status.
Unfortunately, the line between a user thread and a daemon thread may not be that clear. While it is true that a daemon thread is used to service a user thread, the time it takes to accomplish the service may be longer than the lifespan of the user thread that made the request. Furthermore, there may be critical sections of code that should not be interrupted by the exiting of the virtual machine. For example, a thread whose purpose is to back up data does not have a use if there are no user threads that can process the data. However, during this backup of data to a database, the database may not be in a state that can allow the program to exit. Although this backup thread should still be a daemon thread, since it is of no use to the program without the threads that process the data, we may have to declare this thread as a user thread in order to protect the integrity of the database.
Ideally, the solution is to allow the thread to change its state between a user thread and daemon thread at any time. Since this is not allowed by the Java API, we can instead implement a lock that can be used to protect daemon threads. An implementation of the DaemonLock class is as follows:
public class DaemonLock implements Runnable { private int lockCount = 0; public synchronized void acquire() { if (lockCount++ == 0) { Thread t = new Thread(this); t.setDaemon(false); t.start(); } } public synchronized void release() { if (--lockCount == 0) { notify(); } } public synchronized void run() { while (lockCount != 0) { try { wait(); } catch (InterruptedException ex) {}; } } }
Implementation of the DaemonLock class is simple: we protect daemon threads by ensuring that a user thread exists. As long as there is a user thread, the virtual machine will not exit, which will allow the daemon threads to finish the critical section of code. Once the critical section is completed, the daemon thread can release the daemon lock, which will terminate the user thread. If there are no other user threads in the program at that time, the program will exit. The difference, however, is that it will exit outside of the critical section of code.
We’ll see an example use of this class in Chapter 7.