Skip to main content
Logo image

Java, Java, Java: Object-Oriented Problem Solving, 2022E

Section 14.2 What Is a Thread?

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.

Subsection 14.2.1 Concurrent 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.
Figure 14.2.3. Each thread gets a slice of the CPU’s time.
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.

Subsection 14.2.2 Multithreaded 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.
Figure 14.2.4. The NumberThread class.
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).
Figure 14.2.5. The Numbers object creates several instances of NumberThread and tells each one to start().
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:
11111111112222222222333333333344444444445555555555
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:
111111111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111112222222
222222222222222222222222222222222222222222222222222222222222222222222
222222222222222222222222222222222222222222222222222222222222222222222
222222222222222222222222222222222222222222223333333333333333333333333
333333333333333333333333333333333333333333333333333333333333333333333
333333333333333333333333333444444444444444444444444444444444444444444
444444444444444444444444444444444444444444444444444444444444444444444
444444444455555555555555555555555555555555555555555555555555555555555
555555555555555555555555555555555555555555555555555555555555552222222
222233333333333333333333333333333333333333333333333333333333333333333
333333333333334444444444444444444444444444445555555555555555555555555
555555555555555555555555555555555555555555555555555555444444444444444
4444444444444444444444444444444444
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.
You have attempted of activities on this page.