Threads
Overview
Threads, in contrast to exceptions (covered in Chapter 10), are absolutely foreign to an RPG program. Therefore, this chapter focuses mainly on Java, without providing many RPG comparisons. However, we predict that you will find this utterly new concept interesting. In fact, we predict that there will be increasingly more discussions of threads on the AS/400, especially since they have been introduced to the operating system starting with V4R2, and can now be used directly by code written in C, C++, and Java.
Synchronous Versus Asynchronous
In RPG, you call subroutines, procedures, and programs synchronously. The code that makes the call does not get control back until the called code has completed. For example, let's say that program P1 calls program P2 using the CALL op-code. Execution of P1 will stop at the CALL op-code to wait for the execution of P2 to end and return, before the code after the CALL statement is executed. A similar situation exists with the EXSR and CALLP op-codes for subroutines and procedures. This is also true of Java method calls, such as myObject.myMethod(), as you have seen. But Java also has built-in support for asynchronous calls. They are calls that spawn a second thread of execution and return immediately. Both the calling code and the called code run at the same time (concurrently). Imagine that, with enough threads of execution, you could have a whole suit of execution. Spun by the collar. (Sorry, bad pun. Really, it's a knit.)
In order to distinguish between traditional synchronous calling and threaded asynchronous calling, the latter is often referred to as spawning instead of calling. A timeline graph that shows which method is currently executing would look something like Figure 11.1.
Figure 11.1: A timeline graph of synchronous versus asynchronous calls
It is important to note that when a method is invoked as a thread, it runs at the same time as the code after the call statement. You can't predict which one ends first, and your calling code does not get the return value from the thread. This is because the call statement returns immediately-before the callee has even run. This is quite similar to submitting a program call to batch in RPG via the SBMJOB command, and is in contrast to using the CALL op-code.
Threads Versus Jobs
Asynchronous execution is not totally foreign to AS/400 programmers. In fact, it is done quite often. Many interactive applications have a PRINT function key or menu option that submits a job to batch, instead of performing it interactively. This allows the user to get control immediately while the print job runs quietly in the background. This is a disconnected job; the application does not care when it ends. It merely submits it and forgets about it.
Some applications that involve several screens of input for a single transaction run a job in the background, gathering information from the database, so this information can be shown to the user by the time he or she reaches the final screen. This is a connected job because the main interactive job must synch up with the background batch job by the final screen. This is usually done using data areas, data queues, or some other form of inter-job communication.
Jobs on the AS/400 are synonymous with processes on other platforms. How do they differ from threads in Java? In the amount of overhead. Starting a new job requires a significant amount of system resources, as you well know. Calling another program in the same job is expensive enough, which is why ILE significantly reduces the need to do this. Starting another job altogether is considerably more expensive. There is overhead in allocating storage, setting up library lists, setting up job attributes, loading system resources, and so on. You would not do this without due consideration, and certainly not for frequently repeated application functions. In Java, the equivalent of starting another job would be starting another Java Virtual Machine. Via the system's command analyzer, you would invoke another Java program via Java MySecondClass, for example. (Invoking another job from within Java is discussed in Chapter 14.)
Threads, on the other hand, have relatively little overhead because they share everything with the other threads in that job or process. This includes all instance-variable data. No new memory is allocated for the secondary threads. Even if you do not spawn a thread, your main code is considered to be running in a thread (the primary or main thread). Each method invoked in another thread gets its own copy of local variables and parameters, as you would expect for a method. On the other hand, instance variables, which are defined at the class level and are equivalent to RPG global fields, are shared. If you spawn two methods for the same instance of a class, they both have the same copies of the global variables in that class. Figure 11.2 depicts this sharing.
Figure 11.2: Threads in Java sharing in stance variable data
To do threads efficiently, of course, the underlying operating system must have true built-in support for them, versus just jobs or processes. Java cannot do this on its own. All major operating systems today support native threads (as opposed to simulated threads or lightweight jobs). This includes OS/400 as of V4R2, as part of its new built-in robust Java support. Typical operating-system thread support includes the ability to start, stop, suspend, query, and set the priority of the thread. For example, you might give a print job low priority so that it gets only idle CPU cycles versus the user-interactive threads. The Java language has built-in support for all of this in its thread architecture.
Questions arising at this point might include the following:
- How do I call a method asynchronously (that is, spawn a thread)?
- How do I stop a thread?
- How do I get back information from that thread?
- If necessary, how do I wait for that thread to end?
- How do I temporarily suspend a thread?
- How do I change a thread's priority?
- How do threads exchange information with each other?
All of these questions will be answered in this chapter. Don't worry. We won't leave you hanging by a thread!
Calling A Method Asynchronously Spawning Threads
The following sections start with an example that is not threaded and show you the code and its output. Then the exampled is changed to run in a thread, using the first of two ways to do this. Finally, the example is changed again, using the second way to run in a thread.
Calling without using threads
The example used throughout the following sections will be the AttendeeList class from Chapter 6, in Listing 6.27. That version uses Hashtable to manage a list of Attendee objects. We have revised it to support an elements method to return an enumeration of objects, which just calls the elements method of Hashtable. We have also revised the display method, which calls display on each Attendee object to print out the current attendee list. Rather than code these changes manually, we call the displayAll method in the Helpers class from Chapter 9 (Listing 9.20), shown here in Listing 11.1.
Listing 11.1: The Helpers Class for Calling Display on a List of Displayable Objects
import java.util.*; public class Helpers { public static void displayAll(Displayable objects[]) { displayAll(new ArrayEnumeration(objects)); } public static void displayAll(Enumeration objects) { Displayable currentObject = null; while (objects.hasMoreElements()) { currentObject = (Displayable)objects.nextElement(); currentObject.display(); } } }
This version of displayAll accepts an Enumeration, passing the output of a call to the new elements method. To do this, we had to change the Attendee class to implement the Displayable interface, as shown in Chapter 9. The revised AttendeeList class is shown in Listing 11.2.
Listing 11.2: The Revised AttendeeList Class that Uses Hashtable and Helpers
import java.util.*; public class AttendeeList { private Hashtable attendees = new Hashtable(); public boolean register(String number, String name) { // register method code not shown } public boolean deRegister(String number) { // deRegister method code not shown } public boolean checkForAttendee(AttendeeKey key) { // checkForAttendee method code not shown } public boolean checkForAttendeeName(String name) { // checkForAttendeeName method code not shown } public void display() { System.out.println(); System.out.println("* * * ALL ATTENDEES * * *"); System.out.println(); Helpers.displayAll(elements()); System.out.println("* * * END OF REPORT * * *"); } public Enumeration elements() { return attendees.elements(); } public static void main(String args[]) { AttendeeList attendeeList = new AttendeeList(); attendeeList.register("5551112222","Phil Coulthard"); attendeeList.register("5552221111","George Farr"); attendeeList.register("5552223333","Sheila Richardson"); attendeeList.register("5554441111","Roger Pence"); attendeeList.display(); } // end main } // end AttendeeList class
The code for the methods is not shown here since it hasn't changed since Chapter 6. Focus on the display method, which now leverages the displayAll method in the Helpers class. The main method here is simply for testing purposes. It populates the list with four attendees and then calls the display method to show the result of printing the list. (This is what we will be converting to a thread shortly.) Here is the output from this class:
* * * ALL ATTENDEES * * * -------------------------- Name........: Sheila Richardson Number......: 5552223333 -------------------------- Name........: Roger Pence Number......: 5554441111 -------------------------- Name........: George Farr Number......: 5552221111 -------------------------- Name........: Phil Coulthard Number......: 5551112222 * * * END OF REPORT * * *
A nice header and footer are printed at the beginning and ending of the report. This is done in the display method of the AttendeeList class.
Now let's convert that displayAll method in Helpers to run in a background thread. If the number of attendees is large, and the report is being printed to a file or printer instead of to the console, running the thread in the background will improve user response time and give control back to the user immediately. Remember, while not shown here, the example in Chapter 6 had a Registration class that drove this AttendeeList class by accepting commands and input from the user, via the console.
There are two ways to run a method asynchronously in Java. The one you choose depends on whether the class containing the method to be run asynchronously is free to inherit from another class or not. If it already inherits from one, then it cannot inherit from another. Java does not allow multiple inheritance.
Using threads, option 1 The extends thread
The method you wish to run asynchronously might be part of a class that is not already extending another class. Or it might not be written yet, which leaves you free to put it in a new class definition. In these cases, you may choose to extend the Java-supplied class Thread from the java.lang package, using these steps:
- Add extends Thread to your class definition.
- Define a method with signature public void run().Because it takes no parameters, the input to it must be passed in through class instance variables.
To run the code, create an instance of the class and invoke the start method on it. This method is inherited from the Thread parent class. Behind the scenes, it uses the operating system to create a thread and then invokes your overridden run method, which is defined as abstract in Thread (so you must override it).
Why not just invoke run directly? Because that would be a synchronous call! The start method Java supplies in the Thread class does the work of creating the asynchronous thread. Figure 11.3 depicts this process.
Figure 11.3: How a thread's start method invokes your run method
Let's put this to work in the Helpers class. The revised class is shown in Listing 11.3. As you can see, the code is changed inside the Enumeration version of displayAll. The real work is still only done in one place, but now that is the non-static run method. The code is simply moved from the old displayAll method to the run method. Because run is non-static, the class has to be instantiated first.
Listing 11.3: The Helpers Class with New Methods for Running in a Thread
import java.util.*; public class Helpers extends Thread { private Enumeration objects; public Helpers(Enumeration objects) // constructor { this.objects = objects; } public void run() // overridden from parent class { Displayable currentObject = null; while (objects.hasMoreElements()) { currentObject = (Displayable)objects.nextElement(); currentObject.display(); } public static void displayAll(Object objects[]) { displayAll(new ArrayEnumeration(objects)); } public static void displayAll(Enumeration objects) { Helpers helpersObject = new Helpers(objects); helpersObject.start(); } } // end class Helpers
Because run by definition (as defined in the parent Thread class) must not take parameters, the most difficult change is getting the Enumeration object to that method from the displayAll method. This is done by passing the Enumeration object as a parameter to the constructor, which in turn stores it away in an instance variable. The code inside run simply uses that instance variable. The displayAll method, after instantiating the object, simply calls the inherited start method on that object, which in turn calls the run method.
In summary, the following changes make this class run the displayAll method asynchronously:
- The class is changed to extend Thread.
- An objects instance variable is defined, so the run method has access to it.
- A constructor is added that takes an Enumeration object as a parameter.
- A run method is added that preserves the signature dictated by the abstract method in the Thread class. The logic to do the real work is moved from the displayAll method to this run method. The Enumeration variable objects is now the instance variable versus a parameter as it was in the displayAll method.
- The displayAll method is changed to instantiate an instance of this class, passing the Enumeration object as a parameter. It then calls the start method on the new class instance. This is inherited from the parent class Thread, and will implicitly invoke the run method.
Not too bad! With only minor changes, suddenly anybody who calls displayAll will get the resulting work done in a thread instead of synchronously. This means that the displayAll method call will now return immediately after it calls start, and both the calling code and the run method will execute simultaneously.
You don't even have to recompile the AttendeeList class. Just re-running it shows the result of this new behavior:
* * * ALL ATTENDEES * * * * * * END OF REPORT * * * -------------------------- Name........: Sheila Richardson Number......: 5552223333 -------------------------- Name........: Roger Pence Number......: 5554441111 -------------------------- Name........: George Farr Number......: 5552221111 -------------------------- Name........: Phil Coulthard Number......: 5551112222
This is interesting! Because the calling code in AttendeeList's display method executes before the run method executes in the background, you actually get the end-of-report footer printed before the report itself. This output is not guaranteed, though. It is possible that run will execute first, after, or at the same time. It depends on the operating system's time-slicing algorithm for assigning CPU cycles to each running thread. The code that spawned the thread is referred to, by convention, as the main thread of execution.
Of course, in real life, you would move the code to print the header and footer into the run method so that it is printed at the right place, but we wanted to show you the asynchronous behavior of threads.
Using threads, option 2 implements Runnable
You might not have the option of changing your class to extend Thread because your class already extends another class. In this case, you can choose to implement the Java-supplied interface Runnable (also defined in the java.lang package). This option is just as easy to implement:
- Add implements Runnable to your class definition.
- Define a method with signature public void run().
To run the code, create an instance of the class Thread, passing an instance of your class to the constructor, and invoke the start method on that Thread instance. Figure 11.4 depicts this architecture.
Figure 11.4: How the start method in the Thread class calls your run method in a runnable class
Listing 11.4 shows the Helpers class re-coded to support this second option. The changes made for this version, versus the version in Listing 11.3, are highlighted in bold.
Listing 11.4: The Helpers Class with Different New Methods for Running in a Thread
import java.util.*; public class Helpers implements Runnable { private Enumeration objects; public Helpers(Enumeration objects) { this.objects = objects; } public void run() { Displayable currentObject = null; while (objects.hasMoreElements()) { currentObject = (Displayable)objects.nextElement(); currentObject.display(); } } public static void displayAll(Object objects[]) { displayAll(new ArrayEnumeration(objects)); } public static void displayAll(Enumeration objects) { Helpers helpersObject = new Helpers(objects); Thread threadObject = new Thread(helpersObject); threadObject.start(); } } // end class Helpers
As you can see, very few changes are required:
- The class definition is changed to implements Runnable.
- The displayAll method is changed to also instantiate a Thread object, passing the Helpers object as a parameter. (The object passed must implement Runnable.)
- The start is called on the Thread object instead of the Helpers object.
Running the AttendeeList class now gives exactly the same output as from Listing 11.3. This shows that the two options are extremely similar. It also shows that, if you first choose to extend Thread, changing it later (if you decide you now need to extend another class) to implements Runnable is very straightforward.
Stopping A Thread
You will find that your primary use of threads will be for putting long-running jobs in the background. This will improve the response time to your end-users. You will also find that, in most such cases, you will want to give users the option of canceling that long-running job. This is good user-in-control design, and your users will expect that kind of control. How many times have you decided to kill a compile job because you discovered an obvious bug in the source while waiting for the job to complete?
Let's say that you want to allow a long-running thread to be stopped. The typical mechanism is to use an instance variable that both the running threaded method and the controlling thread (usually just the main or default thread) have access to. The controlling thread waits for a user indication that the running thread should be killed, and then sets the common instance variable to indicate this. Meanwhile, the method running in the thread periodically checks that variable, and, if it is set, voluntarily ends itself by returning.
Suppose you have (admittedly contrived) code that loops for a given number of seconds and displays the elapsed seconds during each iteration. This code will be in method run, as usual. The number of seconds is passed in on the command line, and then the user can cancel the loop by pressing the Enter key on the command line. Listing 11.5 shows this code.
Listing 11.5: A Class Stoppable by the User
public class TestThreads extends Thread { private long seconds; // how long to run private boolean stop = false; public TestThreads(long seconds) // constructor { this.seconds = seconds; } public void run() { for (int secs=0; (secs < seconds) && !stop; secs++) { try { sleep(1000L); // sleep for one second System.out.println(secs + " seconds"); } catch (InterruptedException exc) { } } // end for-loop if (stop) System.out.println("... thread stopped"); } public static void main(String[] args) { TestThreads thisObject; long longValue; if (args.length != 1) { System.out.println("Please supply number of seconds"); return; } try { longValue = Long.parseLong(args[0]); } catch (NumberFormatException exc) { System.out.println("Sorry, " + args[0] + " is not valid"); return; } TestThreads thisObject = new TestThreads(longValue); System.out.println("Running... "); thisObject.start(); Console.readFromConsole("... press to stop"); thisObject.stop = true; // Enter pressed. Signal stop } // end main method } // end TestThreads class
If you run this program from the command line and pass in a maximum number of seconds, you will see that it prints out the current seconds count every second and is stoppable by pressing Enter key:
>java TestThreads 30 Running... ... press to stop 0 seconds 1 seconds 2 seconds 3 seconds ... thread stopped
Here is the breakdown of this class:
- It inherits from Thread so that it can run a thread.
- A constructor takes in a given number representing the maximum number of seconds to loop, and stores it in a class instance variable.
- A run method loops for a given number of seconds. In each iteration, it calls its inherited method sleep, which takes a number of milliseconds to sleep (a long value) as a parameter. This can throw an exception, so you must monitor for it. The sleep method is a friendly way to pass time, as it lets other threads run. If you chose to implement Runnable versus extend Thread, you would code Thread.sleep(1000L).
- A main method validates the input, creates an instance of this class, and calls its inherited start method to spawn its run method as a thread. It then uses the Console class from Chapter 6 to wait for the user to press Enter. When this happens, the object's instance variable stop is set to true. Keep in mind that you can try this example as non-threaded by swapping the start method call with a direct call to run. You will see that, without the use of threads, the loop cannot be interrupted and canceled.
- The run loop will stop either when the maximum seconds is reached or when the stop instance variable gets set to true.
This convention of using a mutually accessible variable to control the stopping of the thread works well in most situations. There are times, however, when it will cause a problem:
- You have no convenient, mutually accessible variable to use for communications.
- You have no easy way in the long-running code to check a variable in a timely manner.
These are examples of cases in which you might find it necessary to forcefully "kill" a running thread. This is possible with the method stop inherited from the Thread class.
Let's try this method in the example, instead of the mutual variable method. You change the main method line of code from this:
thisObject.stop = true;
to this:
thisObject.stop();
and then recompile and run. After a few seconds of running, press Enter to get the expected results:
>java TestThreads 30 Running... ... press to stop 0 seconds 1 seconds 2 seconds
In fact, this time it is even more responsive. Pressing Enter results in an immediate end to the program. The other method can take up to a second to respond, while the run method waits to wake up from its "sleep."
The one potential downside of using stop() is that the run method does not get a chance to do any cleanup that it might require (in this case, to simply print out "thread stopped"). The need for this is rare. For example, you might need to close an open file as part of your cleanup.
There is a way to get control when your code dies. The stop method works by sending an exception of class type ThreadDeath to the thread object it was invoked on (thisObject, in the example). Because this exception extends Error instead of Exception, you do not normally monitor for it. However, if you do want to know when your code is being "killed" by the stop method, you can put the entire body inside a try/catch block, catching ThreadDeath. You must put the whole body inside the try because you do not know which instruction will be running when the death knell comes. Listing 11.6 shows the body of the run method in a try/catch block.
Listing 11.6: Placing the Entire run Method Inside a try/catch Block for ThreadDeath
public void run() { try { for (int secs=0; (secs < seconds) && !stop; secs++) { try { sleep(1000L); // sleep for one second System.out.println(secs + " seconds"); } catch (InterruptedException exc) { } } // end for-loop if (stop) System.out.println("... thread stopped"); } catch (ThreadDeath exc) { System.out.println("... thread killed"); throw(exc); } }
Now, when you run and cancel, you see the following:
>java TestThreads 30 Running... ... press to stop 0 seconds 1 seconds 2 seconds ... thread killed
Notice that the code re-throws the ThreadDeath exception after catching it. This is important so that the thread continues to die as expected (with dignity!). You might say, then, that you should try to catch your body before it dies!
If you implemented Runnable instead of extending Thread, you would invoke stop on the Thread instance instead of the class instance: threadObject.stop().This is because stop is a member of the Thread class. When you extend Thread, you inherit stop.
We warn you that as of JDK 1.2.0, the use of the stop method has been "deprecated," meaning it is no longer recommended. When you compile code that does use it, you get this warning message:
Note: TestThreads.java uses or overrides a deprecated API. Recompile with "-deprecation" for details.
Apparently, this is due to the concern that cleanup code could too easily be skipped over, leading to hard-to-find bugs. It is recommended that you always use the first option-setting an instance variable and checking it regularly in your asynchronous code.
Stopping multiple threads The ThreadGroups class
The previous example was easy, in that only a single thread was running. What if you instead started multiple threads running and wanted to stop all of them at the same time? You could, of course, invoke stop on each of them in turn. In real life, however, this can get messy, since you might not know how many threads are running and do not have a convenient way of enumerating all of them. This is common enough, especially in Internet programming where, for example, you might have numerous threads, downloading images, and resources. In fact, Java designed-in support for thread groups. This is a mechanism for partitioning threads into a uniquely named group, and allowing individual actions such as stop to be easily applied to all threads in the group.
To create a thread group, you create an instance of the ThreadGroup class and pass in any unique name you want for the group:
ThreadGroup groupObject = new ThreadGroup("longRunning");
To identify that new threads are to be created as part of a particular thread group, you pass in the ThreadGroup object as a parameter to the Thread constructor:
Thread threadObject = new Thread(groupObject, thisObject);
This works best for the implements Runnable option, versus the extends Thread option. However, the latter can be used, as long as you create a new Thread object and pass an object of your class as the second parameter, as shown. Listing 11.7 revises the TestThreads class to test this. (Only the changes are shown in the listing.)
Listing 11.7: Using ThreadGroup to Stop Multiple Threads
public class TestThreads implements Runnable { public static void main(String[] args) // cmdline entry { // existing unchanged code not shown thisObject = new TestThreads( longValue ); ThreadGroup groupObject = new ThreadGroup("longRunning"); Thread threadObject1 = new Thread(groupObject, thisObject); Thread threadObject2 = new Thread(groupObject, thisObject); System.out.println("Running... "); threadObject1.start(); threadObject2.start(); Console.readFromConsole("... press to stop"); groupObject.stop(); } }
To test ThreadGroup, the code is changed to use implements Runnable instead of extends Thread, the main method is changed to create a thread group, and two Thread objects are put in that group. Then, both threads are started, and the thread group's stop method is used to stop both of them. Running this gives the following result:
>java TestThreads 30 Running... ... press to stop 0 seconds 0 seconds 1 seconds 1 seconds 2 seconds 2 seconds ...thread killed ...thread killed
You see each line twice because two threads are running.
This ability to control multiple threads as a group is a welcome addition that Java offers above the typical operating-system support for threads. You will find it can save much ugly code. Note that the same object (thisObject) is used in this example and two threads are spawned on it. This is quite legal and quite common.
Note |
The example shown here only catches ThreadDeath, so it only runs cleanup code when threads are stopped-not when they die for other reasons or run cleanly. If cleanup really is important regardless of how the thread ends, you should put it in a finally clause, as discussed in the previous chapter. |
Once again, though, the recommendation is to not use the stop method, but rather to iterate through each thread in the group and set its instance variable to make it voluntarily stop. We wish they would have added a method signature in the Runnable interface for this voluntary stop idea (for example, setStop(boolean)), but that would have affected too much existing code to do in a 1.2.0 release. You will see later in this chapter how to iterate through the threads in a thread group.
Ending programs with Java running threads
At this point, you might be wondering what happens when the main method ends and threads are still running. You saw that, after spawning the threads, the main method regained control immediately. The threads then started running asynchronously in the background. What happens when the end of the main method is reached, and there are still background threads running? When execution reaches the very end of the main method, the Java Virtual Machine will queue up its "exit" until all active threads have finished running. That is, the program will remain running until those background threads have all finished. You will notice this because you will not get control back at the command line where you issued "java xxx" and you will see Java listed as one of the programs still running in the call stack.
There are times when you simply want to force an exit. That may involve killing any rogue threads still running. You can do this by exiting your program with System.exit(0);. Unlike an implicit or explicit return statement, this does not wait for running threads to end. Sometimes this is necessary for idle background threads, as you will see when using the AS/400 Toolbox for Java classes, for example.
Daemon Threads
The statement about programs not ending until all threads have finished does have a corollary: When you create a Thread object, you can invoke the setDaemon(true) method on it to identify this thread as a daemon (pronounced "dee-mon").
This doesn't mean you've sold your soul! It means this thread is a service thread that never ends. At program end time, Java will not wait for daemon threads before exiting. Instead, it will just kill those threads. An example of a daemon thread is a timer that just runs in the background and sends out "tick" events, say. Another example might be a thread that watches a data queue or phone line. Marking these types of threads as daemons saves you the trouble of explicitly killing them when you are ready to exit your program.
Even if you do not use threads yourself, Java considers your non-threaded code to be part of a main thread. There are other default threads in any Java program, notably the garbage collector. This a daemon thread that always lurks in the background, waiting for an opportunity to vacuum up an unused object. Using a graphical user interface causes another thread to run as well to watch for user events like mouse movements. However, this is not a daemon thread, so you must explicitly code System.exit(0) to end a GUI application.
Thinking Asynchronos Danger Lurks
The examples of threads so far have been used to allow long-running code to be interrupted. In a real-world application, you will also use threads in other ways. For example, you will use them for any potentially long-running operation to ensure overall system efficiency and higher throughput. Just as a bank has multiple tellers and a grocery store has multiple checkout counters, your programs will often have multiple asynchronous transaction threads. Often, this design will involve one common repository class, such as a bank or store class, and a separate transaction class that is threaded. You will spawn multiple transaction threads, each taking as input an object in the repository, and acting on that object. The transaction thread class will take an instance of the repository class as a constructor parameter, and its run method will invoke one or more of the methods on that repository object. This is illustrated in Figure 11.5.
Figure 11.5: Multiple threads of execution acting on a single object
In this design, you will end up with many simultaneous threads using a single instance of an object (a singleton). The implication is that they will be attempting to view and change the variables in that object simultaneously. Consider an RPG IV module that is used in a *PGM object. It has global variables, just as a Java class has instance variables. Like threads, you can have multiple users running your program simultaneously. However, each user gets his or her own copy of those global variables. With threads, they all share the same copy! This can be dangerous, of course. The threads might "step on each other," with one changing a variable that undermines another.
As an application programmer, you are used to this. You already have to deal with the problems of simultaneous access to your database, and you religiously use locking mechanisms to ensure the integrity of the database. Thus, to build "thread safe" Java programs, you have to learn the Java syntax and idioms necessary to do with instance variables what you already do with database records.
The role of threads in a complex multi user application
Will you have to worry about complex, multithreaded applications? Perhaps not, if all you are doing initially is adding a Java GUI onto your host RPG application. In this case, your Java user interface will run on the client, and each concurrent user will invoke independent instances (jobs) of your RPG backend code as needed. Your existing database logic in the RPG code will be as robust as always. However, as you delve deeper into writing and running Java server code on the AS/400 itself, you might come to a design like the one in Figure 11.6.
Figure 11.6: The threaded server application architecture with one or more threads per client
In this scenario, instead of having separate AS/400 jobs servicing each client, you have only one server job running, with one or more threads per client. This scales better (al- though admittedly the AS/400 does an exceptional job of handling many thousands of jobs) because threads have less overhead than separate jobs. Combined with RMI (Remote Method Invocation), CORBA (Common Object Request Broker Architecture), or servlets, this can offer an effective new way to design large-scale applications with thousands of concurrent users. To do this, however, you will have to delve deeply into threads and thread safety.
How single objects can have multiple threads of execution
You might be a tad unclear as to how one object can have multiple threads of execution. Think of a bank object. At any one time, it may have thousands of individual threads calling its transferFunds method. It might seem confusing to have so many executing threads, perhaps all on the same method. Do not mix up objects with executing threads. One is about memory, and the other is about instruction pointers.
It might help to think of the object as a database file, the methods as RPG programs that use that database, and the threads as users running the RPG programs. You have only one database file but, at any one time, you have many users running the RPG programs that manipulate that database.
Multithreaded danger An inventory example
To see how multiple threads using a shared object can be dangerous, consider a system where orders of an item are accepted and fulfilled. The in-stock inventory of the item is also monitored. You might have a class named Inventory that manages this, as shown in Listing 11.8.
Listing 11.8: An Inventory Class that Fulfills Orders and Manages Inventory Stock
public class Inventory { private static final int AMOUNT_INCREMENT = 2000; private int onHand = 5000; // amount in inventory public boolean stop = false; // stop whole thing /** method to fulfill an order */ public void takeOrder(int howMany) { int old = onHand; String error = ""; if (stop) // have we been stopped? return; // exit now if (howMany > onHand) { // increase inventory addToInventory(howMany); error = "Order: " + howMany + ", old: " + old + ", new: " + onHand; } onHand = onHand-howMany; // actually take order if (onHand < 0) // should never happen, but still... { System.out.println("Error-onHand less than zero! " + onHand); System.out.println(error); stop = true; } } // end takeOrder method /** method to increase inventory stock, taking into * account the size of the current order */ private void addToInventory(int howMany) { if (howMany > AMOUNT_INCREMENT) onHand +=(howMany-onHand)+1; else onHand += AMOUNT_INCREMENT; } } // end Inventory class
This is a very simple class. It starts with an initial amount of inventory onHand (5,000), and each order taken (takeOrder method) decrements the amount of the order from the inventory. First, however, a check is made to ensure the size of the order will not deplete the current inventory. If this would be the case, the inventory is increased before the order is filled (addToInventory method). Note that it checks the stop instance variable before even bothering to enter the body of the method. You will see where stop is set at the end of the method.
This is very basic stuff-what could go wrong? Look at the takeOrder method. Because the inventory is bumped up to cover the current order whenever necessary (admittedly a non-robust algorithm), it seems ludicrous to have the "if (onHand < 0)" check. How can it get below zero if the lines of code just above it ensure that it does not? In a synchronized world, of course, it cannot. But in a threaded world, it can. To see this, you need another class-a thread class- whose run method will call the takeOrder method on an instance of Inventory. This typical "transaction" thread class is shown in Listing 11.9.
Listing 11.9: A Class to Place a Single Order in a Thread
public class OrderThread implements Runnable { Inventory inventoryObject; // passed in to us int howMany; // how many items to order /** constructor */ public OrderThread(Inventory inventoryObject, int howMany) { this.inventoryObject = inventoryObject; this.howMany = howMany; } /** "run" method, called by using Start(). * This method places the order for the given amount */ public void run() { // place the order inventoryobject.takeOrder(howMany); } }
An instance of this class will be created for every order, and it will be run as a thread. However, there will be only a single instance of the Inventory class. That instance will be passed in via the constructor to every instance of this OrderThread class. This makes sense; while you get many orders, there should never be more than one inventory.
A final class, shown in Listing 11.10, is needed to test this little system. It contains the main method to get control from the command line. This will create a single Inventory object, but many sample OrderThread objects, to really stress-test Inventory.
Listing 11.10: Code to Run and Test the Inventory Class
public class TestInventory { public static void main(String[] args) // cmdline entry { Inventory inv = new Inventory(); java.util.Random random = new java.util.Random(); int idx; // place the order inventoryObject.takeOrder(howMany); System.out.println("Running... "); for (idx = 0; (idx <= 1000) && !inv.stop; idx++) { int nextRandom = java.lang.Math.abs(random.nextInt()); nextRandom = (nextRandom % 10000) + 1; OrderThread newOrder = new OrderThread(inv,nextRandom); Thread newThread = new Thread(newOrder); newThread.start(); } if (inv.stop) System.out.println("...stopped at: " + idx); else System.out.println("...all orders placed."); } // end main method } // end TestInventory class
This test creates a thousand order-taking threads, and each one asks for a random number of items that ranges up to 10,000. Potentially, all of these threads will run simultaneously, really testing the logic that is designed to never let the inventory fall below zero. This code creates a single instance of the Inventory class and passes it into every instance of OrderThread, so that all threads are operating on a single object. The Random object from the java.util package generates random numbers for the simulated orders. If the inventory ever does fall below zero (seemingly impossible, but still…), the code notices this and stops creating new threads because the system has obviously degenerated and is now unstable.
If you compile and run these classes, you get output similar to the following:
Running... Error-onHand less than zero! -12814 Error-onHand less than zero! -20960 Error-onHand less than zero! -30707 Error-onHand less than zero! -38948 Error-onHand less than zero! -47915 Error-onHand less than zero! -57462 Error-onHand less than zero! -64871 Error-onHand less than zero! -67549 Error-onHand less than zero! -68902 Error-onHand less than zero! -76587 Order: 6277, old: 2004, new: -6537 Error-onHand less than zero! -79915 Error-onHand less than zero! -84342 Error-onHand less than zero! -94274 Order: 8146, old: 577, new: -12814 Order: 9747, old: 1263, new: -20960 Order: 8241, old: 4602, new: -30707 Order: 8967, old: 7394, new: -38948 Order: 9547, old: 8968, new: -47915 Order: 7409, old: 5518, new: -57462 Order: 2678, old: 991, new: -64871 Order: 1353, old: 512, new: -67549 Order: 7685, old: 2512, new: -68902 Order: 3328, old: 589, new: -76587 Order: 4427, old: 2805, new: -79915 Order: 9932, old: 385, new: -84342 ...stopped at: 86 Error-onHand less than zero! -6537 Order: 9088, old: 5323, new: 2551
There are a number of very interesting (scary?) things about this output:
- The inventory falls below zero despite explicit code to check and prevent that!
- Even after the stop variable stops new threads from executing the takeOrder thread, numerous Error outputs are returned, indicating these threads were already past the "if (stop) return;" code at the beginning of the method.
- The Error and Order lines are not always synchronized in the output, even though they are issued one line after another in the code.
- The Error and Order lines both print out the value of the onHand variable (look at the "new" value for the Order lines), just at different points in the takeOrder method. However, while no changes are made to the variable in the intervening code, the variable value has still changed between the two.
All of this clearly indicates one thing: computers cannot be trusted! Actually, it demonstrates that there are multiple threads of execution running inside the takeOrder method simultaneously. The switch from one thread to another can, and does, happen quickly (from one line to the next) and arbitrarily. This causes a problem because of the common variable (onHand) that these lines of code are sharing and manipulating.
Another thread is gaining control between the line of code that checks the onHand balance:
if (howMany > onHand)
and the line of code that decrements the balance:
onHand = onHand-howMany;
It is running the same decrementing line of code. As shown in Figure 11.7, the check is passing for a particular thread, but by the time it actually does the onHand variable decrement, another thread has already decremented the variable.
Figure 11.7: Thread time-splicing
This causes the variable to be decremented twice without the check, letting it go below zero. Threads work by preemptive time-slicing. That is, each thread is given a small amount of CPU time to perform a few atomic instructions, then it is preempted, and another thread is given a similar amount of CPU time to perform a few of its instructions. This continues until each thread is complete (by reaching the end of the run method). An atomic instruction is essentially one line of bytecode. Generally, a single line of Java source code compiles into numerous Java bytecode instructions. This is not unlike RPG, where a single C-spec statement can compile into multiple underlying machine-code instructions. This means you cannot guarantee that an entire line of source code will run before the next thread is given control.
The solution Synchronizing asynchronous execution
This might sound hopeless. If you cannot guarantee the order of execution, how can you possibly guard against these unexpected concurrency errors? The answer to providing thread safety is elegantly simple. It involves merely adding one Java keyword to one line of code!
You need to be able to guard against this unexpected interruption whenever you have code that depends on a common variable remaining stable from one line to the next. The magic keyword in Java to do this is synchronized. When specified as a method modifier, it tells Java that the entire method needs to be executed without interruption by other threads. Effectively, it allows only one thread to execute this method at a time. All waiting threads get queued up "at the door." As each thread finishes executing the method, the next waiting thread is let in.
Let's simply change the takeOrder method definition to include the modifier synchronized, as shown in Listing 11.11.
Listing 11.11: Specifying the synchronized Method Modifier
public synchronized void takeOrder(int howMany) { int old = onHand; String error = ""; if (stop) // have we been stopped? return; // exit now if (howMany > onHand) { // increase inventory addToInventory(howMany); error = "Order: " + howMany + ", old: " + old + ", new: " + onHand; } onHand = onHand-howMany; // actually take order if (onHand < 0) // should never happen, but still... { System.out.println("Error-onHand less than zero! " + onHand); System.out.println(error); stop = true; } }
Now, after compiling, run the test again. You should get no unexpected errors:
>java TestInventory Running... ...all orders placed.
This code will run more slowly slower because you have considerably reduced the amount of asynchronous execution. However, it will run correctly, and that, after all, is the fundamental requirement.
Fine grained synchronization
Instead of synchronizing the entire takeOrder method, you could actually synchronize just the lines of code you need to guard instead. Java defines a synchronized block as a block of code that can be placed inside a synchronized statement so only that block is protected from interruptions by other threads.
To use this fine-grained synchronization, you have to think carefully about what code is exposed by multiple concurrent threads of execution. At a minimum, it is any code that changes a common instance variable. If you have code that tests the current value of the variable and then does work based on that current variable value, you will need to synchronize the entire block. For each line of code, you need to always be thinking, "What if the value of the variable changed right now?"
In the case of the example, the onHand variable check and the onHand variable decrement need to be treated as a single unit of operation. This guarantees that no other thread can decrement the variable in between, which would cause an underflow. So, remove synchronized from the takeOrder method declaration and instead place it around this sensitive block of code, as in Listing 11.12.
Listing 11.12: Using a synchronized Block Versus a Method
synchronized(this) { if (howMany > onHand) { addToInventory(howMany); // increase inventory error = "Order: " + howMany + ", old: " + old + ", new: " + onHand; } onHand = onHand-howMany; // actually take order } // end synchronized(this)
In this example, there will be no appreciable difference in total execution time, only because you have to put almost the entire method's code into the synchronized statement anyway. However, if there were a significant amount of other code outside of the synchronized block, you would see overall throughput improvements.
In general, you should simply use synchronized at the method level (as a modifier) on any methods that manipulate common variables, unless:
- You are very sure about the minimum set of code that needs to be synchronized. In this example, moving the line that decrements onHand out of the synchronized block causes the unexpected error situations to happen again. If you were not checking for this in the code, you would have ended up with a very serious bug that could have gone undetected for a long time, or until a supplier received a negative-amount order!
- The synchronized versus unsynchronized code ratio is worth the extra risk.
Was it even worth using threads in this example? Maybe not, since you ended up having to synchronize the majority of the code. However, threads are usually a good idea because your code per transaction is usually complex, and the synchronized part-even if it is an entire method-is relatively small. That is, usually, the thread will involve more code than a single method call.
The use of threads can give very busy applications at least the chance for individual transactions to be completed in a shorter time than if they all had to wait for the previously submitted transactions to complete. Further, by spawning threads, you give control back to the user immediately, rather than forcing him or her to wait an indefinite amount of time for the transaction to complete. This reason alone dictates that threads should be used more often than not for user-initiated transactions. "Leave the customer in control" is a maxim to live and code by.
Threaded application design alternatives
The example puts each transaction in its own thread. This is not the only possible design, of course. Another option would be to instead give each user his or her own thread and let it perform the transactions synchronously within the thread. This is a reasonable alternative because users will expect their own transactions to be performed in the order they are submitted anyway. It might, thus, reduce the overall number of threads running and improve response time. If you have too many threads competing for processor time, however, you might run into thrashing-a situation where so many threads are running that each one gets only enough time to do a minuscule amount of work each slice.
Another option would be to create a fixed-size thread pool of transaction or service threads. In this design, a predetermined number of threads-say, a dozen or so-are spawned at application start time, and each transaction or thread-qualifying request is fed to the next available thread. If no thread is available, the request is queued up and the next transaction thread to become available reads it from the queue and executes it. Or the thread-pool grows by one thread to a pre-set maximum. Again, this can reduce the amount of thread-switching and improve performance for very heavy-use applications.
MORE ON THE Synchronized KEYWORD
You have now seen the basics of threads in Java. The remainder of this chapter goes into more detail, and can be safely skipped if you are only looking for an introduction to Java or to threads. If you are ready for more detailed information on threads, however, read on.
The synchronized keyword, as you have seen, can be specified as a method modifier or as a code-block keyword. In the latter case, you saw in the example that it requires a parameter. In the example, the keyword this represented the current object. The synchronized keyword is actually equivalent to the AS/400 command ALCOBJ (Allocate Object) with the *EXCL (Exclusive, no read) parameter. That is, it locks an object so that you have exclusive access to it. It always locks some Java object. When used as a method modifier, it locks the object the method is part of. When used as a code-block keyword, it locks the object you specify as a parameter. In both cases, the entire object is locked, not just the method or code block. Thus, at runtime, when a thread (including the main thread) calls a synchronized method or tries to enter a synchronized block, the algorithm is this:
- Is the specified object locked? (Is another thread running any synchronized code for this object right now?)
- If yes, wait in this object's queue.
- If no, lock this object, run the code.
When the code is done running (execution reaches the end of the method or block), the object is unlocked, and the next thread in the queue is allowed in. Just as with ALCOBJ, nested synchronized methods or blocks on the same object bump up the lock count for the object. It is not until the current thread that has the lock reduces the lock to zero that the object is finally unlocked for others to use, as shown in Figure 11.8.
Figure 11.8: Synchronized lock count
The use of synchronized as a method modifier is actually equivalent to putting the entire method body in a synchronized(this) block. It is, to be sure, the safest and easiest way to synchronize sensitive code that changes common variables in this object. However, there will also be times when code in one method changes variables in another object (either directly or through setXXX methods). In these cases, you have to use synchro nized(object) blocks around the sensitive code, where object is the object reference variable that will be changed.
When you lock an object via the use of synchronized (either as a method modifier or a code-block keyword), it is important to know that you do not block other, unsynchronized methods from running in that same object. This means you can have one thread running an unsynchronized method that reads the common variables at the same time another thread is running inside a synchronized method that perhaps changes those variables. Locking an object only affects other threads that attempt to run synchronized code on that object. Normally, code that reads only a common variable is okay to leave unsynchronized, unless it is doing multiple lines of code that depend on the variable not changing from one line to the next. For example, if you have something like this:
if (account < 0) sendNotice("You have an outstanding account of " + account);
you will clearly have to be careful that the account value does not go above zero by the time the sendNotice method is called in the second line. These two lines should be placed inside a synchronized(this) block to ensure the common variable does not change from one line to the next.
Synchronizing producer consumer relationships with wait notify
There are times when you will have one synchronized thread that needs to wait on another thread. Java supplies two methods, each one part of the base java.lang.Object class, which are available to all. They are named wait and notifyAll. These methods can only be used inside synchronized methods or blocks. The wait method will wait indefinitely or (optionally) for a specified number of milliseconds, until another thread calls notifyAll. The wait method is always used inside a loop that is checking for some condition on which the thread depends. After waiting, the condition is rechecked:
while (variable < threshold) wait();
The thread that calls wait gets put on a wait queue for the current object, until the object is unlocked. That allows another thread to get in for this object. The threads on a wait queue are only released when notifyAll is called by some other thread for this same object.
There is actually a method named notify as well, which explicitly releases the thread that has been waiting the longest. NotifyAll will release all waiting threads. What does it mean to be released? It means this thread is put back in the queue, waiting to get into the synchronized object. Another thread might have gotten in because wait resets the lock count to zero in the meantime. When it does finally get back in, it starts executing again where it left off at the wait method call. The lock count is then restored to the value it had when the thread call originally called wait.
In Figure 11.9, Tn represents Thread n. If there were more threads in the wait queue, notifyAll would move them all to the lock queue, while notify would move only the first one (T1, in this case). Note that a thread will also move from the wait queue to the lock queue if it specified a number of milliseconds in the call to wait(mmmm), and that time limit has expired.
Figure 11.9: A lock queue versus a wait queue
You will use the wait/notify pair when one section of code produces some output that another section of code (perhaps the same method, perhaps a different method) depends on. For example, you might have a queue object with synchronized methods for reading and writing the queue. The read method would wait until the queue is non-empty:
// inside read method while (isEmpty()) wait();
The write method would notify or notifyAll after adding the entry to the queue:
// inside write method addEntry(newItem); notify();
When do you use notify and when do you use notifyAll? That's a good question. If only there were a good answer. In this case, only one item was added to the queue. Because you know only one thread will be able to use it, notifyAll is not appropriate. If there were an append method that added numerous items to the queue, you would use notifyAll so that all waiting threads would get a chance. The worst that will happen is that one or more threads will return to life only to find their condition still is not met. They will redo their wait call.
A thread calling the read method in this case would be a good candidate for a daemon thread. You would typically not want the application to be prevented from exiting if that read method was still waiting for an entry on the queue.
Deadly embrace
Synchronization is a great tool to ensure correctness in your multi-threaded applications. However, it is dangerous tool, as well. You can very easily arrive at a situation where all running threads are blocked because they are waiting for each other. This is like stating "When two trains meet at an intersection, neither can leave until the other is gone."
Consider a situation where thread T1 runs synchronized method obj1.method1(), locking obj1. Thread T2 runs synchronized method obj2.method2(), locking obj2. Now, obj1.method1 tries to call obj2.method2, and so thread T1 is put in the lock queue for obj2 while it waits for thread T2 to finish. But obj2.method2 calls obj1.method1, and so thread T2 gets put in the lock queue for obj1 while it waits for thread T1 to finish. As shown in Figure 11.10, each is now blocked, waiting for the other, and will wait forever. No other threads needing these objects will ever run. Also, unless these are daemon threads or System.exit is used, the program will not end unless it is forcefully killed.
Figure 11.10: Deadlock!
This is a deadly embrace known as deadlock. There is nothing that Java can do to help you here! If you hit this problem, it will manifest intermittently (because it is timing dependent) and be a complete bear to debug. You need to avoid the problem completely by careful design-that is, by avoiding mutual calls from one object's synchronized method to another's and back again. If necessary, use a third object with a synchronized method that makes the necessary calls to the other two objects via unsynchronized calls.
The wait/notify pair neither helps nor hinders deadlock, but it does add the risk of a thread waiting forever if another thread does not someday notify it. However, because the waiting thread unlocks the object, at least other threads have a chance to run. If it is important for the waiting thread to eventually run (say, if it is waiting on resources in order to place a customer order), then this will still be a serious problem. You might want to specify a time-out limit, even if it is 10 minutes, on the wait method to indicate when something appears to be stuck. Of course, there is a risk that it is waiting on itself:
public synchronized void waitOnMe() { while (variable < threshHold) wait(); variable += threshHold; notify(); }
In this case, the code that notifies waiting threads is clearly unreachable. There is no point in waiting on a situation your subsequent code will address. Just go ahead and address it!
Thread Priorities
The discussion about synchronization uses the terms lock queue and wait queue. They are misnomers, however, in that they imply that threads are put into and taken off queues in a deterministic manner-say, first in, first out. In fact, they are randomly chosen by the thread scheduler. The algorithm used to choose them is not programmatically predictable. It may vary from platform to platform, depending on the underlying operating system's scheduling support for threads. Better terms might be "lock set" and "wait set," but these create their own aura of confusion.
One of the criteria Java tries to enforce in the scheduling of threads (and all multithreaded operating systems support) is thread priority. By default, your threads will all have normal priority and, thus, the same relative weighting for this criteria. However, by using the Thread method setPriority, you can set the priority to any number between one (lowest) and 10 (highest). The default is five (normal). For convenience, there are predefined constants in the Thread class for MIN_PRIORITY (one), NORM_PRIORITY (five), and MAX_PRIORITY (10).
New threads inherit the priority of their parent threads (the main thread has normal priority), or that of their ThreadGroup, if one is specified. Using ThreadGroups is a convenient way to set the priorities of all similar-role threads.
When the thread scheduler needs to pick another thread for its turn to run, get off the locked queue, or get off the wait queue, it will use an algorithm involving both the thread's priority and its time waiting so far. Higher-priority threads, all things being equal, will get more CPU time than lower-priority threads. It is a general rule of thumb that user-interface threads run at a high priority to improve response time, and daemon background threads run at a low priority to take whatever cycles they can steal.
Loose Threads
We conclude this chapter by discussing some other aspects and functions available to you as a threaded programmer:
- Named threads. When you create a new Thread object, you can specify a new unique name for that thread, and query it at any time. This can be helpful for debugging or logging purposes.
- Yield. This is a friendly static method in Thread that you can use in your code to voluntarily give up your current CPU slice and let another thread run. If all your threads consist of the same synchronized method, there is no point in invoking yield because no other thread can run anyway. Until you finish the method and, hence, relinquish the lock, the other threads remain stuck on the lock queue. Unlike wait, yield does not unlock the object and does not put your thread on the wait queue.
- Suspend and resume. One thread can suspend another by calling its suspend method (good choice of method name, no?), and subsequently bring it back to life by invoking its resume method. This could be useful when you want to confirm a user's cancel request, for example. While asking the user to confirm, you suspend the thread (or ThreadGroup), and if the decision is not to cancel after all, you resume it. These also make a Pause push-button easy to implement.
- Join. This is an interesting method! It is easy to wait for a thread to finish. You simply call join on its Thread object. This will not return until that thread has finished (by returning from its run method). An example of where this is very useful follows shortly.
- Checking the pulse. You might occasionally need to determine if a given Thread object has finished running or not. The Thread method isAlive will return true if the thread has not yet finished (not returned from its run method). Be careful, though-if you have not yet called start on this thread, you will get false from this query.
Timing Threaded Applications
When writing applications, you often want to measure the elapsed time, to gauge the performance impacts of changes you make. This is especially true in a multithread application, where you want to ensure that adding a wait here, a yield there, and a synchronized statement over there does not seriously degrade the overall throughput of the application.
To measure an application's time, an easy trick is to change the main entry point to record the current time in milliseconds at the very beginning, and record it again at the very end. Then, take the difference and spit it out to standard-out (that is, the console) or a log file. Make a number of test runs through the application and average the total elapsed time. If you change something, you can rerun the test cases and compare the average elapsed time. Of course, this will be very dependent on the current load of the system, so it has to be taken with a grain of salt. For code running on the workstation, the usual trick is to run the test cases after a fresh reboot, so that memory is in as consistent a state as possible between runs.
To measure the elapsed time in the inventory example, change the main method in the TestInventory class to print out the elapsed time in milliseconds that the entire process takes. This is easy using the ElapsedTime class from Chapter 8 (shown in Listing 8.2). Just instantiate this class and call its setStartTime at the beginning of the code in main, and its setEndTime at the end of the code in main, and write the result out to the console.
There is a trick, though! You can't just take the time at the end of the main method because at that point, the threads are still running. Thus, you need a way to wait for all active threads to complete and then record the ending time. Do this by creating all the threads in a single ThreadGroup and then waiting for them all to finish. This is a good strategy, anyway, because it also gives an easy way to stop all these threads when something untoward happens or is detected: just use object.stop() on the ThreadGroup object.
How do you wait for all the threads in the group to finish? Painfully, as it turns out! You have to enumerate all the threads in the group, then invoke the Thread method join on each of them. The method join "waits" on the thread to finish, and only then do you get control back. (If it is already finished, you get control back immediately.)
A join method for the ThreadGroup itself is missing from Java, but the code is not intellectually taxing. We wrote a helpful static method named waitOnThreadGroup to do this, shown in Listing 11.13. You simply code in a call to it and don't get control back until all threads in the given ThreadGroup have completely. Note that join may throw an InterruptedException exception, so you have to catch it.
Listing 11.13: The Revised TestInventory Class with Time-Recording Logic
public class TestInventory { public static void main(String[] args) // cmdline entry { Inventory inv = new Inventory(); java.util.Random random = new java.util.Random(); int idx; ElapsedTime timeRecorder = new ElapsedTime(); timeRecorder.setStartTime(); System.out.println("Running... "); ThreadGroup orderGroup = new ThreadGroup("Orders"); for (idx = 0; (idx <= 1000) && !inv.stop; idx++) { int nextRandom = java.lang.Math.abs(random.nextInt()); nextRandom = (nextRandom % 10000) + 1; OrderThread newOrder = new OrderThread(inv, nextRandom); Thread newThread = new Thread(orderGroup, newOrder); newThread.start(); } if (inv.stop) System.out.println("...stopped at: " + idx); else System.out.println("...all orders placed."); waitOnThreadGroup(orderGroup); timeRecorder.setEndTime(); System.out.println("Elapsed time: " + timeRecorder); } // end main method public static void waitOnThreadGroup(ThreadGroup group) { Thread allThreads[] = new Thread[group.activeCount() + 10]; group.enumerate(allThreads); for (int idx = 0; idx < allThreads.length; idx++) { if (allThreads[idx] != null) try { allThreads[idx].join(); } catch (InterruptedException exc) {} } } } // end TestInventory class
With accurate elapsed-time checking in place, you can make a few sample runs and record the average time:
>java TestInventory Running... ...all orders placed. Elapsed time: Elapsed time: 0 hours, 0 minutes, 0 seconds, 440 milliseconds >java TestInventory Running... ...all orders placed. Elapsed time: Elapsed time: 0 hours, 0 minutes, 0 seconds, 460 milliseconds >java TestInventory Running... ...all orders placed. Elapsed time: Elapsed time: 0 hours, 0 minutes, 0 seconds, 420 milliseconds
The average time in this case is 440 milliseconds (your mileage may vary). Interestingly enough, for the first edition of this book, on a slower machine and an earlier JDK, this took 6.5 seconds!
Summary
This chapter covered threads in Java, including the following concepts:
- There are two ways to define threads: extend the Thread class or implement the Runnable interface.
- Once you have a Thread object, you start it by calling start, which in turn calls the run method in your class.
- The ThreadGroup class is for grouping similar-function threads.
- To stop a thread, either set an instance variable that the threaded code checks occasionally, or use the deprecated stop method from the Thread or ThreadGroup class.
- When the main method reaches the end or a return statement, the exit is queued up until all non-daemon threads run to completion. To force termination, use System.exit(0);.
- Call setDaemon on the Thread object to identify it as a background that is to be stopped when the main method ends.
- Use the method modifier or block statement synchronized to avoid common variable corruption by forcing only a single thread at a time into the synchronized code.
- Use Thread's wait and notify methods as a means of waiting and triggering co-dependent synchronized threads.
- Deadlock is a deadly embrace where two methods are waiting for each other to finish before continuing.
- Threads have a priority, set by Thread's setPriority method. The higher the priority, the more CPU cycles the thread gets.
- The Thread class has methods sleep, yield, suspend, resume, and isAlive for managing the non-synchronized thread state.
- The Thread method join is used to wait for a running thread to finish.