9.4. Scheduling and Priority
Java makes few
guarantees
about how it schedules threads. Almost all of Java's thread scheduling is left up to the Java implementation and, to some degree, the application. Although it might have made sense (and would
certainly
have made many developers
happier
) if Java's developers had specified a scheduling algorithm, a single scheduling algorithm isn't
necessarily
suitable for all the roles that Java can play. Instead, Sun decided to put the
burden
on you to write robust code that works whatever the scheduling algorithm, and let the implementation tune the algorithm for whatever is best.
Therefore, the priority rules that we describe
next
are
carefully
worded in the Java language specification to be a general guideline for thread scheduling. You should be able to rely on this behavior overall (statistically), but it is not a good idea to write code that relies on very specific features of the scheduler to work properly. You should instead use the control and synchronization tools that we have described in this chapter to coordinate your threads.
Every thread has a priority value. In general, any time a thread of a higher priority than the current thread becomes runnable (is started, stops sleeping, or is notified), it preempts the lower priority thread and begins executing. By default, threads with the same priority are scheduled round-
robin
, which means once a thread starts to run, it continues until it does one of the following:
-
Sleeps, by calling
Thread.sleep( )
or
wait( )
-
Waits for a lock, in order to run a
synchronized
method
-
Blocks on I/O, for example, in a
read( )
or
accept( )
call
-
Explicitly yields control, by calling
yield( )
-
Terminates, by completing its target method or with a
stop( )
call (deprecated)
This situation looks something like Figure 9-4.
9.4.1. Thread State
At any given time, a thread is in one of five general states that
encompass
its lifecycle and activities. In Java 5.0, these states are made explicit through the
Thread.State
enumeration and the
getState( )
method of the
Thread
class:
-
NEW
-
The thread has been created but not yet started.
-
RUNNABLE
-
The normal active state of a running thread, including the time when a thread is blocked in an I/O operation, like a read or write or network connection.
-
BLOCKED
-
The thread is blocked, waiting to enter a synchronized method or code block. This includes the time when a thread has been awakened by a
notify( )
and is attempting to reacquire its lock after a
wait( )
.
-
WAITING
-
TIMED_WAITING
-
The thread is waiting for another thread via a call to
wait( )
or
join( )
. In the case of
TIMED_WAITING
, the call has a timeout.
-
TERMINATED
-
The thread has completed due to a return, an exception, or being
stopped
.
We can show the state of all threads in Java (in the current thread
group
) with the following snippet of code:
Thread [] threads = new Thread [ 64 ]; // max threads to show
int num = Thread.enumerate( threads );
for( int i = 0; i < num; i++ )
System.out.println( threads[i] +":"+ threads[i].getState( ) );
You will probably not use this API in general programming, but it is interesting and useful for experimenting and learning about Java threads.
9.4.2. Time-Slicing
In addition to prioritization, all modern systems (with the exception of some embedded and "micro" Java environments) implement thread time-slicing. In a time-sliced system, thread processing is chopped up so that each thread runs for a short period of time before the context is switched to the next thread, as shown in Figure 9-5.
Higher-priority threads still
preempt
lower-priority threads in this scheme. The addition of time-slicing
mixes
up the processing among threads of the same priority; on a multiprocessor machine, threads may even be run
simultaneously
. This can introduce a difference in behavior for applications that don't use threads and synchronization properly.
Strictly
speaking, since Java doesn't guarantee time-slicing, you shouldn't write code that relies on this type of scheduling; any software you write should function under round-robin scheduling. If you're wondering what your particular flavor of Java does, try the following experiment:
public class Thready {
public static void main( String args [] ) {
new ShowThread("Foo").start( );
new ShowThread("Bar").start( );
}
static class ShowThread extends Thread {
String message;
ShowThread( String message ) {
this.message = message;
}
public void run( ) {
while ( true )
System.out.println( message );
}
}
}
The
THReady
class starts up two
ShowThread
objects.
ShowThread
is a thread that goes into a hard loop (very bad form) and prints its message. Since we don't specify a priority for either thread, they both inherit the priority of their creator, so they have the same priority. When you run this example, you will see how your Java implementation does its scheduling. Under a round-robin scheme, only "Foo" should be printed; "Bar" never appears. In a time-slicing implementation, you should occasionally see the "Foo" and "Bar" messages alternate (which is most likely what you will see).
9.4.3. Priorities
As we said before, the priorities of threads exist as a general guideline for how the implementation should allocate time among competing threads. Unfortunately, with the complexity of how Java threads are mapped to native thread
implementations
, the exact meaning of priorities cannot be relied upon. Instead you should only consider them a hint to the VM.
Let's play with the priority of our threads:
class Thready {
public static void main( String args [] ) {
Thread foo = new ShowThread("Foo");
foo.setPriority( Thread.MIN_PRIORITY );
Thread bar = new ShowThread("Bar");
bar.setPriority( Thread.MAX_PRIORITY );
bar.start( );
}
}
We would expect that with this change to our
Thready
class, the Bar thread would take over completely. If you run this code on the Solaris implementation of Java 5.0, that's what happens. The same is not true on Windows or with some older versions of Java. Similarly, if you change the priorities to values other than min and max, you may not see any difference at all. The subtleties relating to priority and performance relate to how Java threads and priorities are mapped to real threads in the OS. For this reason, thread priorities should be reserved for system and framework development. It is not as useful in practice as simply implementing correct application-level control over processing using explicit synchronization.
9.4.4. Native Threads
A few times we mentioned the
term
native threads
. This means that threads in the Java VM are tied directly or indirectly to threads in the underlying OS or hardware, which makes it possible for them to run truly independently and across multiprocessor machines. Older implementations of threading in Java (originally known as "green threads") effectively simulated threading within an individual process and had serious limitations blocking threads in I/O operations and with scalability. Again, all modern VMs (possibly excluding embedded environments) use native threads in one way or another. Some VMs may allow you to tune the mapping. In general, you shouldn't worry too much about how threads are implemented as long as you stick to the rules we've discussed and don't rely on
peculiarities
of scheduling or priorities.
9.4.5. Yielding
Whenever a thread sleeps, waits, or blocks on I/O, it gives up its time slot, and another thread is scheduled. As long as you don't write
methods
that use hard
loops
, all threads should get their due. However, a thread can also signal that it is willing to give up its time voluntarily at any point with the
yield( )
call. We can change our previous example to include a
yield( )
on each iteration:
...
static class ShowThread extends Thread {
...
public void run( ) {
while ( true ) {
System.out.println( message );
yield( );
}
}
}
You should see "Foo" and "Bar" messages strictly alternating. If you have threads that perform very intensive calculations or
otherwise
eat a lot of CPU time, you might want to find an appropriate place for them to yield control occasionally. Alternatively, you might want to drop the priority of your compute-
intensive
thread so that more important processing can proceed around it.
Unfortunately, the Java language specification is very weak with respect to
yield( )
. It is another one of those things you should consider an optimization hint rather than a guarantee. In the worst case, the runtime system may simply ignore calls to
yield( )
.
|