Site icon yCrash

Introduction to ExecutorService in Java

Working solely with Thread instances can be challenging. Threads must be correctly started, stopped and disposed of. We often need to synchronize threads, schedule tasks, create thread pools and devise a means of communication between threads. All this requires complex and error-prone coding.

Luckily, Java introduced the Concurrency API, which allows us to use threads without directly using the Thread class. This API, among other classes, contains the ExecutorService interface, which creates and manages threads for us. Moreover, it automatically provides features such as thread pooling and scheduling, simplifying the concurrent execution of tasks.

In this tutorial, we’ll learn about the ExecutorService class. First, we’ll explore ways to instantiate it. Then we’ll see how to submit tasks for execution. Finally, we’ll examine how to shut down the ExecutorService once we no longer need it.

Creating ExecutorService

Based on the number of threads, we distinguish single-threaded and pool-threaded executors. Let’s discuss how to create each.

Single-Threaded Executor

As the name suggests, a single-threaded executor represents an executor that uses a single thread. We should use the ExecutorService every time we execute the task in a separate thread, even if we need a single thread.

One way to instantiate such an executor is by using factory methods provided in the Executor class:

When working with a single-threaded executor, the tasks will be executed in the order in which we added them to the executor service.

It’s also important to note that the main() method executes in a different thread that has nothing to do with the ExecutorService. In other words, it can perform tasks while other threads (managed by the ExecutorService) are running.

Let’s create an example of a single-threaded executor:

ExecutorService service = Executors.newSingleThreadExecutor();

If we have multiple tasks we want to execute, other tasks will be postponed until the currently running task is completed.

Pool-Threaded Executor

As previously mentioned, if a single-threaded executor needs to execute multiple tasks, it’ll wait for a thread to become available before running the next task. On the contrary, we can perform various tasks concurrently using a pool-threaded executor

To create an instance of the pool-threaded executor, we can use several factory methods provided in the Executor class:

Let’s create a pool-threaded executor:

ExecutorService service = Executors.newFixedThreadPool(5);

Submitting Tasks

Now let’s explore the methods to submit tasks for execution.

The simplest way to submit a task is by using the execute() method:

public class PoolThreadedExecutor {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(5);
        service.execute(() -> System.out.println("Task 1"));
        service.execute(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("Task 2: " + i);
            }
        });
        service.execute(() -> System.out.println("Task 3"));
    }
}

Here, we created a pool of five threads. Furthermore, we defined three tasks that will be executed concurrently since the number of tasks is less than the number of available threads. This method works on the fire-and-forget principle, which doesn’t return a value.

That’s to say, once submitted, the results aren’t available to the calling thread.

However, if we need an object to determine whether the task is complete, we can use the submit() method instead:

public static void main(String[] args) {
    try {

        Callable<String> task = () -> "Avocado";

        ExecutorService service = Executors.newFixedThreadPool(5);
        Future<String> result = service.submit(task);

        while (true) {
            if (result.isDone()) {
                System.out.println(result.get());
                break;
            }
        }

    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

The submit() method accepts either Runnable or Callable and returns the Future. Then, we can use the result to determine whether the task is complete.

Let’s explore the other methods we can use when submitting tasks:

Shutting Down a Thread Executor

Lastly, let’s show the importance of shutting down the executor service. Resources such as executors should be closed to prevent memory leaks.

Let’s consider the following example:

ExecutorService service = Executors.newFixedThreadPool(500);
for (int i = 0; i < 500; i++) {
    service.execute(() -> System.out.println("Doing some work"));
}

// Simulating application running
Thread.sleep(1000000);

Here, we created 500 threads to execute some tasks. Then, we called the Thread.sleep() method to simulate the application running.

Threads we created may remain alive, preventing GC, which can lead to memory leaks.

Let’s take a thread dump and upload it to the fastThread tool,which will give us an in-depth analysis of the dump as well as making useful recommendations . The report shows the warning that we’re not using 90% of our threads:

Fig: Thread Pools showing 90% of unused threads

The entire report is available here.

We can fix the problem by calling the shutdown() method:

ExecutorService service = null;
try {
    service = Executors.newFixedThreadPool(500);
    for (int i = 0; i < 500; i++) {
        service.execute(() -> System.out.println("Doing some work"));
    }
} finally {
    if (service != null) {
        service.shutdown();
    }
}

// Simulating application running
Thread.sleep(1000000);

The shutdown() method first rejects any new task submitted for execution while continuing to execute the ongoing task.

In addition, uploading the thread dump of modified code to the fastThread tool no longer shows a potential problem. We can check the entire report here

Alternatively, we can use the shutdownNow() method to try to stop all running tasks and those that have yet to be started.

Conclusion

This article taught us how to work with single-threaded and pool-threaded executor services.

To summarize, we saw how to instantiate the ExecutorService, run tasks, and properly close it once we no longer need it. Lastly, we learned how to diagnose potential memory leaks using the fastThread tool.

Exit mobile version