Multi-threading is a method of writing code for executing tasks in parallel. Java has had excellent support for writing multi-threaded code since the early days of Java 1.0. Recent enhancements to Java have increased the ways in which code can be structured to incorporate multi-threading in Java programs.

In this article, we compare a few of these options so you can better judge which option to use for your next Java project.

Multiple worker threads within a process.

Method 1: Extending the Thread class

Java provides a Thread class which can be extended to implement the run() method. This run() method is where you implement your task. When you want to kick off the task in its own thread, you can create an instance of this class and invoke its start() method. This starts the thread execution and runs to completion (or terminates in an exception).

Extending Thread allows a worker task to run in a separate thread

Here is a simple Thread class which just sleeps for a specified interval as a way of simulating a long-running operation.

        public class MyThread extends Thread
{
  private int sleepFor;

  public MyThread(int sleepFor) {
    this.sleepFor = sleepFor;
  }

  @Override
  public void run() {
    System.out.printf("[%s] thread starting\n",
    Thread.currentThread().toString());
    try { Thread.sleep(this.sleepFor); }
    catch(InterruptedException ex) {}
    System.out.printf("[%s] thread ending\n",
    Thread.currentThread().toString());
  }
}

Create an instance of this Thread class by giving it the number of milliseconds to sleep.

        MyThread worker = new MyThread(sleepFor);

Kick off the execution of this worker thread by invoking its start() method. This method returns control immediately to the caller, without waiting for the thread to terminate.

        worker.start();
System.out.printf("[%s] main thread\n", Thread.currentThread().toString());

And here is the output from running this code. It indicates that the main thread diagnostic is printed before the worker thread executes.

        [Thread[main,5,main]] main thread
[Thread[Thread-0,5,main]] thread starting
[Thread[Thread-0,5,main]] thread ending

Since there are no more statements after starting the worker thread, the main thread waits for the worker thread to finish before the program exits. This allows the worker thread to complete its task.

Method 2: Using a Thread Instance With a Runnable

Java also provides an interface called Runnable which can be implemented by a worker class to execute the task in its run() method. This is an alternative way of creating a worker class as opposed to extending the Thread class (described above).

Class Papaya extends Fruit but implements Runnable to be able to run a task in a separate thread.

Here is the implementation of the worker class which now implements Runnable instead of extending Thread.

        public class MyThread2 implements Runnable {
  // same as above
}

The advantage of implementing the Runnable interface instead of extending the Thread class is that the worker class can now extend a domain-specific class within a class hierarchy.

What does this mean?

Let us say, for example, you have a Fruit class which implements certain generic characteristics of fruits. Now you want to implement a Papaya class which specializes certain fruit characteristics. You can do that by having the Papaya class extend the Fruit class.

        public class Fruit {
  // fruit specifics here
}

public class Papaya extends Fruit {
  // override behavior specific to papaya here
}

Now suppose you have some time-consuming task that Papaya needs to support, which can be performed in a separate thread. This case can be handled by having the Papaya class implement Runnable and provide the run() method where this task is performed.

        public class Papaya extends Fruit implements Runnable {
  // override behavior specific to papaya here

  @Override
  public void run() {
    // time consuming task here.
  }
}

To kick off the worker thread, you create an instance of the worker class and hand it over to a Thread instance at creation. When the start() method of the Thread is invoked, the task executes in a separate thread.

        Papaya papaya = new Papaya();
// set properties and invoke papaya methods here.
Thread thread = new Thread(papaya);
thread.start();

And that is a brief summary of how to use a Runnable to implement a task executing within a thread.

Method 3: Execute a Runnable With ExecutorService

An ExecutorService provides an abstraction for creating and managing threads.

Starting with version 1.5, Java provides an ExecutorService as a new paradigm for creating and managing threads within a program. It generalizes the concept of thread execution by abstracting away creation of threads.

This is because you can run your tasks within a pool of threads just as easily as using a separate thread for each task. This allows your program to track and manage how many threads are being used for worker tasks.

Suppose you have a 100 worker tasks waiting to be executed. If you start one thread per worker (as presented above), you would have 100 threads within your program which might lead to bottlenecks elsewhere within the program. Instead, if you use a thread pool with, say 10 threads pre-allocated, your 100 tasks will be executed by these threads one after another so your program is not starved for resources. In addition, these thread pool threads can be configured so that they hang around to perform additional tasks for you.

An ExecutorService accepts a Runnable task (explained above) and runs the task at a suitable time. The submit() method, which accepts the Runnable task, returns an instance of a class called Future, which allows the caller to track the status of the task. In particular, the get() method allows the caller to wait for the task to complete (and provides the return code, if any).

In the example below, we create an ExecutorService using the static method newSingleThreadExecutor(), which as the name indicates, creates a single thread for executing tasks. If more tasks are submitted while one task is running, the ExecutorService queues up these tasks for subsequent execution.

The Runnable implementation we use here is the same one described above.

        ExecutorService esvc = Executors.newSingleThreadExecutor();
Runnable worker = new MyThread2(sleepFor);
Future<?> future = esvc.submit(worker);
System.out.printf("[%s] main thread\n", Thread.currentThread().toString());
future.get();
esvc.shutdown();

Note that an ExecutorService must be properly shut down when it is no longer needed for further task submissions.

Method 4: A Callable Used With ExecutorService

Starting with version 1.5, Java introduced a new interface called Callable. It is similar to the older Runnable interface with the difference that the execution method (called call() instead of run()) can return a value. In addition, it can also declare that an Exception can be thrown.

An ExecutorService can also accept tasks implemented as Callable and returns a Future with the value returned by the method at completion.

Here is an example Mango class which extends the Fruit class defined earlier and implements the Callable interface. An expensive and time-consuming task is performed within the call() method.

An implementation of the Callable interface can also be used with an ExecutorService
        public class Mango extends Fruit implements Callable {
  public Integer call() {
    // expensive computation here
    return new Integer(0);
  }
}

And here is the code for submitting an instance of the class to an ExecutorService. The code below also waits for the task to complete and prints its return value.

        ExecutorService esvc = Executors.newSingleThreadExecutor();

MyCallable worker = new MyCallable(sleepFor);
Future future = esvc.submit(worker);
System.out.printf("[%s] main thread\n", Thread.currentThread().toString());
System.out.println("Task returned: " + future.get());
esvc.shutdown();

What Do You Prefer?

In this article, we learned a few methods to write multi-threaded code in Java. These include:

  1. Extending the Thread class is the most basic and has been available from Java 1.0.
  2. If you have a class which must extend some other class in a class hierarchy, then you can implement the Runnable interface.
  3. A more modern facility for creating threads is the ExecutorService which can accept a Runnable instance as a task to run. The advantage of this method is that you can use a thread pool for task execution. A thread pool helps in resource conservation by reusing threads.
  4. Lastly, you can also create a task by implementing the Callable interface and submitting the task to an ExecutorService.

Which of these options do you think you will use in your next project? Let us know in the comments below.