Our next examples show two ways of performing round-robin scheduling. These techniques are mostly useful for programs that will execute on a green-thread implementation of the Java virtual machine, since native-thread implementations all perform some sort of round-robin scheduling already. If you don’t know which implementation your program will eventually run on, you may want to use one of these techniques to ensure round-robin scheduling, even if the underlying implementation is performing it for you: these techniques are not in conflict with native-thread implementations, though they do not necessarily provide a benefit on those platforms.
Remember that at issue here is the behavior of a Java program that contains one or more CPU-intensive threads. A Java program could have hundreds of threads that may only periodically need access to the CPU and otherwise spend most of their life in the blocked state: in that case, there isn’t likely to be much competition for the CPU, and each thread gets an opportunity to run whenever it has work to do. We only face the problem of CPU starvation when there is at least one CPU-intensive thread that may potentially prevent all other threads from running.
If we have only a single CPU-intensive thread, there is no need for a complicated scheduling mechanism: all we need to do is lower the priority of the CPU-intensive thread below the priority of the other threads in our Java program. This allows the other threads to run whenever they have work to do, while the CPU-intensive thread continues to execute whenever the remaining threads are blocked. We’ll build on this principle in our scheduler class: our CPU-intensive threads will all have a lower priority than threads that are mostly blocked.
We’ll look at two schedulers in this section. The basic principle behind each scheduler is that each thread under its control is given a fixed amount of time during which it runs. When the specified time period elapses, another thread runs; this process proceeds indefinitely.
How do we go about creating a round-robin scheduler? Clearly, we need to begin with some sort of periodic timer; every time the timer goes off, we can make a different thread become the currently running thread. What do we need to do to make this happen?
The simplistic answer to this question is: nothing. That is, our simple scheduler is simply a high-priority timer that periodically wakes up only to go back to sleep immediately. This creates, in effect, a timer-based scheduling event: each time the timer thread wakes up, it becomes the currently running thread, which also adjusts the list of threads at the priority of the previously running thread:
public class SimpleScheduler extends Thread { int timeslice; public SimpleScheduler(int t) { timeslice = t; setPriority(Thread.MAX_PRIORITY); setDaemon(true); } public void run() { while (true) try { sleep(timeslice); } catch (Exception e) {} } }
We’ll use this class in the example from the beginning of Chapter 6 so that we can illustrate its behavior:
class TestThread extends Thread { String id; public TestThread(String s) { id = s; } public void doCalc(int i) { } public void run() { int i; for (i = 0; i < 10; i++) { doCalc(i); System.out.println(id); } } } public class Test { public static void main(String args[]) { new SimpleScheduler(100).start(); TestThread t1, t2, t3; t1 = new TestThread("Thread 1"); t1.start(); t2 = new TestThread("Thread 2"); t2.start(); t3 = new TestThread("Thread 3"); t3.start(); } }
In this program there are three threads (t1
,
t2
, and t3
) at the Java default
priority of NORM_PRIORITY
, and the
SimpleScheduler thread that runs at a priority of
MAX_PRIORITY
. The SimpleScheduler thread is
normally blocked, so the list of threads starts out in this state:
PRIORITY 5: t2 -> t3 -> t1 -> NULL BLOCKED: SimpleScheduler -> NULL
At this point, t1
is the currently running
thread, and we’ll start to see output lines that say
“Thread 1.” When SimpleScheduler wakes up, it moves to
the runnable state and, because it is the highest priority thread in
the Java virtual machine, it becomes the currently running thread:
PRIORITY 5: t2 -> t3 -> t1 -> NULL PRIORITY 10: SimpleScheduler -> NULL
SimpleScheduler immediately executes the sleep()
method, moving it back to the blocked state; the Java virtual machine
then selects the next thread in the list (t2
) as
the currently running thread and moves it to the end of the list:
PRIORITY 5: t3 -> t1 -> t2 -> NULL BLOCKED: SimpleScheduler -> NULL
As this continues, each thread in the list of threads at priority 5 becomes the currently running thread in turn.
This scheduler requires that the virtual machine reorder the threads on a priority list whenever one of them is selected to run. As we mentioned in the last chapter, this is almost universally the case, but it is not a requirement of the Java specification, and we know of one real-time operating system on which this scheduling mechanism does not work.
Note that this mechanism still works for native-thread implementations. On a Windows implementation, the effect is that the currently running thread changes more often than specified by the sleep value within the SimpleScheduler, since the operating system will sometimes change the currently running thread while the scheduler is sleeping. On a Solaris implementation, the reordering of the threads will be dependent on the number of LWPs, but the interruption is sufficient to cause a single LWP to schedule another thread, which achieves the desired effect.
Now we’ll look into building a more complete scheduler that will schedule our threads in a round-robin fashion. We can also use it to limit round-robin scheduling on native-thread platforms that timeslice as their default behavior; this limiting is achieved simply by using a very large value as the timeslice that the scheduler gives to a particular thread. However, since there are circumstances on native-thread platforms where the highest priority thread is not necessarily the currently running thread, we cannot completely prevent some sort of round-robin scheduling on those platforms: the best we can do is to use this scheduler to bias the operating system to favor one particular thread.
The example we outline in this section assumes that there is a single CPU. If you need to use this technique on a machine with multiple CPUs, you will need to adjust the scheduler so that it creates N currently running threads rather than one currently running thread (where N is the number of processors on the machine). As written, this technique will work on machines with multiple processors—that is, it will prevent any CPU starvation—but it will have less of an effect on the overall scheduling of the threads.
We’ll start building this scheduler by establishing threads at three priority levels:
The scheduler itself is a separate thread running at level 6. This allows it to run in favor of the default threads created by the Java virtual machine and APIs and in favor of any threads the scheduler is controlling. This thread spends most of its time sleeping (i.e., blocked), so it doesn’t usually become the currently running thread.
The scheduler selects one thread from all the threads it is controlling and assigns that thread a priority value of 4. Most of the time, this is the nonblocked thread with the highest priority in the Java virtual machine, so it is the thread favored to become the currently running thread.
All remaining threads under control of our scheduler run at priority level 2. Since there is always a thread running at level 4, these threads usually do not run at this priority; they remain at this priority until they are selected by our scheduler to have a priority level of 4, at which time they become favored to be the currently running thread.
The idea behind the scheduler is that the programmer assigns certain threads to be under control of the scheduler. The scheduler selects one and only one of these threads and assigns it a priority of 4, while the rest of the threads have a priority of 2. The priority 4 thread is the currently running thread; from time to time, the scheduler itself wakes up and selects a different thread as the single priority 4 thread. On green-thread platforms, the priority 4 thread will always be selected as the currently running thread; on native-thread platforms, it will usually be selected as the currently running thread.
For all the threads in this scheduling system—the scheduler thread itself plus any threads the programmer designates to be controlled by our scheduler—it is clear that no CPU starvation will occur: the scheduler thread will always run when it needs to, and as long as that thread correctly adjusts the priorities of the remaining threads under its control, all other threads will get their opportunity to become the currently running thread.
In order to keep track of all the threads, we’ll use the
CircularList we developed in Chapter 5. This class gives us the queueing behavior we
need to keep track of the threads under the control of our scheduler:
we can add threads to the list with its
insert()
method, remove them with its
delete()
method, and, more important, go through
the list by repeatedly calling its getNext()
method.
Here’s the first pass at our scheduler:
public class CPUScheduler extends Thread { private int timeslice; // # of milliseconds thread should run private CircularList threads; // All the threads we're scheduling public volatile boolean shouldRun = false; // Exit when this is set public CPUScheduler(int t) { threads = new CircularList(); timeslice = t; } public void addThread(Thread t) { threads.insert(t); t.setPriority(2); } public void removeThread(Thread t) { t.setPriority(5); threads.delete(t); } public void run() { Thread current; setPriority(6); while (shouldRun) { current = (Thread) threads.getNext(); if (current == null) return; current.setPriority(4); try { Thread.sleep(timeslice); } catch (InterruptedException ie) {}; current.setPriority(2); } } }
Although there are some necessary adjustments that we’ll add to this scheduler throughout the rest of this chapter, this code is the essence of the scheduler. The refinements that we’ll add are important in terms of making the class robust and thread safe, but they don’t add to the basic functionality: we want to understand the functionality before we look at some of the subtle issues involved in this class.
The programmer uses two methods to interface with the scheduler:
addThread()
, which adds a thread to the list of
thread objects under control of the scheduler, and
removeThread()
, which removes a thread object
from that list.[10]
Given this interface, we can use the CPUScheduler class in the ThreadTest class we introduced at the beginning of this section:
class TestThread extends Thread { String id; public TestThread(String s) { id = s; } public void doCalc(int i) { } public void run() { int i; for (i = 0; i < 10; i++) { doCalc(i); System.out.println(id); } } } public class Test { public static void main(String args[]) { CPUScheduler c = new CPUScheduler(100); TestThread t1, t2, t3; t1 = new TestThread("Thread 1"); t2 = new TestThread("Thread 2"); t3 = new TestThread("Thread 3"); c.addThread(t1); c.addThread(t2); c.addThread(t3); t1.start(); t2.start(); t3.start(); c.start(); } }
When our program calls
c.start()
, the CPUScheduler’s
run()
method gets called; it is this
run()
method that actually manipulates all the
threads to create the timesliced, round-robin scheduling. At its base
level, the logic for our scheduler is simple: it loops forever, going
through all the threads in our circular list of threads and adjusting
their priorities as it goes. In between, it sleeps for
timeslice
milliseconds. The
current
thread runs for that many milliseconds
before the scheduler wakes up again and readjusts the thread’s
priority. When there are no threads left to schedule—which
would happen if the programmer had called
removeThread()
on all the threads previously
added—the CPUScheduler exits by returning from the
run()
method.
Let’s examine how the four threads in our program—threads
t1
, t2
,
t3
, and the CPUScheduler thread—will
behave now. After we call the c.start()
method,
the threads in the program are in this state:
PRIORITY 2: t1 -> t2 -> t3 -> NULL PRIORITY 6: CPUScheduler -> NULL
As the highest priority thread in the program, the CPUScheduler
thread is the currently running thread. It starts executing the
run()
method, where the first thing it does is
change the priority of thread t1
to 4:
PRIORITY 2: t2 -> t3 -> NULL PRIORITY 4: t1 -> NULL PRIORITY 6: CPUScheduler -> NULL
The CPUScheduler, still the currently running thread, now sleeps,
placing it into the blocked state. This causes
t1
to become the currently running thread:
PRIORITY 2: t2 -> t3 -> NULL PRIORITY 4: t1 -> NULL BLOCKED: CPUScheduler -> NULL
When the CPUScheduler thread wakes up, it changes the priority of
t1
back to 2 and the priority of
t2
to 4:
PRIORITY 2: t3 -> t1 -> NULL PRIORITY 4: t2 -> NULL PRIORITY 6: CPUScheduler -> NULL
And so the cycle continues.
Now that we have the base logic of the CPUScheduler written correctly, we need to make sure the CPUScheduler class is itself thread safe and that we haven’t introduced any race conditions into the scheduler by having incorrectly synchronized data. We’ll go through this process in a series of stages because the example illustrates the necessary steps that you must take in designing any class to work with multiple threads.
At first glance, there don’t appear to be any variables that
need synchronization: the only instance variable that needs to be
protected is the variable threads
, and all
changes to the threads
variable occur via
methods of the CircularList class that are already synchronized. But
what would happen if you called the
remove-Thread()
method and removed the thread
that the CPUScheduler has marked as the current
thread? It would be an error for the CPUScheduler to change the
priority of this thread once it has been removed from the
threads
list, so the
removeThread()
method must somehow inform the
CPUScheduler that the current
thread has been
removed.
This means that the variable current
must become
an instance variable so that both the run()
and
removeThread()
methods can access it. We can then
synchronize access to that variable. Here’s the new
CPUScheduler class:
public class CPUScheduler extends Thread { ... private Thread current; public void removeThread(t) { t.setPriority(5); threads.delete(t); synchronized(this) { if (current == t) current = null; } } ... public void run() { ... try { Thread.sleep(timeslice); } catch (InterruptedException ie) {}; synchronized(this) { if (current != null) current.setPriority(2); } } }
Alternatively, we could make the run()
and
removeThread()
methods themselves synchronized:
public synchronized void run() { ... } public synchronized void removeThread(Thread t) { ... }
As we’ve seen, making the run()
method
synchronized is typically a bad idea, so we’ll reject this idea
for now, but we’ll be revisiting this decision soon.
We’ve synchronized all the variables of our CPUScheduler, but we’re still not protected from threads that exit while they are under our control.
In
particular, the run()
method changes the
priority of a thread, which is a valid operation only if a thread is
in the runnable state. What happens if the thread that we’ve
assigned to level 4 exits its run()
method while
our CPUScheduler is sleeping? When the CPUScheduler wakes up, it
tries to set the priority of that thread, which is now in the exiting
state, to 2—an operation that generates an exception.
Similarly, if the thread that is running decides to call the
stop()
method of one of the priority 2 threads
in the CPUScheduler’s list, then the next time the CPUScheduler
selects that thread and sets its priority, we’ll get an
exception.
So we need to place all the calls to the
setPriority()
method inside a
try
/catch
clause in order to be
alerted to these types of situations. This means we must modify our
code everywhere we call the setPriority()
method:
public void removeThread(Thread t) { try { t.setPriority(5); } catch(Exception e) {} threads.delete(t); synchronized(this) { if (current == t) current = null; } } public void run() { while (shouldRun) { ... try { current.setPriority(4); } catch (Exception e) { removeThread(current); } ... synchronized(this) { if (current != null) try { current.setPriority(2); } catch (Exception e) { removeThread(current); } } ... } }
Note that in in the run()
method, when the
exception is thrown we need to remove the thread from the list of
threads we’re interested in, which means that we must also use
the catch
clause in the
removeThread()
method.
We’ve made the methods of the CPUScheduler thread-safe, but what about the class itself? What if two threads try to create a CPUScheduler? This would be very confusing: we’d end up with two scheduling threads that would compete with each other to schedule other threads. So we need to allow only one instance of the class to be instantiated. We’ll do this by creating a static variable in the class and testing it to make sure that an instance of the CPUScheduler class doesn’t already exist. Because we can’t make the constructor itself synchronized, we’ll also need to introduce a synchronized method to access this static variable. Thus the constructor and related code for the class now look like this:
public class CPUScheduler extends Thread { private static boolean initialized = false; private synchronized static boolean isInitialized() { if (initialized) return true; initialized = true; return false; } public CPUScheduler(int t) { if (isInitialized()) throw new SecurityException("Already initialized"); threads = new CircularList(); timeslice = t; } }
If all the threads under its control exit, the CPUScheduler itself exits. In a program where the tasks are well defined at the beginning of execution—like the TestThread class we’ve looked at so far—that might be fine. But what if we wanted to add the CPUScheduler to our TCPServer? As presently written, the CPUScheduler wouldn’t work for that case: as soon as no clients were connected to the TCPServer, the CPUScheduler would exit, and any further clients that connected to the server would not be timesliced.
Instead, we need to make the CPUScheduler a
daemon thread
and adjust the logic of its run()
method. This
should make sense: the CPUScheduler is only useful when there are
other threads in the program that it can schedule. In the TCP-Server
case, there will always be at least one other thread in the program:
the listener thread of the TCPServer. That listener thread creates
other threads for the CPUScheduler to manipulate as clients connect
to the server. The implementation of our timesliced TCPServer to
perform calculations looks like this:
import java.net.*; import java.io.*; public class CalcServer { public static void main(String args[]) { CalcRequest r = new CalcRequest(); try { r.startServer(3535); } catch (Exception e) { System.out.println("Unable to start server"); } } } class CalcRequest extends TCPServer { CPUScheduler scheduler; CalcRequest() { scheduler = new CPUScheduler(100); scheduler.start(); } void doCalc(Socket s) { } public void run(Socket s) { scheduler.addThread(Thread.currentThread()); doCalc(s); } }
Every time the run()
method of the CalcRequest
class is called, it is called in a new thread, so we need to add that
thread to the CPUScheduler that was created in the constructor of the
class. As long as the CPUScheduler doesn’t exit when there are
no threads to schedule (which now means simply that no client is
currently connected), we’ll have a timesliced calculation
server. During an active session of our CalcServer, we’ll have
these threads:
The thread that waits for connections and creates the client threads.
These threads execute the calculation on behalf of a connected client.
The daemon thread performing the scheduling.
We can gracefully shut down the CalcServer by setting the
shouldRun
flag of the server to
false
; eventually the client threads complete
their calculation and exit. When all the client threads have exited,
only the daemon CPUScheduler thread remains in the program, and the
program terminates.
We need to change the CPUScheduler so that instead of returning when there are no threads to be scheduled, it simply waits for more threads. Here’s the entire code for the modified CPUScheduler class (we’ll show the entire class here, since at this point, we have a complete implementation):
public class CPUScheduler extends Thread { private CircularList threads; private Thread current; private int timeslice; private static boolean initialized = false; private boolean needThreads; private static synchronized boolean isInitialized() { if (initialized) return true; initialized = true; return false; } public CPUScheduler(int t) { if (isInitialized()) throw new SecurityException("Already initialized"); threads = new CircularList(); timeslice = t; setDaemon(true); } public synchronized void addThread(Thread t) { t.setPriority(2); threads.insert(t); if (needThreads) { needThreads = false; notify(); } } public void removeThread(Thread t) { threads.delete(t); synchronized(this) { if (t == current) current = null; } } public synchronized void run() { setPriority(6); while (true) { current = (Thread) threads.getNext(); while (current == null) { needThreads = true; try { wait(); } catch (Exception e) {} current = (Thread) threads.getNext(); } try { current.setPriority(4); } catch (Exception e) { removeThread(current); continue; } try { wait(timeslice); } catch (InterruptedException ie) {}; if (current != null) { try { current.setPriority(2); } catch (Exception e) { removeThread(current); } } } } }
In the constructor, we’ve set the thread to be a daemon
thread—the point of this adjustment. Note that we also changed
the run()
method so that when we try to retrieve
a thread from the list, we loop until one is available. If no thread
is in the list, we wait until one is available, which requires that
we add a flag to the addThread()
method to
signify whether it should notify the CPUScheduler thread that a
thread has been added.
In addition, note that we’ve changed the
run()
method itself to a synchronized method and
replaced the call to the sleep()
method with a
call to the wait()
method. This is one example
of the exception to the general rule that the
run()
method should not be synchronized: since
we actually spend more time waiting in this method than executing
code, its quite okay to synchronize the run()
method, since it will release the lock whenever it waits for
something to happen.
What happens in our scheduler if the currently running thread blocks? Let’s see what would happen to our TestThread program if the currently running thread suddenly entered the blocked state. We’d start out with the threads in a state like this:
PRIORITY 2: t3 -> t1 -> NULL PRIORITY 4: t2 -> NULL BLOCKED: CPUScheduler -> NULL
Thread t2
is the currently running thread,
executing its calculations while the CPUScheduler is sleeping. If
t2
now enters the blocked state for some reason,
we end up with threads in this state:
PRIORITY 2: t3 -> t1 -> NULL PRIORITY 4: NULL BLOCKED: t2 -> CPUScheduler -> NULL
This means that t3
becomes the currently running
thread, even though it’s at priority 2. When the CPUScheduler
wakes up, it resets the priority of t2
to 2,
sets the priority of t3
to 4, and goes back to
sleep, leaving our threads in this state:
PRIORITY 2: t1 -> NULL PRIORITY 4: t3 -> NULL BLOCKED: t2 -> CPUScheduler -> NULL
Everything is okay again, but at some point it will be
t2
’s turn to be priority 4. Since the
CPUScheduler has no way of determining that t2
is blocked, it sets the priority of t2
to 4. The
Java scheduler again selects one of the threads at priority 2 to be
the currently running thread.
Our code was correct: the threads involved all got some timeslice in which to run. But there was a short period of time during which the CPUScheduler slept, the priority 4 thread blocked, and a priority 2 thread became the currently running thread. In effect, this priority 2 thread stole some CPU time; it could do this because there was a time gap between when the priority 4 thread blocked and the priority 6 thread woke up.
It’s probably not a crisis that this happened, since once the CPUScheduler woke up, we got back to the thread state we wanted. We could have prevented this CPU stealing from happening if somehow we knew when the priority 4 thread had blocked. However, on a native-thread platform, we cannot prevent a lower-priority thread from running at some point anyway, which is really just a variation of the behavior that we’re discussing here. So solving this problem is not something that we’ll be able to do in an absolute sense.
It is conceivable that on a green-thread platform, we could create a new thread within the CPUScheduler class at priority 3. When the priority 4 thread blocks, this priority 3 thread would become the currently running thread; this priority 3 thread could inform the priority 6 thread that it should wake up and perform some scheduling. Note that on a native-thread platform this does not work: the priority 3 thread might still run even if the priority 4 thread has not blocked, and on a Windows platform, priority 3 and 4 share the same underlying operating system priority. Altering the priority levels of the threads to avoid this overlap—by, for example, running the scheduler at priority 8 and the target thread at priority 6—is a possibility, but we’ve seen that putting a CPU-intensive thread above the default priority level (especially the level at which the system GUI thread runs) is not always a good idea. And this does not prevent the priority 3 thread from running when the target thread is not blocked.
Even on a green-thread platform, this problem is impossible to solve
in the general case. If all the threads to be scheduled were to
block, then the priority 3 thread would continually run, consuming a
lot of CPU resources but performing no real work. In the first
edition of this book, we showed how to overcome that problem by
suspending the priority 3 thread, but now the
suspend()
method has been deprecated, so that
solution is no longer an option. And since the benefit provided by
such a solution would be very marginal, we’re not too worried
that such a solution does not exist.
The moral of the story is what we’ve said all along: Java’s scheduling mechanisms give you some control over how threads are scheduled, but that control is never absolute.
[10] There’s a subtle error here, in
that when the thread is removed from the scheduler, we assign it the
default thread priority rather than the priority it had when it was
added to the scheduler. The correct practice would be to save the
thread’s priority in the call to the
addThread()
method and then restore that priority
in the removeThread()
method; we’ll leave
that implementation to the reader.