Java Lesson 14: Threads

1
44

Introduction

Compared to other programming languages, Java provides a rich set of multithreading capabilities. In this lesson we will learn the basics of executing several compute streams concurrently.

Historical Perspective

By today’s standards, the earliest computers were very slow and only capable of performing one operation at a time. As computers became faster time-sharing systems became popular, where several users shared one computer at the same time. When a user paused to think or type an instruction, the computer could work for another user. Time-sharing computers were fast enough that each user had the illusion they were the only person using the computer.

As the price of computer chips decreased, manufacturers found ways to design computers with more than one processor inside the box. Modern computers, even personal computers, usually have two or more processors – often a processor for general calculations, another for video graphics processing, and possibly another for input/output (I/O) operations.

When you use your computer, tablet, or cell-phone, it appears as if your current application is the only one running. In reality there are often many processes or threads running simultaneously. For example, while entering numbers into the cells of a spreadsheet you appear to have the computer’s full attention. Meanwhile, the computer uses tiny slices of computer time to:

  • Compute a formula, such as the sum of a column in the spreadsheet.
  • Save a backup copy of the spreadsheet on your hard disk or on the network.
  • Run an anti-virus program in the background.
  • Update the time in the system tray.
  • Look for new email messages.

Large computationally intensive programs, such as those that do a lot of mathematical operations, are divided into several smaller chunks. Each chunk, or thread, can execute simultaneously, or concurrently, with other threads, thus reducing the total execution time. Examples of complex calculations are weather forecasting, stock market simulations, and genetic research. Consider a program which takes one hour to run. By dividing into four smaller threads that run concurrently the total execution time becomes 15 minutes.

The Java language includes keywords and system libraries which make developing multi-threaded programs relatively easy.

Disadvantages of Multi-Threading

There are two disadvantages of multi-threading:

  1. Multi-threaded programs are inherently more complex, and thus are more difficult to write and understand than single-threaded programs.
  2. Multi-threading adds extra overhead to the system. Some programs may only see a marginal increase in overall performance due to extra work that the computer and operating system must to do manage a multi-threaded program.

With those caveats in mind, let’s examine how Java supports multi-threading.

Java Multi-Threading

Flow diagram of thread execution.
Flow diagram of thread execution.

A thread is a separate process that can execute concurrently with other processes.

Java’s multi-threading classes provide several methods for multi-threaded programs, including:

There are three common ways to make programs multi-threaded:

  1. Create a class which extends the Thread class.
  2. Implement Runnable and pass it to the constructor of the Thread class.
  3. Implement Runnable in an Anonymous Inner Class.

We will see an example of each way in this tutorial.

Extend the Thread Class

Program14a.java is a small program which defines a class called MyThread which extends Java’s Thread class. The Thread class has a run() method which we will override (i.e., replace) with our custom code. Our code contains a for loop that counts from one to twenty and displays a message on the console each time through the loop. In the main() method we declare two new instances of MyThread named t1 and t2 respectively. Finally, we start running both threads with the start() method. The start() method automatically calls the run() method in the MyThread class.

Note: We call start() instead of run() because run() will execute within the thread of the main() method instead of in a separate thread. Always put your custom code in run(), call start(), and let Java call run() from start().

Examine the output of Program14a closely. Thread t1 starts first, and outputs the first three lines:

By this time, the second thread t2 has started, and it outputs the line:

The first thread t1 then gets an opportunity to run, and it outputs the line:

Execution of both threads continue until all output displays. Notice the interleaved output from both threads. If you run the program many times, you will discover the interleaving is essentially random – there is no guaranteed order to the output because both threads (and possibly other system activities) are running asynchronously.

Execution of Program14a.java, extend the Thread class.

Implement Runnable

Program14b.java is another small program which creates a class called MyThread which implements Java’s Runnable class and passes it to the constructor of the Thread class. Once again, we override the Thread class’s run() method with our custom code. Our code is the same for loop from the previous example that counts from one to twenty and displays a message on the console each time through the loop. In the main() method we declare two new instances named t1 and t2. Finally, we start running both threads with the start() method. The start() method automatically calls the run() method in our MyThread class.

Examine the output of Program14b closely. Thread t1 starts first, and outputs the first line:

By this time, the second thread t2 has started, and it outputs the line:

As in the previous example, execution of both threads continues until all output displays. Notice how the interleaved output from both threads. If you run the program more than once you will discover the interleaving is essentially random – there is no guaranteed order to the output because both threads (and possibly other system activities) are running asynchronously.

Execution of Program14b.java, implements Runnable.

Implement Runnable as an Anonymous Inner Class

A variation on the second way is to use something known as an Anonymous Inner Class. Instead of creating a new class and giving it a name, we put the code in the main() method inside of the call to the constructor. This inner class has no name, and thus is referred to as anonymous.

For simplicity in Program14c.java we only create one thread, although you can create as many anonymous inner classes as you like. Although we only create one thread, it is in theory running in parallel with the main program thread.

Using Thread.Sleep() for Multi-Threading Examples

A common practice when developing multi-threading examples is to call the Thread.sleep(duration) method, which causes the thread to pause for duration milliseconds. There are 1000 milliseconds in one second, so the statement

causes the thread to pause for one second. The purpose of Thread.sleep() in multi-threading examples is to simulate the computer doing many computations that take a long time to run, without actually wasting those cpu cycles.

Thread.sleep() can throw an InterruptedException, so we enclose it in a try-catch block.

Since Program14c only has one thread it only has one set of Running… output. However, when running Program14c you will notice there is a half-second pause between each line of output because the thread sleeps for 500 milliseconds on each pass through the for loop.

Compile and execution of Program14c, with anonomous inner class.

Measuring Thread Execution Time

The Java library provides a convenient method called System.currentTimeMillis() which returns a long integer containing the current date and time accurate to the current millisecond. By calling this method at two places in a program (e.g., when the program starts and when the program finishes), we can subtract the start time from the end time to determine the duration (i.e., the time required for the program to execute).

Sequential Execution Example

Program14Seq.java is a program which takes a long time to run. It fills two large arrays, array1 and array2, with the sine and cosine of 10 million numbers. Each array has 10 million elements. System.currentTimeMillis() is called at the beginning of the program and again at the end of the program, and then the run time (in milliseconds) is calculated and displayed.

Compile and execution of Program14Seq.java, which fills two large arrays sequentially and takes 9,850 milliseconds to run.

The program takes 9,850 milliseconds, or 9.85 seconds, to do the sine and cosine mathematical operations and fill the two large arrays. This program is clearly cpu-intensive and may benefit from multi-threading.

Note: Because each computer is different the elapsed time you see will be different.

Multi-Threaded Execution Example

Like the previous program, Program14Thr.java also fills two large arrays, array1 and array2, with the sine and cosine of 10 million numbers, but it is written to use multi-threading. The first thread (thread1) fills array1 with the sine of all the numbers from 1 to 10 million, and the second thread (thread2) fills array2 with the cosine of the numbers. After starting the two threads, the program calls thread1.join() and thread2.join() so the program waits until both threads have finished executing before continuing on with the main program. Since both threads run concurrently we expect the total run time will be something less than the 9,850 milliseconds that Program14Seq required.

Compile and execution of Program14Thr.java, which uses two threads to fill two large arrays and takes 6,172 milliseconds to run.

This improved program only takes 6,172 milliseconds, or 6.17 seconds, to do the same sine and cosine mathematical operations and fill the two large arrays as the non-threaded program.

Note: Because each computer is different the elapsed time you see will be different. You will also see a different elapsed time each time you run the program due to other activities your computer is performing.

Using Windows Task Manager to Monitor System Performance

The Windows Task Manager is used to view system performance on Microsoft Windows computers. Other operating systems have similar utilities. An interesting exercise is to monitor the CPU usage while running a program.

The screenshot below shows the CPU usage while the non-threaded Program14Seq program filled both arrays sequentially. The computer has four cpu processors, but only one was significantly busy.

tutorial14_cpu_nonthreaded2

The next screenshot shows the CPU usage when the threaded Program14Thr program filled both arrays concurrently. This time two cpu processors were significantly busy, thus indicating that rewriting the program to use multi-threading achieved the desired goals of making more efficient use of the available cpu resources and reducing the run time.

tutorial14_cpu_threaded2

Summary

This tutorial lesson showed three basic ways to implement multi-threading in Java programs. If you have programs which are cpu-intensive or need to do activities simultaneously (e.g., reading and writing files), then you may want to consider two or more threads and the other multi-threading methods that Java offers.

Next Lesson

Next we will move on to Java Applets.

1 COMMENT

Leave a Reply