Other Thread-Scheduling Methods

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.

The suspend() and resume() Methods

There are two methods that can directly affect the state of a thread:

void suspend() (deprecated in Java 2)

Prevents a thread from running for an indefinite amount of time.

void resume() (deprecated in Java 2)

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.

Alternatives to the suspend() and resume() methods

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.

The yield() Method

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:

static void yield()

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.

Yielding versus priority-based scheduling

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.

Daemon Threads

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:

void setDaemon(boolean on)

Sets the thread to be a daemon thread (if on is true) or to be a user thread (if on is false).

boolean isDaemon()

Returns true if the thread is a daemon thread and false if it is a user thread.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset