introduce

As the number of cores available in today’s processors increases, and as the need to achieve higher throughput continues to grow, multithreaded apis are becoming very popular. Java provides its own multithreading framework, called the Executor Framework.

1. What is the Executor framework?

The Executor framework contains a set of components for managing worker threads effectively. The Executor API uses Executors to decouple task execution from the actual task to be executed. This is an implementation of the producer-consumer pattern.

Java. Util. Concurrent. Executors provides the factory methods for creating work thread thread pool.

To use the Executor framework, we need to create a thread pool and submit tasks to it for execution. The Executor framework’s job is to schedule and execute the submitted tasks and get the results back from the thread pool.

One basic question that comes to mind is why do we need Thread pools when we create java.lang.Thread objects or invoke program parallelism that implements the Runnable/Callable interface?

The answer lies in two fundamentals:

  1. Creating a new thread for a new task has the additional overhead of thread creation and destruction. Managing the life cycle of these threads significantly increases CPU execution time.
  2. Creating threads for each process without any restrictions results in creating a large number of threads. These threads can take up a lot of memory and cause a waste of resources. When one thread is about to use the CPU’s time slice after another thread has used the CPU’s time slice, the CPU spends a lot of time switching the context of the thread.

All of these factors can lead to reduced system throughput. Thread pools overcome this problem by keeping threads alive and reusing them. When there are more tasks submitted to the thread pool than there are threads executing, those extra tasks are queued. As soon as the executing thread is free, it takes the next task from the queue and executes it. For the JDK providing the executors off the shelf, the task queue is basically limitless.

2. Executors Type

Now that we know what executors are, let’s take a look at the different types of executors.

2.1 SingleThreadExecutor

This thread pool executor has only one thread. It is used to perform tasks in a sequential manner. If this thread hangs due to an exception while executing a task, a new thread is created to replace it, and subsequent tasks are executed in the new thread.

ExecutorService executorService = Executors.newSingleThreadExecutor()
Copy the code

2.2 FixedThreadPool (n)

As the name implies, it is a thread pool with a fixed number of threads. Tasks submitted to executor are executed by a fixed number of n threads, and if there are more tasks, they are stored in LinkedBlockingQueue. This number n is usually related to the total number of threads supported by the underlying processor.

ExecutorService executorService = Executors.newFixedThreadPool(4);
Copy the code

2.3 CachedThreadPool

This thread pool is primarily used in scenarios where a large number of short-term parallel tasks are performed. Unlike a fixed thread pool, this thread pool has an unlimited number of threads. If all threads are busy executing tasks and a new task arrives, the thread pool creates a new thread and submits it to the Executor. As soon as one of the threads becomes idle, it performs a new task. If a thread is idle for 60 seconds, they are terminated and removed from the cache.

However, if it is not managed properly, or if the task is not very short, the thread pool will contain a large number of active threads. This can lead to resource disorder and therefore performance degradation.

ExecutorService executorService = Executors.newCachedThreadPool();
Copy the code

2.4 ScheduledExecutor

This type of executor is used when we have a task that needs to be run periodically or when we want to defer a task.

ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);
Copy the code

You can use scheduleAtFixedRate or scheduleWithFixedDelay to periodically execute tasks in ScheduledExecutor.

scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)
Copy the code

The main difference between the two approaches is their response to delays between successive periodic tasks.

ScheduleAtFixedRate: Tasks are executed at a fixed interval no matter when the previous task ends.

ScheduleWithFixedDelay: The delay countdown starts only after the current task has completed.

3. Understanding of Future objects

Can use executor return Java. Util. Concurrent. The Future object access to task executor of the results. A Future can be thought of as an executor’s response to a caller.

Future<String> result = executorService.submit(callableTask);
Copy the code

As mentioned above, tasks submitted to executor are asynchronous, meaning that the program does not wait for the current task to complete and moves directly to the next step. Instead, executor sets the task in the Future object every time it completes execution.

The caller can continue executing the main program, and when he needs to submit the result of the task, he can call the.get() method on the Future object to get it. If the task completes, the result is immediately returned to the caller, otherwise the caller is blocked until the Executor completes the operation and calculates the result.

If the caller cannot wait indefinitely for the result of the task execution, the wait time can also be set to timed. This can be done with the future.get (Long Timeout, TimeUnit Unit) method, which throws a TimeoutException if no result is returned within the specified time range. The caller can handle this exception and continue executing the program.

If an exception occurs while executing the task, the call to the GET method will throw an ExecutionException.

For the Future. The get () method returns as a result, one of the most important thing is that only submit the task of a Java implementation. Util. Concurrent. Callable interface to return to the Future. If the task implements the Runnable interface, calls to the.get() method will return NULL once the task completes.

Another concern is the future.cancel (Boolean mayInterruptIfRunning) method. This method is used to cancel execution of a committed task. If the task is already executing, executor will attempt to interrupt the task execution if the mayInterruptIfRunning flag is true.

Example: Create and execute a simple Executor

We will now create a task and try to execute it in the Fixed Pool Executor:

public class Task implements Callable<String> {

    private String message;

    public Task(String message) {
        this.message = message;
    }

    @Override
    public String call(a) throws Exception {
        return "Hello " + message + "!"; }}Copy the code

The Task class implements the Callable interface and has a method of type String as a return value. This method can also throw exceptions. This ability to throw an exception to executor and the executor’s ability to return it to the caller is important because it helps the caller know the status of the task execution.

Now let’s perform this task:

public class ExecutorExample {  
    public static void main(String[] args) {

        Task task = new Task("World");

        ExecutorService executorService = Executors.newFixedThreadPool(4);
        Future<String> result = executorService.submit(task);

        try {
            System.out.println(result.get());
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Error occured while executing the submitted task"); e.printStackTrace(); } executorService.shutdown(); }}Copy the code

We created a FixedThreadPool executors with 4 threads because the demo was developed on a quad-core processor. If you are executing a task that performs a large number of I/O operations or spends a long time waiting for external resources, the number of threads may exceed the number of cores of the processor.

We instantiated the Task class and submitted it to Executors. The result is returned by the Future object, and we print it on the screen.

Let’s run ExecutorExample and look at its output:

Hello World!
Copy the code

As expected, the task appends the Hello greeting and returns the result via the Future Object.

Finally, we call shutdown on the executorService object to terminate all threads and return resources to the OS.

The.shutdown() method waits for executor to complete the currently committed task. However, if the requirement is to shutdown the executor immediately without waiting, we can use the.shutdownNow() method.

Any task to be executed returns the result to the java.util.list object.

We can also create the same task by implementing the Runnable interface:

public class Task implements Runnable{

    private String message;

    public Task(String message) {
        this.message = message;
    }

    public void run(a) {
        System.out.println("Hello " + message + "!"); }}Copy the code

There are some important changes when we implement Runnable.

  1. fromrun()Method to obtain the results of task execution. So let’s just print it right here.
  2. run()Method must not throw any checked exceptions.

5. To summarize

Multithreading is becoming more mainstream as processor clock speeds struggle to increase. However, dealing with the life cycle of each thread is difficult because of the complexity involved.

In this article, we present an efficient and simple multithreading Framework, the Executor Framework, and explain its different components. We also looked at different examples of creating commit and execute tasks in executor.

As always, the code for this example is available on GitHub.

Original text: stackabuse.com/concurrency…

By Chandan Singh

Translator: KeepGoingPawn