Skip to main content.

Web Based Programming Tutorials

Homepage | Forum - Join the forum to discuss anything related to programming! | Programming Resources

Tricks of the Java Programming Gurus

Chapter 6 -- Effective Use of Threads

Chapter 6

Effective Use of Threads


CONTENTS


Unlike most common programming languages, Java incorporates threads into its design. This provides a number of advantages for the programmer. Because threads are an integral part of the Java environment, the programmer knows threads are always available. For a platform-neutral environment, this is an important attribute. The programmer can also feel confident that the underlying libraries are thread-safe. Because threads are defined as being part of the environment, any semantic clashes between the language and the presence of threads are eliminated.

A thread is an independant sequence of execution within a Java application (stand-alone Java programs or Java applets embedded in some other program such as your Web browser). Every Java application runs within a Java VM (virtual machine). The Java VM may be, simultaneously, running multiple applications and/or multiple parts of a single application (see Figure 6.1). Every Java application is given at least one thread and may create more at its discretion. Although your application may have only one thread, it is probably safe to assume that the underlying Java VM may be using threads of its own to assist you. Some common uses of threads by the Java VM include the garbage collector and the AWT windowing toolkit.

Figure 6.1 : A Java Application.

Threads enable you to take better advantage of the computer and to parallelize your application. In the presence of a multiprocessor system, each thread in your application may run on a separate CPU and truly run in parallel. However, you can still have advantages on a uniprocessor system. A lot of time may be spent by the application waiting for certain events, such as IO. The uniprocessor system can take advantage of this time by executing another thread. Thus, while your application is waiting for input from the user, it can still be running some cute animation on the screen. You can do this without threads, but the presence of threads makes this type of task much easier. The use of threads also enables you to create simpler and easier-to-understand programs. A thread may be executing a single set of code doing a simple task, leaving other work to other threads. In this way, the programmer may be able to take a program that is hard to comprehend and divide it into separate distinct tasks that are easy to understand and, together, accomplish the more difficult task.

How Does Java Define Threads?

The Java Language Specification defines threads to be part of the standard Java libraries (i.e. the java.* packages). Every implementation of Java must provide the standard libraries, and thus must support threads in some fashion. Therefore, the developer of Java applications can always expect threads to be present, and can be assured the standard Java libraries will be thread-safe (or at least have a defined behavior in the presence of threads). However, the Java programmer cannot make assumptions on the specific behavior of threads. Much of the specific behavior is left to the implementations (this will be discussed in more detail below).

Threads and Java

To use threads in Java, the user should be familiar with a few Java classes (and interfaces). There are not many and they are easy to learn. In the following sections you examine each class in some detail. Most methods within the classes are fairly intuitive. Others-the more complicated ones-are accompanied by tips on usage.

The Runnable Interface

At this point in your Java development career, you should be familiar with interfaces. The Runnable interface is most helpful because it enables any class that implements it to run easily within a thread. Here is the elementary Runnable interface:

public interface Runnable extends Object
{
  public abstract void run();
}

That is the entire interface-very simple, but quite powerful. When an object implements the Runnable interface it will, of course, provide a concrete version of the run() method. When a Thread is instantiated it can optionally take as one of its parameters an object that is an instance of Runnable. The thread will then execute the run() method of that object as its main. When the run() method exits (normally through an uncaught Throwable), the thread effectively dies. Thus, this simple interface enables any object whose class implements it to become alive.

Why have a Runnable interface and a Thread class? There are times when it will be more appropriate to just implement Runnable, and there are times when it is better to subclass Thread. Most of the time you could probably do either. Deciding when to use which one depends on your project's design and perhaps on your personal taste. There are cases where you could easily choose either method; however, there are other times where you must choose to implement Runnable. Using the Runnable interface is the more flexible of the two because you can always do it. You cannot always extend Thread.

As with all interfaces, true power occurs when you have a class that should, or must, subclass some other class to obtain a certain functionality. At the same time, you would like it to execute within its own thread. This is a case for using Runnable. If your class must subclass another class, you must implement Runnable to make an instance run in its own thread. A case in point is a simple applet. The applet class must subclass java.lang.Applet, so, to make it run in its own thread, you must implement Runnable, or you end up breaking what may be one class into two-one subclasses Thread and the other subclasses your other class. To be forced into this may conflict with your design, a case where the language will be getting in your way.

Once you have a class that implements Runnable and provides a run() method, you are almost ready to use it. To start the class in a thread, you still must instantiate the Thread class. Every thread within Java is associated with an instance of the Thread class, even if the Runnable interface is being used. When you instantiate the Thread class, you have the option of passing an instance of Runnable to the constructor of Thread. When this is done, it tells the Thread object to use the run() method from this passed-in object as the thread's main. What really occurs is that the default run() method of the Thread object simply calls the run() method of the passed-in object. This can easily be seen while in a debugger, or with the following program.

public
class Runn implements Runnable
{
  public static void main(String[] args)
  {
    new Thread( new Runn() ).start();
  }

  public void run()
  {
    new Exception().printStackTrace();
    System.exit(0);
  }
}

The ThreadGroup Class

Before you dive into the Thread class, let's take a look at ThreadGroup. An instance of the ThreadGroup class represents a collection of Java threads and other thread-groups. You can view the set of thread-groups within the Java VM as a tree, with the system thread-group as the root node. In this thread-tree, all threads are leaves, and thread-groups are (mostly) internal nodes (see Figure 6.2). An empty thread-group is also a leaf. Every Thread object and ThreadGroup object belongs to some thread-group, except for the system thread-group. The system thread-group is special; it is created by the Java VM and has no parent thread-group. Every other ThreadGroup object and Thread object is a descendant of the system thread-group. You learn the importance of this feature later in this chapter.

Figure 6.2 : The thread group tree.

The ThreadGroup class does not merely provide a container for these other objects; it also provides a certain amount of control over them. You can set actions on the thread-group that affect all its descendants. For example, you can set the maximum priority of a ThreadGroup, and this will prevent any other ThreadGroup or Thread from obtaining a higher priority. The ThreadGroup object will also make use of an installed security manager. Most security manager checks verify that the thread requesting the service has authority to modify the ThreadGroup object being accessed.

A primary example is a browser that enables the execution of applets. Each applet is composed of a set of classes and will run in one or more threads. When the browser is created, it has no idea when or what kinds of classes will enter the browser and begin executing. Furthermore, the browser may need to execute several applets at the same time. The browser can use thread-groups, along with a security manager to prevent threads created by applets from setting their priority higher than system threads and from modifying system threads or threads belonging to other applets. System threads may include items such as a garbage collector thread, window manager event thread, or other such threads that may provide service for the entire Java VM. In a sense, a ThreadGroup is one of several classes available to browsers, or other programs, to control distinct applets.

Here is the Java ThreadGroup class, including its nonpublic fields (shown for completeness), but minus the method implementations. (The comments are mine.) Much of this information can be obtained from the command javap -p ThreadGroup. This discussion is from the Sun JDK. The public (more specifically the nonprivate) fields for the standard Java classes should be the same on other implementations. It is possible that other implementations may differ slightly in their private fields; however, the functionality should remain consistent across all Java implementations.

public class ThreadGroup
{
  // constructors
  private  ThreadGroup();
  public   ThreadGroup( String );
  public   ThreadGroup( ThreadGroup, String );
  // thread control methods
  public final synchronized void  stop();
  public final synchronized void  suspend();
  public final synchronized void  resume();
  public final synchronized void  destroy();
  public void                     uncaughtException( Thread, Throwable );

  // public methods to set/get thread-group attributes
  public final String             getName();
  public final ThreadGroup        getParent();
  public final int                getMaxPriority();
  public final boolean            isDaemon();
  public final void               setDaemon(boolean);
  public final synchronized void  setMaxPriority(int);
  public final boolean            parentOf( ThreadGroup );

  // managing group contents
  private final synchronized void add( ThreadGroup );
  private synchronized void       remove( ThreadGroup );
  synchronized void               add( Thread );
  synchronized void               remove( Thread );
  public synchronized int         activeCount();
  public int                      enumerate( Thread[] );
  public int                      enumerate( Thread[], boolean );
  private synchronized int        enumerate( Thread[], int, boolean );

  public synchronized int         activeGroupCount();
  public int                      enumerate( ThreadGroup[] );
  public int                      enumerate( ThreadGroup[], boolean );
  private synchronized int        enumerate( ThreadGroup[], int, boolean );

  // security related methods
  public final void               checkAccess();
  // debug and help methods
  public synchronized void        list();
  void                            list( PrintStream, int );
  public String                   toString();

  // non-public data fields
  ThreadGroup   parent;       //the group which contains this group
  String        name;         //the text name of this group
  int           maxPriority;  //the max priority of any thread in this group.
  boolean       destroyed;    //used internally to flag a dead group
  boolean       daemon;       //true if a daemon group
  int           nthreads;     //number of threads contained by this group
  Thread        threads[];    //the actual threads
  int           ngroups;      //number of subgroups
  ThreadGroup   groups[];     //the actual groups
}

Using ThreadGroup

The ThreadGroup class contains no public data fields thus you can only interface to thread groups with method calls. The purpose of each data field in the ThreadGroup class (see above) is pretty apparent and you will only need to know of them if you subclass ThreadGroup or if you need to view the contents of a ThreadGroup object while debugging.

Note
The ThreadGroup contains references to all its descendant threads and groups through the threads[] and groups[] data fields, thus an application does not need to maintain a reference to the thread or group it creates. The Java garbage collector will not collect a thread or group object as long as a reference exists. This means that you can create threads with a simple statement such as new Thread( myGroup, "myThread" ).start(); and not have to store the returned object reference.

Creating a new ThreadGroup object is pretty straightforward. You must provide a name for the group and optionally a parent thread group. If you do not provide a parent group then the ThreadGroup of the current thread is used. Thus the following two code sequences are equivalent.

ThreadGroup mygrp = new ThreadGroup( "MyGroup" );
ThreadGroup mygrp = new ThreadGroup((Thread.currentThread().getThreadGroup(), "MyGroup" );

Note
There is a third constructor which is private and thus cannot be called by any Java method outside of the class. This constructor is used by the Java VM to create the system thread-group. In the Sun JDK, the source to the ThreadGroup object shows that this constructor simply assigns itself the name system and sets the group priority to the maximum. The Sun JDK VM is written in C and will call this constructor during the initialization of the Java environment.

Thread Group Control Methods

There are a number of method which allow an application to control the execution of all the threads in the ThreadGroup hierarchy-this is the entire thread-tree using the thread group object being accessed as the root. If one of the stop(), suspend(), or resume() methods of a thread group is invoked, then every thread and thread group contained by that group will have its similar method invoked. Thus, a call such as mygroup.stop() will cause every thread in the group mygroup and every group which is a child of mygroup will have its stop() method invoked, (likewise for suspend() and resume()). Although these methods are within a group, their primary functions are to act on all their threads.

When all the threads in a group (and all of its subgroups) have exited, the group's destroy() method should be invoked. This method effectively cleans up memory resources. It should never be invoked on a group which still contains threads or groups which themselves contain threads. If it does the IllegalThreadStateException will be thrown, which is why this method cannot be used to kill all the threads within a group. The destroy() method will call the destroy() method on all of the groups subgroups, then marks the group as destroyed and removes itself from its parent group. After invoking destroy() the group can no longer have new objects added to it, otherwise IllegalThreadStateException is thrown.

Whenever a thread encounters an unhandled Throwable-one that propagates up to the main method of the thread-the uncaughtException() method of the threads ThreadGroup is invoked. The default method will simply attempt to call its parent's uncaughtException(). If it has no parent, it will simply invoke the printStackTrace() method of the passed in Throwable object. The ThreadGroup class can be subclassed and this method overridden in order for an application to install its own mechanism for dealing with unhandled throwables.

Note
The run() method defined in Runnable and Thread has no throws clause. This means the compiler should enforce all Exception class throwables to be handled. Other Throwables, such as Error subclasses and RuntimeException subclasses, can be propogated by run() and thus caught by uncaughtException().

Accessing Thread Group Attributes

Every thread group has a number of attributes associated with it. The thread groups name and parent attributes are set when the group is created and cannot change. To obtain those attributes the getName() and getParent() methods are invoked.

The groups maximum priority attribute can be obtained with getMaxPriority() and altered with setMaxPriority(). This maximum priority cannot be set higher than its current maximum priority, and will cause each of its subgroups to have their setMaxPriority() methods invoked (which may or may not cause a change), thus a group can never raise its priority and can never have a priority higher then its parent. If you attempt to set a group's maximum priority higher then its current value the request is silently ignored. Changing a thread group's maximum priority will not affect the current priorities of any threads within the group. However, any existing thread cannot have its priority changed to a value greater then its group's current maximum priority. Look at the following program.

Public class Sample extends Thread
{
  public static void main( String[] args )
  {
    System.out.println( currentThread().getThreadGroup() );
    System.out.println( currentThread() );
    currentThread().setPriority(NORM_PRIORITY-1);
    System.out.println( currentThread() );
    currentThread().getThreadGroup().setMaxPriority( MIN_PRIORITY );
    System.out.println( currentThread().getThreadGroup() );
    System.out.println( currentThread() );
    currentThread().setPriority(NORM_PRIORITY-2);
    System.out.println( currentThread() );
  }
}

The preceding program produces the following output.

java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[main,4,main]
java.lang.ThreadGroup[name=main,maxpri=1]
Thread[main,4,main]
Thread[main,1,main]

Caution
When a thread is created it takes on the priority of the thread that created it, without checking the current maximum priority of its thread group. Thus it is possible to have new threads within a group to have priorities greater then its thread group.

Another ThreadGroup attribute describes if the thread group is a daemon group (true), or not (false). Those with a UNIX background will recognize the term daemon. It is most often used in the UNIX world to indicate a background process. You don't see a daemon process-it is just sort of there and provides some service. In Java there are daemon threads (see next section) and daemon groups. When a ThreadGroup is marked as a daemon group and all its subgroups and threads have been removed, the thread-group will be automatically destroyed. The current daemon-status of a ThreadGroup can be obtained via the isDaemon() method. This attribute can be changed at any time during the ThreadGroup's lifetime. The ThreadGroup constructor will set the daemon attribute to the value of its parent's-getParent().isDaemon(). The value of this attribute has no affect on the daemon value of any Thread objects.

Thread Group Contents

At this point you are well aware that a ThreadGroup can contain two kinds of items-Thread objects and ThreadGroup objects. You can use the methods activeCount() and activeGroupCount() to obtain the current number of Thread and ThreadGroup objects, respectively, currently contained in the group. The values returned are the sum of all the threads, or thread groups, within the thread-tree (using the thread group as the root), not just those in the thread group. Therefore if you call activeCount() on the system group (the first group created by the Java VM) you will get the current count of threads in the entire Java VM, not just those in the system group.

To obtain the actual Thread or ThreadGroup objects contained by the thread group you can use the enumerate() methods. To obtain the Thread objects of a group you would invoke one of the following:

public int enumerate( Thread[] list );
public int enumerate( Thread[] list, boolean recurse );

When you invoke enumerate() you pass it a pre-allocated Thread[] object. The enumerate() method will then fill in this array with the current Thread objects that the group contains. When the parameter recurse is set to true then enumerate() will obtain all the Thread objects in the thread-tree (using the group as the root), otherwise it obtains only the ones directly in the group. The actual number of elements placed in the array is returned by the method. If you invoke the first version of enumerate() it simply invokes the second with the recurse parameter set to true. For example, the following code fragments are equivalent, and both return all the Thread objects in the thread-tree where mygrp is the root.

int actual_count = mygrp.enumerate( list );
int actual_count = mygrp.enumerate( list, true );

You can obtain the ThreadGroup objects in a thread-tree in a similar manner by invoking one of the following methods.

public int enumerate( ThreadGroup[] list );
public int enumerate( ThreadGroup[] list, boolean recurse );

If, for example, you have the object representing the system thread-group-call it sys_grp-and you want to obtain a list of all threads running in the Java VM, you can achieve this with the following code:

void ListThreads()
{
  Thread[] thd_list = new Thread[ sys_grp.activeCount() ];
  // get all threads in and below the given group
  int actual = sys_grp.enumerate( thd_list );
  // print each thread object
  for( int x=0; x<actual; x++ )
  {
    System.out.println( x+": "+thd_list[x] );
  }
}

You will see code similar to this in the program AllThreads included on the CD-ROM. AllThreads creates a bunch of threads and thread-groups, then searches the tree for the system group and performs some code very much like that previously shown, thus listing every thread currently running in the Java VM.

It should be noted that the numbers returned by activeCount() and activeGroupCount() are the counts at the point in time when the specific thread-group was queried. By the time you use those counts, they may not be accurate. For example, one thread in a program obtains the thread counts, but after the count is returned, another thread adds 10 more threads to an existing group. When an enumeration method is called, it will include those 10 new threads in its listing. If the array you passed in was created using the value returned by activeCount(), it will not be large enough to hold all the current threads. The enumeration methods will not overflow the array. They will fill the array as much as possible and simply return; thus, you may not get an accurate list. You must remember Java is a dynamic multithreaded environment and these methods simply provide a snapshot of the system. After you obtain the list of thread and/or thread group objects, you cannot expect that list to remain valid for any length of time. To do so would require freezing the Java system to prevent threads from dying or being created.

Note
The enumerate methods and count methods will be de-emphasized in a future version of Java (probably v1.1). The methods threadCount(), allThreadsCount(), groupsCount(), and allGroupsCount() will return counts specific to the group or the group's thread-tree. The methods threads(), groups() will return arrays of thread or thread group objects containing the threads or groups specfic for the group. Similarly the allThreads() and allGroups() methods will return the objects for the group's entire thread tree. You will no longer have to get the counts, allocate the array's and then fill the arrays.

Security

checkAccess()

The ThreadGroup class contains a method, checkAccess(), which is called to perform security checks if needed. It essentially checks to see whether the current thread (the one calling the ThreadGroup method), has the rights to modify the ThreadGroup object being called. If the check fails, a SecurityException will be thrown. The method is quite simple; it will query the Java Runtime for the currently installed SecurityManager object. If one is present, it will call the checkAccess() method of the security object passing it the ThreadGroup object. If no security manager has been installed, the check simply returns (thus enabling full access). This allows the ThreadGroup object to use whatever security policy has been put in place, without the ThreadGroup class being modified. The checkAccess() method is a final method and thus cannot be overridden, even by subclasses. The following methods within the ThreadGroup class call checkAccess():

The Thread Class

The Thread class defines the application level interface to Java threads. This class is not the thread; it describes the attributes of a thread. In fact, the Thread object can still be accessed after the thread's main has completed. If an application is using classes that implement Runnable, it still needs to create an instance of Thread and assign the Runnable object to that thread:

Runnable runner = GetRunner(); //magically returns a runnable
Thread thd = new Thread( mygroup, runner, "mythread" );
// the thread is now created and will use the run()
// method defined in the Runnable object.

Although you can assign the same Runnable object to different threads, you normally will create a new instance for each new thread. If you do not, you must be careful because all the threads will be accessing the same instance data.

Note
If using a subclass of Thread, all the instance data fields will be thread-specific data (sometimes referred to as thread local storage). If using Runnable and you assign the same Runnable object to many threads, this won't be true.

Using the Thread Class

Lets now take a look at what the Thread class provides and how to use it. Following is the public interface of the Thread class (as of version 1.02). Although we won't look at every item we will cover the ones used most often.

public class Thread implements Runnable
{
  // thread class constants
  public static final int MIN_PRIORITY = 1;
  public static final int NORM_PRIORITY = 5;
  public static final int MAX_PRIORITY = 10;
  // static methods
  public native static Thread currentThread();
  public native static void yield();
  public native static void sleep(long);
  public static void sleep(long, int);
  public static int activeCount();
  public static int enumerate(Thread []);
  // constructors
  public Thread();
  public Thread(Runnable);
  public Thread(ThreadGroup, Runnable);
  public Thread(String);
  public Thread(ThreadGroup, String);
  public Thread(Runnable, String);
  public Thread(ThreadGroup, Runnable, String);
  // thread control methods
  public void run();
  public native synchronized void start();
  public final void join();
  public final synchronized void join(long);
  public final synchronized void join(long, int);
  public final void suspend();
  public final void resume();
  public final void stop();
  public final synchronized void stop(Throwable);
  public void interrupt();
  public static boolean interrupted();
  public boolean isInterrupted();
  public void destroy();
  // thread attributes
  public final native boolean isAlive();
  public final void setPriority(int);
  public final int getPriority();
  public final void setName(String);
  public final String getName();
  public final ThreadGroup getThreadGroup();
  public final void setDaemon(boolean);
  public final boolean isDaemon();
  // security related methods
  public void checkAccess();
  // debugging help
  public native int countStackFrames();
  public static void dumpStack();
  public String toString();
}

Creating Threads

The Thread class has seven constructors to choose from (you can see their signatures earlier in this chapter). The only input you can provide the constructor (in varying combinations) is a ThreadGroup object of the thread-group in which you want the new thread to be created. A String value contains the textual name of the newly created thread. Providing names for your thread is handy for debugging purposes. Finally, an instance of the Runnable interface can be provided. If you supply a runnable object, the thread will be created and the run() method of the newly created thread will immediately call the run() method provided by runnable object. This effectively activates that runnable object.

Once your thread is created you must activate that thread. When a thread object is created, the actual thread does not begin execution, rather it is just prepared to do so. You must invoke the start() method on the thread object to cause the thread to begin executing. After the thread's start() method has been invoked the thread can be scheduled for execution. You do not know exactly when it will begin execution, nor should you care.

Controlling Threads

There are a number of methods for a thread to control itself as well as other threads. By calling the sleep() and yield() methods, a thread can effectivly give up the processor. The yield() method simply gives control to the thread scheduler. The scheduler will simply pull a thread off of the ready queue. If the thread which performed the yield is the highest priority thread then it may immediately get control back. You cannot expect yield to always cause another thread to get control. The sleep() method (there are two) will cause the thread to be taken off of the ready queue for a specified amount of time. The specified duration the thread will not be scheduled for execution-it is sleeping. The two sleep() methods allow you to indicate the time in milliseconds or milliseconds plus additional nanoseconds.

Note
The sleep() and yield() methods are static and thus operate on the callers thread.

Caution
The current Sun JDK (v1.02) implementation of sleep( long, int ) will not really use nanosecond granularity. It will round to the nearest millisecond.

There are many cases where threads will need to wait for another thread to finish executing before proceeding. The join() method is used to allow one thread to wait for the completion of another. There are three variations of this method. The one without parameters will cause the caller to wait indefinitely for the target thread to exit. The other two versions enable the caller to specify a time-out value. It either takes one parameter that is time-specified in milliseconds or two parameters where both are in milliseconds plus an additional value in nanoseconds.

Caution
In the current implementation of Java from Sun, timeouts specified with nanoseconds are rounded to the nearest millisecond. Thus, join( 10 ) and join( 10, 1 ) result in the same timeout period.

Threads can affect the execution of other threads in a number of ways. When the suspend() method is invoked the target thread is removed from the ready queue and will no longer receive any CPU time-it will not be scheduled. The resume() methods places it back on the ready queue. When you resume a thread, there is no guarantee that thread will begin executing immediately, because it is just now eligible for scheduling. There are applications that will find these methods valuable but most applications will not. Java provides much better mechanisms for thread synchronization-discussed in much detail in the next chapter.

Java also provides a method for one thread to gently interrupt another with the interrupt() method. I say gently, because the interrupt will not affect the current execution of the interrupted thread. The thread must query the system to see if it has been interrupted via the isInterrupted() method (you also can use IsInterrupted() to queiry another thread's interrupt status). The thread being interrupted might be in the middle of an important transaction which needs to complete, thus a thread must choose when to check if it has been interrupted. The benefit of this mechanism is that the interrupt will cause a thread to awaken from sleep(), wait(), and join(). This awakeing occurs by the InterruptedException being thrown by the above routines.

Caution
The current implementation of Java (v1.02) does not implement the interrupt methods. If they are called, they will throw an instance of NoSuchMethodError. The interfaces are scheduled to appear in Java v1.1.

How Threads End

Normally a thread will end by the thread itself simply exiting from its run() method. However a thread may cause another to exit by invoking stop() on the thread. You can pass stop() a Throwable object which the Java VM will then cause to be thrown at the target thread-that is the target thread will behave as if it encountered a throw statement with the given object. Unless that throwable is being handled by the thread, the thread's run() method will exit and the uncaughtException() method of the thread's group will be invoked. Calling stop() with no parameters causes a ThreadDeath object to be thrown at the target thread. Thus the following lines are equivalent.

Mythd.stop();
mythd.stop( new ThreadDeath() );

There is a destroy() method defined, but it is currently not implemented. This method, when (or if) implemented, will simply destroy the thread without cleaning up the thread's resources. Thus synchronization objects will not be updated. An applicaiton should never have a reason to use such a method because it would be dangerous.

Thread Attributes

Each thread contains a number of attributes. Some must be set when the thread object is created and can never be altered, while others can be changed throughout the thread's life. The attributes are: name, priority, thread group, and daemon status. The thread group must be specified while creating the thread and cannot change. The name can be queried and set using the getName() and setName() methods. The priority defaults to the priority of the creating thread. The current priority can be obtained from the getPriority() method. Before starting a thread and during its execution the threads priority can be altered by calling the setPriority() method with the desired priority. The priority can not be set higher than the maximum priority of the thread's group. If an attempt is made to set the priority higher it will silently be ignored and the priority will be changed to be equivalent to the current maximum priority of the threads group.

Recall from the ThreadGroup description that the term "daemon" is most commonly used in the UNIX community. For a Java thread, it essentially is used to indicate the type of thread. The most important attribute to remember is the Java VM will not exit if non-daemon threads are still present. If the main thread of an application ends and the only remaining threads are daemon threads, the Java VM will exit, essentially killing the daemon threads without warning. A daemon thread should be used for some background task that most likely is providing a service to the application. For example, a communications server may have a background thread that simply listens on a port for new connections. With a new connection, a new Java thread is spawned to serve that connection. The listen thread can be marked a daemon thread. The more important session thread, one using the connections, will probably not be a daemon thread. Marking a thread as a daemon is a judgment call on the part of the developer; however, it is a handy feature.

Tip
If the developer does not provide a specific mechanism where the thread can exit, it is probably a candidate for being a daemon thread.

Security

The Thread class supports the presense of a security policy by providing the checkAccess() method and using that method internally to prevent unauthorized modifications of a thread object. This method simply attempts to get the currently installed SecurityManager object, which implements the security policy. If one is found then it is invoked to verify that the calling thread has the proper rights to modify the thread object. If the calling thread does not have the correct rights an instance of SecurityException is thrown. The following are methods within the Thread class that invoke this method:

Using Threads

There are several example programs on the CD-ROM that go with this chapter. The best way to understand them is to run the programs. Most are simple stand-alone programs that can be run from the command line. However, there are some that make use of the AWT and browsers. In general, all the programs show various parts of the Thread and ThreadGroup classes. None show all the features. The best way to learn them is to experiment. Included on the CD-ROM directory associated with this chapter is an HTML file named index.html, which you can view in a browser (such as Netscape Navigator) and which has a description of the included demos plus links to their source code. You can view the source directly in the browser. You have to go to the command prompt to run the stand-alone applications. Applets can, of course, be directly run in the browser. Now let's take a look at some basic thread examples.

Priority.java

This is, perhaps, one of the simplest threaded programs you are likely to encounter. Its real purpose was to see how Java Thread priorities match with the underlying native threads (if running on such a platform). The program simply creates a Thread for each Java priority level and assigns that thread its respective priority level. The threads are very simple; they wait on an object until notified, then exit. This enables the whole set of threads to be active and alive while their priorities can be viewed. After being created and started, the program then uses the ThreadsList class to provide a textual listing of the threads. It then sits forever, waiting for the user to type a key, after which it notifies all the threads to end. Take a moment to check the source now.

It's a short program, right? As you can see, the single class Priority implements the Runnable interface and includes the required run() method:

public void run()
  {
    try
    {
      synchronized( this )
      {
        this.wait();
      }
    }
    catch( Exception ee )
    {
      ee.printStackTrace();
    }
  }

To call the wait() method of an object, you must be synchronized on that object. This means you must either be in a synchronized method or within a synchronized block, as in this example. When the wait() is performed, the monitor for the object will be released. When the wait() returns, the monitor will once again be locked. Thus, after signaling a thread to continue from a wait(), the thread may still need to wait until it can reaquire the monitor. This is important to remember. Chapter 7, "Concurrency and Synchronization," discusses the details of Java's synchronization facilites in much depth. The wait() method can throw an InterruptedException, so you must be prepared to handle such an exception.

To get this run() method to execute in its thread, you must first create an instance of the class that implements Runnable, such as the following:

Priority self = new Priority();

Then you create an instance of Thread, passing it the desired Runnable object. This example passes the same object to each thread. Thus, the primary portion of the application is the following:

for( int x=Thread.MIN_PRIORITY; x<=Thread.MAX_PRIORITY; x++ )
    {
      Thread thd = new Thread( self );
      thd.setPriority( x );
      thd.setDaemon( true );
      thd.start();
    }

Pretty straight-forward stuff. The Thread class is instantiated, passing it the already created Runnable object, thus instructing the thread to use the run() method in the associated Runnable object. In this example, you need to keep a temporary copy of the thread object in the local variable so you can perform a few additional operations: otherwise, you could have simply performed the following statement and drop the return value.

new Thread( self ).start();

Instead, the example uses the thread object to set the priority and to mark the thread as a daemon thread. Finally, the thread is started and loops around to do the next thread.

Because the threads will all immediately block on the runnable object, and because they all are using the same runnable object, you can release them all with a single call to notifyAll() on the runnable object.

This test is very simple, but shows the basics for starting threads as well as some simple use of the Runnable interface and the synchronization facilites inherit in every Java object. Later, when examining thread priorities, if you are using a Win32 machine and have the pview95 (pview on NT) program, you can see that Java threads produce Win32 threads-notice the priority levels Win32 uses.

PPrimes.java

This is also a simple program. It uses threads to find prime numbers. It is not a very good prime number generator, but it does show how to use Java threads. Further, if you run it on Solaris and Win32, you will notice slightly different behavior due to the differences in the threading model between the two systems (see the following sections on Performance and Java threads internals). Finally, you also can see that using multiple threads does not mean it will run faster. If you run this on a multiprocessor Windows NT box, you might actually see a performance increase. (I did not have access to one while writing this book, so I can only dream.)

This program takes input such as the following:

java Pprimes 200 5.

This will cause it to find all primes from 1 to 200 and will use 5 threads. It simply divides the range into 5 parts, the first being 0..39, then 40..79, and so forth. Then each thread simply finds all primes within its range. It uses a modified brute-force method to find the primes and writes them to the screen as it finds them (thus they don't come out in order). It also provides the time in milliseconds to complete the task. You can vary the number of primes to find, but for simplicity the number is rounded up to a multiple of the number of threads used. You also can specify how many threads there are, including one.

You can view the source from the CD-ROM. Notice that the program makes use of the join() method to enable the main thread to wait for all the prime finders to finish before it exits. This program is also an example of subclassing Thread, as opposed to using Runnable.

ThreadsList.java

Although this sample can be run by itself, it is not too exciting that way. It is mostly a little utility class, which will provide a listing of all the threads currently in the Java VM. Some of the other sample programs use this class. This is a simple class that demonstrates how you can obtain access to threads via the ThreadGroup. It simply starts from the caller's thread and finds its thread-group, then walks up the thread tree until the root (the system thread-group) is found. It then walks down the tree visiting every group and listing all the threads within that group.

Debugging a Threaded Program

The best method for debugging threaded programs is to avoid having to debug. When using threads, it pays to plan carefully what your threads will do and how they will do it. Pay extra attention to the places where threads interact with each other. Undoubtedly, your threads will have to share data with one another or perhaps take turns accessing some resource. You should be sure these points of contact between threads are done in a thread-safe manner. It is often helpful to treat your threads as if they can all be running at the same time (in a multiprocessor machine with a Java VM that will support it, such as Java on NT, this can be a reality). Don't build any assumptions into your application of which thread will run first, or reach a certain point, or so forth. You often will not be able to predict this, and even if you can today on your current Java VM, it may not be true on a different Java VM.

Of course, even the best of you will have problems in your programs. Unfortunately, if your problems are related to threads misbehaving or not playing well together, it can be hard to fix. The biggest reason is the unpredictable nature of thread-related errors. Synchronization issues may not show at the point of the failure, but rather in some other location and at some other point in time. At this point, the help of a thread-aware debugger is indispensable. The current JDK from Sun builds in remote debugging capabilities (see Chapter 26, "The Java Debugger API") and also includes a proof-of-concept command-line debugger called jdb, which is simply a front-end to the Java VM's debugging capabilities. At the time of this writing, other companies (Sun, Symantec, Borland, and SGI) were coming out with GUI-based debuggers, which should make life easier.

No matter which debugger you use, the basics remain the same. Use the debugger to provide you with information about all the current threads that are running. This is where providing meaningful names for your threads (see setName()) comes in handy. The debugger should show you the thread's name and other information, such as where in the source code it is currently executing and the thread's state (running, sleeping, in a monitor, waiting on a condition, suspended, at a breakpoint, or perhaps a zombie). This information is indispensible; it enables you to notice unusual conditions quickly. For example, you may have two threads that should never be runnable at the same time for various reasons. The debugger can show this kind of problem quickly. The other big use of the debugger when debugging a multithreaded application is the debugger's capability of suspending all (or certain) threads. When you are debugging one thread-perhaps while it is at a breakpoint and you are examining data-it is often helpful to suspend the other threads to prevent them from continuing and affecting the application's state.

If you are the type that hates debuggers, or uses them only as a last resort, there are other techniques. You can sprinkle trace code throughout your application, perhaps activated by a command-line switch or some other mechanism (such as a special key sequence, an extra menu in your GUI application, or a special data packet across the socket connection). If you use tracing code, it is often helpful to include the thread ID in the trace message. See the following example:

System.err.println( "["+Thread.currentThread()+"]Just got here" );

This causes the current thread's toString() method to be called. The default toString() identifies the thread. When you view the tracing output, you can follow the order in which separate threads access various parts of your application. By providing meaningful names for your threads you can often quickly spot errors. Using thread groups (and giving the groups meaninful names) also often helps. The thread group's list() method can be used to dump a thread/thread group listing to the screen from wherever you need.

There is no easy guide for debugging programs in general, and especially for debugging multithreaded applications. It often requires detailed knowledge about the application and how it is structured. The best form of debugging is to avoid having to do it!

Performance

Performance, as it relates to multithreaded applications, is a difficult subject-especially in a platform-neutral language and environment such as Java. The problem is that performance is tied to many factors: the specific machine you are using (CPU, memory, and so forth), the OS that is running, the specific Java implementation being used, and the application you are writing.

If you are running a Java implementation which can take advantage of a multiprocessor machine and you have such a machine, your multithreaded application will have an advantage over a similar single-threaded application. This assumes that the threads within your application can execute in parallel. If you don't have an multiprocessor box, or a Java VM that will use a multiprocessor box, you still have a good reason to use threads. Java is platform-neutral, and therefore your Java application may run on computers to which you don't have access. If that application is multithreaded it will be multiprocessor-ready.

Obviously, the presence of multiple processors will have a positive effect on your multithreaded application, but other factors must be considered. The performance of the same application could vary when run on various Java VMs, even on the same type of machine. This would fall on the Java VM implementer's shoulders. Some factors that may affect your application include the context switch time, which is the time it takes for the Java VM's thread manager to switch threads. Other areas include the various synchronization objects. The time it takes to obtain and release locks may vary over implementations.

Thread Scheduling

The Java language and runtime define the presence of threads as well as a standard interface to their usage. Java also provides the mechanisms for controlling and synchronizing threads. However, because Java is platform-neutral (both hardware and operating system), it refrains from defining, or requiring, certain low-level policy information of threads. Java does not define the scheduling algorithm used to implement threads. The scheduling algorithm dictates which thread should be executed when and for how long. It also dictates how that thread relinquishes control to other threads. There is often much debate over the wisdom of this choice; however, it offers the most freedom to the implementers of Java, which may make it much easier for Java to be implemented everywhere.

There are some basic terms that you should be aware of to help you understand the nature of thread scheduling. When a thread can be suddenly interrupted so that another thread can begin work, it is said that thread has been preempted. The process of the system switching between two threads is called a context switch. On some systems, a thread scheduling policy will include time slicing. This means that when each thread is executing, it will only execute until it blocks to do some service, such as IO or waiting on a synchronization object, or until its time slice runs out. At this point, the system will preempt the thread in order to give another thread a chance to run. Other common terms describe the state of a thread. A thread can be runnable, which means it is ready to execute and is just waiting for the system to schedule it. A thread can be blocked, which means the thread is waiting for the completion of some service, after which it will again be runnable. Finally a thread can be running, which means it is the current thread being executed. There can be only one running thread per CPU in the system. You may also see terms such as stopped, zombie, or sleeping. For simplicity it suffices to think of threads being in one of the three states: blocked, runnable, or running (see Figure 6.3).

Figure 6.3 : Thread states.

By not explicitly dictating scheduling policy, the implementation of Java has a certain amount of flexibility. The levels of priority that Java defines (currently 10 values in the range MIN_PRIORITY to MAX_PRIORITY) should be thought of only as a hint to the system. The scheduling algorithm deployed by the implementation may or may not take advantage of them. Currently, priorities are taken advantage of in the Sun Java implementation. The Java language does not dictate when or how a context switch takes place; nor does it require that threads be pre-empted. This freedom for implementers is a double-edged sword. On one side, it may increase the number of platforms on which Java will be available. On the other, it can make life difficult for the unsuspecting developer.

It is very easy to develop a program that is expecting certain behavior of the threads. When run on a different platform, one that the developer may not have access to, a radically different behavior is seen. As a real-world example, the current implementation of Java by Sun uses different thread scheduling rules when running on Win32 or on Solaris/SPARC.

Sun JDK/Solaris

The Sun JDK implementation of Java on Solaris (SPARC) does not use the Solaris Thread Library; instead, for various reasons the Java group wrote its own thread package called Green Threads. Green Threads was implemented back when Java was called "Oak" and the project was called the "green project," and probably before Solaris became stable. Green Threads does not use Solaris LWPs (LightWeight Process) and will not take advantage of a multiprocessor system. If you run Java on your 8-processor SparcCenter 1000, your multithreaded application will not use all the processors. Sun is reportedly converting their JDK to use Solaris Threads, which will help Java applications perform better on the Solaris platform.

Until then, you must live with Green Threads. Green Threads operates totally at the user level. The Solaris operating system will have no idea that multiple threads are running; it will look like a single-threaded process as far as Solaris is concerned (see Figure 6.4). The following are the main features of the threads in Green Threads:

Figure 6.4 : Green Threads on Solaris.

Because Green Threads does not have to make a system call to perform context switches, it can do so very rapidly. A Green Threads thread will run forever as long as it does not perform a blocking call (such as IO) and another higher-priority thread does not become runnable. Higher-priority threads preempt lower-priority threads. Green Threads will use priority inversion to boost a thread's priority temporality. This means that if it owns a monitor on which a higher-priority thread is blocked, this prevents a higher-priority thread from being starved by low-priority threads. The Green Threads package provides support to turn potentially blocking system calls into asynchronous calls to prevent one thread from blocking the entire process. Finally, the Green Threads package is designed to have a system-independent layer and a system-dependent layer, thus helping the porting effort to platforms that do not offer threads or dedicated hardware. In those cases, the Green Threads package can be used to provide the necessary thread support for Java.

The day will come when Java uses the Solaris Threads on the Solaris platform. Solaris Threads are the native threads under the Sun Solaris operating system. The main properties of its threads are the following:

Solaris Threads carry out most of the thread management responsibilities in user space, as opposed to system space. This makes thread context switching very light, because it does not require a kernel call. The Solaris Threads will use the underlying LWPs, which are controlled by the kernel (see Figure 6.5). LWP's are essentially equivalent to Win32 threads, that is, they are a kernel resource and are scheduled and preempted by the kernel. LWP's are scheduled to run on any of the available processors in an multiprocessor computer. Therefore Solaris Threads will take advantage of a multiprocessor system.

Figure 6.5 : Solaris Threads.

Sun JDK/Win32

The Sun JDK implementation of Java on the Win32 operating systems (Windows 95 and Windows NT) takes a different approach from the Solaris version. It uses the native Win32 threads of the underlying operation system (see Figure 6.6). Thus, whenever you create a Java thread, it translates directly to a Win32 thread. Win32 is relied on for the thread scheduling policy, as well as for all thread synchronization policies. Therefore, Win32 Java threads utilize the time-sliced and priority-based preemptive capabilities of Win32. The Win32 events, mutexes, and critical sections are used to perform the various kinds of synchronization. This has the benefit that Java behaves like other Win32 programs and the implementation appears to have been easier. However, threads under Win32 behave differently than Green Threads in Solaris; therefore, the developer has to be careful not to assume Win32 type threads.

Figure 6.6 : Windows 95 threads.

The Win32 scheduler schedules threads based on their dynamic priority. How it computes the dynamic priority of a thread is slightly complicated and the relevant Win32 documentation should be consulted. I will describe the basic policy here. Each Win32 process can be in one of four priority classes: HIGH_PRIORITY, NORMAL_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, and REALTIME_PRIORITY_CLASS. Each thread within the process can be in several thread priority levels. The priority class of the process and the thread priority level determines base priority for each thread. During execution, that base priority can be adjusted to come up with the threads dynamic priority. The scheduler will maintain a queue for each priority level. Threads within a level will run in round-robin fashion, and the scheduler will not run any threads in a lower level until the higher level has no runnable threads (see Figure 6.7). Therefore, two compute-bound threads (those that do no, or little, IO) within the same level will not result in one being starved; however, those in a lower level can be starved. The advantage of Win32 is on a multiprocessor NT machine; here, Java threads truly can run in parallel. Java will execute within the NORMAL_PRIORITY (the Win32 default). Each thread will also begin in the normal priority level (again the Win32 default), however as the Java thread priority is adjusted so is the Win32 thread priority level (this is dicussed further below).

Figure 6.7 : Windows 95 scheduling.

Yielding Control

The differing scheduling policies used by the Sun JDK Java implementation on Solaris and Win32 is cause for some concern for developers. Typically, if your threaded application works well on Solaris, it will work well on Win32. The opposite is not true. A Thread in Win32 will be preempted when its time-slice is up; in Green Threads, a thread will be preempted only if a higher-priority thread becomes available. Therefore, on Solaris, a thread can easily starve other threads from getting a chance to run, whereas the same program will run well in Win32.

This situation can be avoided. If your threads perform IO, or other system calls, or they often use synchronization techniques (such as wait() and notify() or use of synchronized methods or blocks) they will provide many chances for other threads to obtain processor time. However, in some situations, you may have threads doing nonblocking processing (such as large math calculations). In these situations, you must be more careful. The use of the yield() method can help. This method causes the calling thread to relinquish control of the processor. The thread manager will simply check for other runnable threads of equal priority and, if one it is executed, the current thread will be placed back on the ready queue. If no runnable thread of equal priority is found, the current thread continues. Therefore, you can't yield to lower-priority threads. The use of yield() is often unavoidable in the Solaris Green Threads environment and is a small cost in Win32. In Win32, it translates into a Sleep(0) Win32 API call.

If you know your mix of threads in a program will be a few compute-bound threads and more IO-bound threads, you may be able to place your compute-bound threads at a lower priority. They will obtain processor time during the times the IO-bound threads are blocked in IO calls. Once the IO-bound threads become runnable (the IO completes), they will preempt the compute-bound threads.

Tip
Be sure any compute-bound threads that do not utilize sleep() or yield() do not have a higher priority than your interactive threads; otherwise, you risk starving your interactive threads until the compute-bound threads are complete.

For most applications, you probably should avoid explicitly setting priorities. As a simple rule, if your thread does not do any blocking-type operations, move its priority down a bit; if you need a user interface thread to respond quickly you may bump its priority up a bit. In general, try to avoid setting priorities. If you can, place a yield() call in your code; this may help the threads on a non-preemptive system behave better-yield will almost never hurt you on any platform. You should not attempt to use the setting of priorities or the presence of sleep() calls as a mechanism for relying on threads to run. This gets complicated and often results in dependencies on the systems on which you are implementing. Your guiding principal should be keep it simple.

Caution
Your application should never rely on yield() to perform correctly. The yield() method should only be present to help thread behavior and perhaps performance.

Limitations

Threads have many good features, and developers should take advantage of them. However, don't get carried away! A thread is a resource that should be used carefully. Not only can the use of threads increase the resource requirement of your application, they can also decrease its performance. Another factor to consider is the type of application you are developing. An application that can be split among very independent threads is much easier to create than one where the threads require much interaction between them. The more threads there are that need to cooperate with one another, the more chances there are for subtle errors and performance problems. It may very well turn out that a multithreaded application will be spending much of its time synchronizing rather than doing work. In that case, it makes better sense to decrease the threads or do the entire application in a single thread. Other applications are naturally divided where each part can run in parallel. On today's SMP hardware, parallel execution is a distinct possibility.

When thinking about the cost of threads, here are some considerations to keep in mind:

Inside Threads

In this section, you look at some of the details of Sun's current JDK implementation. If you are not interested in the nitty-gritty details of how threads are implemented, you can skip this section. However, understanding how things work often leads to a better understanding of how to use them. Sun provides the source code to the JDK implementation for noncommercial use at no cost, although you need to complete a licensing agreement. This is a painless task-visit its Web site for more information:

http://www.javasoft.com

Because the source to the JDK implementation is the property of Sun, it cannot be provided here, and this discussion will be mostly descriptive of what it does.

Layers

Threading in the Java VM is implemented in a set of layers with an abstract API at the top and a system-dependent interface at the bottom (see Figure 6.8). Most of the layers are very lightweight. This scheme helps make the Java VM both portable to multiple platforms and flexible in the choice of a thread package: the native OS, Green Threads, or some other package. Most thread implementations have enough similarities to make the abstraction layer easy and lightweight. For example, the Microsoft C _beginethreadex() call and Solaris thr_create() call are different names and slightly different parameters, but they are very close in behavior, and thus it is easy to come up with an abstract "create a thread" routine.

Figure 6.8 : Java Thread Architecture.

Green Threads

It is clear that the Green Threads package was written to be portable across a variety of systems, not just flavors of UNIX (see Figure 6.9). The package is also more complete than Java uses. The Green Threads package provides the low-level data structures and functions to describe threads, thread contexts, thread control structures (runnable queues, and so forth), and thread synchronization objects, such as condition variables and mutexes.

Figure 6.9 : Green Threads Architecture.

Because Green Threads manages its own thread context switching, it must prevent a single thread from performing operations that will prevent the other threads from executing, such as IO or other system calls. Green Threads, as implemented on Solaris, provide replacements for many system calls. Rather than calling the Solaris write() function code, running in a Green Threads thread calls a special wrapper for the system write(). This wrapper turns a potentially blocking system call into an asynchronous system call. Green Threads then blocks the thread. To the thread it simply calls write() and blocks waiting for the completion; however, what really happens is Green Threads arranges for the OS to signal it when the IO is complete. Until that signal comes in, Green Threads is free to schedule another thread for service. When the signal comes in, indicating an outstanding IO needs attention, Green Threads will determine which IO needs completion and handle the IO. The waiting thread then becomes runnable.

Note
Under Green Threads (that is, Java on Solaris), if a native method performs a blocking operation, the entire Java application will block. Writers of native methods under a Green Threads implementation should arrange for asynchronous IO, such as that provided by the Green Threads package.

Recall that the Green Threads package totally manages its own threads. On a system such as Solaris, it essentially utilizes only one LWP at a time; thus, it won't take advantage of multiple processes. Recall also that Green Threads will perform thread context switches when that thread blocks for some reason, such as IO, waiting, sleeping, or a yield. A context switch will also be performed when a higher-priority thread becomes runnable.

Green Threads accomplishes this management task by utilizing two features-signals and a super-high priority thread. By using signals, the Green Threads manager can be notified of system events, such as IO completions and timer ticks. It uses this signal handling time to gain control and manage the other threads. The clock handler thread is a special thread installed by the Green Threads package. It runs at a priority of 11, which places it above the maximum priority Java allows (Thread.MAX_PRIORITY). When in a debugger, you can see this thread running in the system thread-group. This thread is, other than for its priority, a normal green thread. It spends most of its time blocked and is signaled by the expired alarm when it needs to perform some duty. The high priority enables it to gain control by preempting any current thread. The duty the clock handler performs is to notify other threads of expired timeouts, such as during a sleep() or a timed wait. Thus, the other threads can then be placed back on the runnable queue and can preempt a running lower-priority thread.

Win32

A Sun JDK Java implementation on Win32 platforms makes use of the native threads provided by the operating system. There is much information on this in the Microsoft development kit help files. The threads in both Windows 95 and Windows NT are kernel-level resources, and a Java Thread directly maps onto a Win32 thread. Internally, the mapping is straightforward. The JDK uses the Microsoft C Runtime _beginthreadx() function to launch the thread. The mapping of Java priorities to Win32 priorities is shown in Table 6.1.

Table 6.1. Java-to-Win32 priority mapping.

Java Priorities
Win32 Priorities
1,2
THREAD_PRIORITY_LOWEST
3
THREAD_PRIORITY_BELOW_NORMAL
4,5,6
THREAD_PRIORITY_NORMAL
7
THREAD_PRIORITY_ABOVE_NORMAL
8,9
THREAD_PRIORITY_HIGHEST
10
THREAD_PRIORITY_TIME_CRITICAL

The Win32 priorities are set by a call to SetThreadPriority(). This changes the thread priority level. Remember, the Java process runs at the Win32 NORMAL_PRIORITY CLASS-Java will not change the priority class (you would need to write a native method to do so). By setting the Java priority you can effectively change the Win32 threads base priority level, and thus influence the Win32 dynamic priority. In general, the Java priority of MAX_THREAD (i.e. 10 or in Win32 THREAD_PRIORITY_TIME_CRITICAL) level should be avoided, because you risk placing your thread at a higher priority than several kernel-level threads. The mapping Java provides allows for an application to greatly control its threads priorities, much like any Win32 application.

Java monitors make use of Win32 Mutex objects. The Java wait(), notify(), and notifyAll() method calls are implemented with condition variables, which Win32 does not have, but which are simulated via a combination of Win32 Event and Mutex objects.

Summary

As you develop applications in Java you will often discover situations where the use of threads can be beneficial. With a proper understanding of how threads work and a realistic expectation of what threads have to offer, a developer can effectively use threads. This chapter introduces you to the basics of Java threads; you should now be ready to use threads effectively and to tackle the following two chapters.