When an operating system starts a program, it creates a new process; a process is essentially a program that is currently running. Every process is given one Thread (it can have more). Think of a thread as a linear path of execution through a program.

Each Thread has its local variables, program counters (a program counter is a pointer to the instruction currently running), and lifetime. You can run more than one Thread in a process (they run concurrently). When you execute a Java program, the OS runs it in the context of a process; and within this process, you can spawn many threads.

Multithreading in Java

Threading is deeply ingrained in Java, that it’s almost impossible to write a program without using it. Even the unassuming “Hello World” program uses threads, and many classes in the Java API are already threaded, so you may have used threads already without knowing it.

Let’s revisit the simple Hello World program (shown below).

import static java.lang.System.out;

public class Hello {
    public static void main(String[] args) {
        out.println("Hello World");
    }
}

We can think of this program as follows:

  1. When we run it (by executing Java Hello, or by clicking the Run button from your favorite IDE), the OS creates a process and gives our “Hello World” program a single thread of execution.
  2. That Thread of execution finds the program’s entry point — static main() — and proceeds through a program path. It executes one statement after another.
  3. When the main() method runs out of statements, the program stops.

You might think of it as a single thread. The JVM spawned the main() Thread, our short program gets one Thread, and in it, runs all the statements inside main(). When main() runs out of statements, the program dies.

Unless you’re entirely new to Java, you must have heard of the Garbage Collector (GC). The GC is like a big brother to your program; it cleans up after it. When there are objects that are no longer referenced, it sweeps them up and frees the memory. This GC is running in the background; it cleans up discarded objects and reclaims memory. So, you see, even the unassuming “Hello World” example has at least two threads; the main() Thread and a background thread where the GC runs.

Why do we need to learn about threads

The importance of thread programming in Java significantly increased when graphical interfaces became more and more common on desktop and mobile platforms. The use of threads made it possible for the programs to appear faster and more responsive.

Imagine a Graphical User Interface (GUI) program; there are plenty of problems to solve in writing one. There should be a set of codes to draw the UI elements, another set of instructions to wait for user input (and react to it), and maybe even play an audio track. Without the facility of threading, it will be next to impossible to do.

Here are some more reasons why we need to learn threads:

  • Better interaction with the user: If there aren’t threads, can you imagine how word processors would work? You can’t have spell-check, auto-formatter, and auto-correction run while typing the doc. From the CPU perspective, even the fastest touch-typist takes a tremendous amount of time between keystrokes. These large gaps of time can be used to run other tasks like auto-correction, auto-formatting, and spell-checking. To the user, these may all seem to be happening in real-time.
  • Take advantage of multiple processors: During the early days of Java (when single-processors were the norm), Java programmers can make their programs appear to be doing tasks concurrently by running instructions (across multiple threads) for a short-time then switch threads to give the appearance of simultaneous execution. In the age of i9s and Ryzen processors, Java threads can now exploit multiple processors’ availability and achieve true simultaneous thread execution. Old Java programs written for a single-processor don’t need to be re-written; they’ll run just fine.
  • Takes advantage of blocking operations: I/O operations are much slower compared to the speed of execution within the CPU, and because of this, the processor can’t do anything while waiting for I/O operations to complete. If you’ve worked with Java I/O before, you may recall that the read() method, when run, has to wait until a byte has been read from a stream (or until an exception is thrown). The Thread that executes the read() method can’t do anything else until some data comes back or until an Exception stops it. If there are other instructions after the read() method, they can’t run until the method finishes. Can you imagine if you’re reading a large amount of data? Your program might be unresponsive for a good while until the I/O operation completes. This is the reason why I/O calls are usually run in a background thread.

Let’s look at some examples that can benefit from threading.

The sample code below may look contrived, but it’s an example of an expensive call (as far as CPU and memory resources go). The code hogs the CPU for calculating Cartesian Products — a Cartesian Product is a mathematical set that results from multiplying other sets. If you run this in the main Thread, say, something that’s also responsible for UI, the UI might feel sluggish to the user. If you can’t avoid nested loops or Cartesian calculations, try not to run it in the main Thread; you can run these types of tasks in a background thread.

private void nestedCall() {
    for (int i = 1; i < 10000; i++) {
        for (int j = 1; j < 10000; j++) {
            for (int k = 1; k < 10000; k++) {
                System.out.printf("%d", i * j * k);
            }
        }
    }
}

Another example (shown below) is a code that reads data from GitHub using an Http library. This is an example of an Android program.

private String fetch(String gitHubId) {

    String userInfo = "";
    String url = String.format("https://api.github.com/users/%s", gitHubId);

    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder()
        .url(url)
        .build();

    try (Response response = client.newCall(request).execute()) {
        userInfo = response.body().string();
    } catch (IOException ioe) {
        Log.e(TAG, ioe.getMessage());
    }
    return userInfo;
}

If you were to call this method directly inside an Activity class, the Android runtime would complain, throwing a NetworkOnMainThreadException because any code that reads from a network connection cannot run on the main thread or the UI thread.

Basic Thread creation

We can run statements in the background by spawning new threads. Thread objects are created either by extending the Thread class or by implementing the Runnable interface. A thread object defines a single path of execution, which happens inside the Thread’s run() method.

Going back to the Hello World example from earlier, let’s revise to make it multi-threaded.

class HelloWorker extends Thread {
    @Override
    public void run() {
        System.out.println("Hello");
    }
}

The HelloWorker class extends the java.lang.Thread class. The run() method of the Thread class needs to be overridden. Then you put everything you want to run in the background inside it the run() method. After that, we instantiate the Thread class and run it.

public class Hello {
    public static void main(String[] args) {
        HelloWorker worker = new HelloWorker(); //Create an instance of thread class
        worker.start(); //Invoke the start() method. If you forget this, the thread won’t run. Calling the start() method kickstarts the thread
        System.out.println("World");
    }
}

Another way to create background Threads is via the Runnable interface, like this;

class HelloWorker implements Runnable {
    @Override
    public void run() {
        out.println("Hello");
    }
}

Instead of extending the Thread class, we implement the Runnable interface. The rest of the code is wholly the same from our previous example; you still override the run() method and put the background task inside it.

To use the Runnable object, we instantiate it. Next, we create a Thread object by passing the instance of the Runnable object to the Thread’s constructor; then, we start the Thread. In code, it looks like the following sample.

HelloWorker worker = new HelloWorker();
Thread thread = new Thread(worker);
thread.start();
System.out.println("World");

The life cycle of a Thread

Threads, like objects, have life cycles. The first phase of a thread’s life is when it’s created. From our previous examples, we created a thread using the simplest of its constructors, the no-arg constructor; but you can create a thread using its other constructors. Here’s an excerpted snippet from the Thread class API.

package java.lang;
public class Thread implements Runnable {
    public Thread();
    public Thread(Runnable target);
    public Thread(ThreadGroup group, Runnable target);
    public Thread(String name);
    public Thread(ThreadGroup group, String name);
    public Thread(Runnable target, String name);
    public Thread(ThreadGroup group, Runnable target, String name);
    public Thread(ThreadGroup group, Runnable target, String name,
        long stackSize);
    public void start();
    public void run();
}

You can create a thread object using a variety of ways. You can call the no-arg constructor (which we’ve done), you can also pass a Runnable object to its constructor (which we’ve also done). Other ways to create a thread class requires that you have knowledge of ThreadGroup and stack size; both of which are outside the scope of this article.

The next phase in the Thread’s life is when it’s started. Merely creating the Thread object doesn’t automatically start it. At the point of creation, the Thread is in a wait state. When a thread is in a wait state, other threads may interact with it; also, while in the wait state, you can change a thread’s name (by calling the setName() method), priority, daemon status, and so on. Calling the start() method of the Thread kicks it into high gear.

The last phase of a Thread’s life is termination. When you start a Thread, it executes the run() method. It will only terminate when the run() method completes. The run() method may very well be complicated, or it may even run forever. It’s your responsibility as a programmer to make sure that at some point, the Thread stops.

If you look at the API of the Thread class, you may notice the method suspend(), resume() and stop(); these may look like good ways to manage the lifecycle of the Thread, but you need to be warned that the stop method was found problematic, that’s why it was deprecated. The suspend() and resume() methods, too, were deprecated.

Pausing and stopping a Thread

If stop(), suspend(), and resume() were deprecated on the Thread class, what are we supposed to use then? Well, to pause a thread, at least for a specific amount of time, we can use the sleep() method; it’s a static method of the Thread class, and it is used like this.

public static void main(String[] args) throws Exception {
    HelloWorker worker = new HelloWorker();
    Thread thread = new Thread(worker);
    thread.start();
    Thread.sleep(1000);
    out.println("World");
}

The Thread responsible for the main() method will be suspended for 1000 milliseconds when it encounters the Thread.sleep() method; and will resume immediately right after.

To stop a thread, you can use internal flags inside the Thread’s run() method. Sample code on how to do this is shown below.

class Forever implements Runnable {
    private boolean isDone = false;

    public void stop() {
        isDone = true;
    }
    @Override
    public void run() {
        while (!isDone) {
            // statements
        }
    }
}

 

Last modified: September 10, 2020

Author

Comments

Write a Reply or Comment

Your email address will not be published.