A thread (or a thread of execution or a thread of control) is a single sequence of executable statements within a program.
For Java applications, the flow of control begins at the first statement in main() and continues sequentially through the program statements. Loops within a program cause a certain block of statements to be repeated. If-else structures cause certain statements to be selected and others to be skipped. Method calls cause the flow of execution to jump to another part of the program, from which it returns after the method’s statements are executed. Thus, within a single thread, you can trace the sequential flow of execution from one statement to the next.
One way to visualize a thread is to imagine that you could make a list of the program’s statements as they are executed by the computer’s central processing unit (CPU). Thus, for a particular execution of a program with loops, method calls, and selection statements, you could list each instruction that was executed, beginning at the first, and continuing until the program stopped, as a single sequence of executed statements. That’s a thread!
Now imagine that we break a program up into two or more independent threads. Each thread will have its own sequence of instructions. Within a single thread, the statements are executed one after the other, as usual. However, by alternately executing the statements from one thread and another, the computer can run several threads concurrently. Even though the CPU executes one instruction at at time, it can run multiple threads concurrently by rapidly alternating among them.
The main advantage of concurrency is that it allows the computer to do more than one task at a time. For example, the CPU could alternate between downloading an image from the Internet and running a spreadsheet calculation. This is the same way you ate toast and cereal and drank coffee in our earlier breakfast example. From our perspective, it might look as if the computer had several CPUs working in parallel, but that’s just the illusion created by an effectively scheduling threads.
Principle14.2.1.JVM Threads.
The Java Virtual Machine (JVM) is itself an example of a multithreaded program. JVM threads perform tasks that are essential to the successful execution of Java programs.
Principle14.2.2.Garbage Collector Thread.
One of the JVM threads, the garbage collector thread, automatically reclaims memory taken up by objects that are not used in your programs. This happens at the same time that the JVM is interpreting your program.
Subsection14.2.1Concurrent Execution of Threads
The technique of concurrently executing several tasks within a program is known as multitasking. A task in this sense is a computer operation of some sort, such as reading or saving a file, compiling a program, or displaying an image on the screen. Multitasking requires the use of a separate thread for each task. The methods available in the Java Thread class make it possible (and quite simple) to implement multithreaded programs.
Most computers, including personal computers, are sequential machines that consist of a single CPU, which is capable of executing one machine instruction at a time. In contrast, parallel computers, used primarily for large scale scientific and engineering applications, are made up of multiple CPUs working in tandem.
Today’s personal computers, running at clock speeds over 1 gigahertz—1 gigahertz equals 1 billion cycles per second—are capable of executing millions of machine instructions per second. Despite its great speed, however, a single CPU can process only one instruction at a time.
Each CPU uses a fetch-execute cycle to retrieve the next instruction from memory and execute it. Since CPUs can execute only one instruction at a time, multithreaded programs are made possible by dividing the CPU’s time and sharing it among the threads. The CPU’s schedule is managed by a scheduling algorithm, which schedules threads for execution on the CPU. The choice of a scheduling algorithm depends on the platform on which the program is running. Thus, thread scheduling might be handled differently on Linux, Windows, and Macintosh systems.
One common scheduling technique is known as time slicing, in which each thread alternatively gets a slice of the CPU’s time. For example, suppose we have a program that consists of two threads. Using this technique, the system would give each thread a small quantum of CPU time—say, one thousandth of a second (one millisecond)—to execute its instructions. When its quantum expires, the thread would be preempted and the other thread would be given a chance to run. The algorithm would then alternate in this round-robin fashion between one thread and the other (Fig. Figure 14.2.3). During each millisecond on a 300-megahertz CPU, a thread can execute 300,000 machine instructions. One megahertz equals 1 million cycles per second. Thus, within each second of real time, each thread will receive 500 time slices and will be able to execute something like 150 million machine instructions.
Under priority scheduling, threads of higher priority are allowed to run to completion before lower-priority threads are given a chance. An example of a high-priority thread would be one that is processing keyboard input or any other kind of interactive input from the user. If such tasks were given low priority, users would experience noticeable delays in their interaction, which would be quite unacceptable.
The only way a high-priority thread can be preempted is if a thread of still higher priority becomes available to run. In many cases, higher-priority threads are those that can complete their task within a few milliseconds, so they can be allowed to run to completion without starving the lower-priority threads. An example would be processing a user’s keystroke, a task that can begin as soon as the key is struck and can be completed very quickly. Starvation occurs when one thread is repeatedly preempted by other threads.
Depending on the hardware platform, Java threads can be supported by assigning different threads to different processors, by time slicing a single processor, or by time slicing many hardware processors.
Subsection14.2.2Multithreaded Numbers
Let’s consider a simple example of a threaded program. Suppose we give every individual thread a unique ID number, and each time it runs, it prints its ID ten times. For example, when the thread with ID 1 runs the output produced would just be a sequence of ten 1’s: 1111111111.
As shown in Figure 14.2.4, the NumberThread class is defined as a subclass of Thread and overrides the run() method. To set the thread’s ID number, the constructor takes a single parameter that is used to set the thread’s ID number. In the run() method, the thread simply executes a loop that prints its own number ten times:
public class NumberThread extends Thread {
int num;
public NumberThread(int n) {
num = n;
}
public void run() {
for (int k=0; k < 10; k++) {
System.out.print(num);
} //for
} // run()
} // NumberThread
Now let’s define another class whose task will be to create many NumberThreads and get them all running at the same time (Figure 14.2.5).
For each NumberThread, we want to call its constructor and then start() it:
public class Numbers {
public static void main(String args[]) {
// 5 threads
NumberThread number1, number2, number3, number4, number5;
// Create and start each thread
number1 = new NumberThread(1); number1.start();
number2 = new NumberThread(2); number2.start();
number3 = new NumberThread(3); number3.start();
number4 = new NumberThread(4); number4.start();
number5 = new NumberThread(5); number5.start();
} // main()
} // Numbers
When a thread is started by calling its start() method, it automatically calls its run() method. The output generated by this version of the Numbers application is as follows:
From this output, it appears that the individual threads were run in the order in which they were created. In this case, each thread was able to run to completion before the next thread started running.
What if we increase the number of iterations that each thread performs? Will each thread still run to completion? The following output was generated for 200 iterations per thread:
In this case, only thread 1 managed to run to completion. Threads 2, 3, 4, and 5 did not. As this example illustrates, the order and timing of a thread’s execution are highly unpredictable.
This example also serves to illustrate one way of creating a multithreaded program:
Create a subclass of the Thread class.
Within the subclass, implement a method with the signature void run() that contains the statements to be executed by that thread.
Create several instances of the subclass and start each thread by invoking the start() method on each instance.
Principle14.2.6.Thread Creation.
One way to create a thread in Java is to define a subclass of Thread and override the default run() method.