| < Day Day Up > |
|
This section demonstrates how to write threads in Java and how the execution of threads differs from the traditional procedural programs with which most programmers are familiar. First, examples of simple procedural and concurrent programs illustrate how they differ in structure. Additional procedural and concurrent programs are then contrasted to show how a concurrent program can produce different, and nondeterministic, results.
To demonstrate how to structure a program that uses threads, this section contrasts a simple procedural program with a single method call with a similar concurrent program that starts one thread. The program in Exhibit 1 (Program2.1) is a simple Java procedural program that starts executing in main. This program creates an object of type ProceduralExample and then calls the run method in that object. The main method then suspends executing, and the program continues executing in the run method. When the run method completes, the program returns to the main method, finishes executing in the main method, and then exits the program.
Exhibit 1: Program2.1: Procedural Program
public class ProceduralExample { public void run() { System.out.println("In Run"); } public static void main(String args[]) { ProceduralExample pe = new ProceduralExample(); pe.run(); } }
A similar program using threads is provided in Exhibit 2 (Program2.2). This program also starts in main and creates an instance of the RunnableExample object. The program does not then call the run method in the RunnableExample object, but instead creates a Thread object that is passed to the RunnableExample object as a parameter to its constructor. This "registers" the RunnableExample object with the thread.
Exhibit 2: Program2.2: Concurrent Program Created Using the Runnable Interface
public class RunnableExample implements Runnable { public void run() { System.out.println("In Run"); } public static void main(String args[]) { RunnableExample re = new RunnableExample(); Thread t1 = new Thread(re); t1.start(); } }
The start method is now called on the Thread object, and the thread begins executing. The main is now free to continue executing (as will be seen in Section 2.3.3), but so is the new thread object. When the start method of the Thread object is called, it starts by calling the run method in the object that was registered with its constructor (in this case, the RunnableExample object). The run method of this object prints out the message and ends. It does not return to the main; instead, because both the main and the created thread have completed executing, the program exits.
Note that the RunnableExample object needed to be declared as "implementing Runnable." This is because the Thread object is expecting to be able to call the run method on this object when its start method is called. Because the Thread object does not know about this RunnableExample object, the only way it can guarantee that the run method will be available is to require that any object that is to be passed to its constructor must implement Runnable, which in turn ensures that it will have a run method to call. This was not necessary in the ProceduralExample program because the definition of the actual object to be called (the ProceduralExample class) was available when the call to the run method was made. Interfaces are covered in greater detail in Chapter 5; for now, simply realize that when thread objects are created they must be given an object that implements Runnable in their definition.
Another way to create a thread does not involve passing an object that implements Runnable to the thread. This can be done by having the class extend the Thread class, as in Program2.3 (Exhibit 3). Because Thread already implements Runnable, the object that extends the Thread class automatically implements the Runnable object. The constructor now does not have an object passed to it because it is itself the Runnable object. The thread can determine if an object was passed to the constructor or not. If one was, it will call the run method on the passed object; otherwise, it will call the run method on the "this," or current, object.
Exhibit 3: Program2.3: Concurrent Program Created by Extending the Thread Class
public class ThreadExample extends Thread { public void run() { System.out.println("In Run"); } public static void main(String args[]) { ThreadExample t1 = new ThreadExample(); t1.start(); } }
These two mechanisms are very similar and can almost be used interchangeably; however, Java supports only a single inheritance model, used by the mechanism to create a thread that extends class Thread. Therefore, classes that extend Thread cannot be used in other inheritance hierarchies, which is a drawback. Because no corresponding negative behavior in regard to using objects that implement Runnable exists, this method is always applicable and preferred. This book always implement threads using the Runnable interface method.
Section 2.3.1 used various mechanisms to describe how the procedural and concurrent programs run; however, because they produced the same results, this is likely to be seen as a distinction without a difference. This section shows how different mechanisms can actually produce very different results.
Program2.4 (Exhibit 4) is a simple procedural program that creates two objects, giving them different numbers so that the outputs from the objects can be distinguished. The run method of the first object is called, producing two lines of output. The program then returns to main, and the run method of the second object is called, again producing two lines of output. This second method completes, and control is returned to main where two more lines are printed out. This output, shown in Exhibit 5, will be the same no matter how many times this program is run. The order of execution is always the same, so we call this execution totally ordered.
Exhibit 4: Program2.4: Procedural Program with Two Method Calls
public class Procedural { private int myNum; public Procedural(int myNum) { this.myNum = myNum; } public static void main(String argv[]) { Procedural a = new Procedural(1); Procedural b = new Procedural(2); a.run(); b.run(); try { Thread.sleep((int)(Math.random() * 100)); System.out.println("in main"); Thread.sleep((int)(Math.random() * 100)); System.out.println("in main"); } catch(InterruptedException e) { } } public void run() { try { Thread.sleep((int)(Math.random() * 100)); System.out.println("in run, myNum = "+ myNum); Thread.sleep((int)(Math.random() * 100)); System.out.println("in run, myNum = "+ myNum); } catch(InterruptedException e) { } } }
Exhibit 5: Output from Exhibit 4 (Program2.4)
in run, myNum = 1 in run, myNum = 1 in run, myNum = 2 in run, myNum = 2 in main in main
Program2.5 (Exhibit 6) is similar to Program2.4 (Exhibit 4) except that now the calls to the methods have been replaced with threads. While Program2.5 looks similar to Program2.4, it behaves in a very different way. In Program2.5, the main creates two objects and uses these objects to create two threads, t1 and t2. The start method for thread t1 is called, and now both the main and thread t1 are free to execute. If the main executes first, it starts t2 and all three threads are free to run. Each thread now has an equal chance of running. Thus, the main could run first, followed by t2 twice, then main, and then t1 twice, as in the first example of output from this program provided in Exhibit 7. It is equally likely that t2 will run, followed by main, then t1, t2, main, and t1, as in the second example output shown in Exhibit 7. In fact, it is not very difficult to show that this simple program has 24 combinations of possible outputs.
Exhibit 6: Program2.5: Concurrent Program with Two Threads and a Main Thread
public Concurrent(int myNum) { this.myNum = myNum; } public static void main(String argv[]) { Concurrent a = new Concurrent(1); Concurrent b = new Concurrent(2); Thread t1 = new Thread(a); Thread t2 = new Thread(b); t1.start(); t2.start(); try { Thread.sleep((int)(Math.random() * 100)); System.out.println("in main"); Thread.sleep((int)(Math.random() * 100)); System.out.println("in main"); } catch(InterruptedException e) { } } public void run() { try { Thread.sleep((int)(Math.random() * 100)); System.out.println("in run, myNum = "+ myNum); Thread.sleep((int)(Math.random() * 100)); System.out.println("in run, myNum = "+ myNum); } catch(InterruptedException e) { } } }
Exhibit 7: Two Possible Outputs from Exhibit 6 (Program2.5)
One Possible Output in main in run, myNum = 2 in run, myNum = 2 in main in run, myNum = 1 in run, myNum = 1 Another Possible Output in run, myNum = 2 in main in run, myNum = 1 in run, myNum = 2 in main in run, myNum = 1
Earlier, a procedural program was said to have a total ordering; that is, it had only one correct ordering of statements in the program. A concurrent program is a partial ordering. By this we mean that even though the threads themselves are ordered and the statements in each thread are actually procedural, the order in which the JVM chooses a thread for execution of its next instruction is not ordered. This allows the statements in the threads to be interleaved, as shown in Exhibit 7. From this example, it is not difficult to see that the number of correct total orderings consistent with the partial orderings imposed by the threads can be very large even for simple concurrent programs. For this program, the number of possible orderings of the output is actually (n + 2)!, where n is the number of threads we start. This large number of correct total orderings makes reasoning about concurrent programs quite complex, much more difficult than for procedural programs. Much of the rest of the text is dedicated to techniques for managing this complexity. How the JVM actually is able to produce this large number of partial orderings is the subject of the Section 2.4, which explains how the JVM builds a process and a thread and how it then uses the information to execute a program.
| < Day Day Up > |
|