The use of ExecutorService in java concurrency

Preface

ExecutorService is a framework for asynchronous execution in java. By using ExecutorService you can easily create a multi-threaded execution environment.

This article will explain in detail the specific use of ExecutorService.

Creating an ExecutorService

Generally speaking, there are two ways to create an ExecutorService.

The first way is to use the factory class methods in Executors, for example.

ExecutorService executor = Executors.newFixedThreadPool(10);

In addition to the newFixedThreadPool method, Executors contains a number of methods to create an ExecutorService.

The second method is to create an ExecutorService directly, because ExecutorService is an interface, we need to instantiate an implementation of ExecutorService.

Here we use ThreadPoolExecutor as an example.

ExecutorService executorService =
            new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>());

Allocate Tasks for ExecutorService

ExecutorService can execute Runnable and Callable tasks, where Runnable has no return value and Callable has a return value. Let’s look at the use of each of the two cases.

Runnable runnableTask = () -> {
    try {
        TimeUnit.MILLISECONDS.sleep(300);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Callable<String> callableTask = () -> {
    TimeUnit.MILLISECONDS.sleep(300);
    return "Task's execution";
};

Assigning a task to an ExecutorService can be done by calling the methods execute(), submit(), invokeAny(), invokeAll().

execute() returns void, which is used to submit a Runnable task.

executorService.execute(runnableTask);

submit() returns Future, which can submit a Runnable task, or a Callable task. There are two methods for submitting a Runnable.

<T> Future<T> submit(Runnable task, T result);

Future<? > submit(Runnable task);

The first method returns the result when it is passed in. the second method returns null.

Look again at the use of callable.

Future<String> future = 
  executorService.submit(callableTask);

invokeAny() passes a list of tasks to the executorService and returns one of the successfully returned results.

String result = executorService.invokeAny(callableTasks);

invokeAll() passes a list of tasks to the executorService and returns the results of all successful executions.

List<Future<String>> futures = executorService.invokeAll(callableTasks);

Closing ExecutorService

The ExecutorService will not automatically close if the tasks in the ExecutorService have finished running. It will wait to receive new tasks. If we need to shut down the ExecutorService, we need to call shutdown() or shutdownNow() method.

shutdown() will destroy the ExecutorService immediately, it will make the ExecutorServic stop receiving new tasks and wait until all existing tasks are executed before destroying it.

executorService.shutdown();

shutdownNow() does not guarantee that all tasks have been executed, it returns a list of unexecuted tasks:

List<Runnable> notExecutedTasks = executorService.shutdownNow();

The best shutdown method recommended by oracle is to use it with awaitTermination:

executorService.shutdown();
try {
if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}

Stop receiving tasks first, then wait for a certain amount of time for all tasks to finish executing, and if the given time is exceeded, end the task immediately.

Future

submit() and invokeAll() both return Future objects. We’ve talked about Future in detail in previous articles. Here’s just a list of how to use.

Future<String> future = executorService.submit(callableTask);
String result = null;
try {
result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}

ScheduledExecutorService

ScheduledExecutorService provides us with a mechanism to execute tasks at regular intervals.

We create the ScheduledExecutorService like this.

ScheduledExecutorService executorService
                = Executors.newSingleThreadScheduledExecutor();

executorService’s schedule method, which can be passed to either Runnable or Callable: `` `

Future<String> future = executorService.schedule(() -> {
        // ...
        return "Hello world";
    }, 1, TimeUnit.SECONDS);

    ScheduledFuture<? > scheduledFuture = executorService.schedule(() -> {
        // ...
    }, 1, TimeUnit.SECONDS);

There are two other more similar methods.

scheduleAtFixedRate( Runnable command, long initialDelay, long period, TimeUnit unit )

scheduleWithFixedDelay( Runnable command, long initialDelay, long delay, TimeUnit unit ) 

The difference between the two is that the former’s period is calculated with the task start time and the latter is calculated with the task end time.

ExecutorService and Fork/Join

java 7 introduces the Fork/Join framework. So what is the difference between the two?

ExecutorService allows the user to control the generated threads themselves, providing more granular control over threads. Fork/Join, on the other hand, is designed to allow tasks to be executed more quickly.