-
Notifications
You must be signed in to change notification settings - Fork 14
Java Concurrency Basics
In previous tutorial, we have learned the low-level APIs that have been part of the Java platform from the very beginning. These APIs are adequate for very basic tasks, but higher-level building blocks are needed for more advanced tasks. This is especially true for massively concurrent applications that fully exploit today's multiprocessor and multi-core systems.
In this tutorial we'll look at some of the high-level concurrency features introduced with version 5.0 of the Java platform. Most of these features are implemented in the new java.util.concurrent packages. There are also new concurrent data structures in the Java Collections Framework.
Since Java 5, the Java concurrency API provides a mechanism that aims at resolving problems. This mechanism is called the Executor framework and is around the Executor interface, its subinterface ExecutorService,ScheduledExecutorService and the ThreadPoolExecutor class that implements both interfaces.
This mechanism separates the task creation and its execution. With an executor, you only have to implement the Runnable objects and send them to the executor. It is responsible for their execution, instantiation, and running with necessary threads. But it goes beyond that and improves performance using a pool of threads. When you send a task to the executor, it tries to use a pooled thread for the execution of this task, to avoid continuous spawning of threads.
Another important advantage of the Executor framework is the Callable interface. It's similar to the Runnable interface, but offers two improvements, which are as follows:
- The main method of this interface, named call(), may return a result.
- When you send a Callable object to an executor, you get an object that implements the Future interface. You can use this object to control the status and the result of the Callable object.
Let's understanthe d Executor framework in depth with all the interfaces and it's implementations.
The java.util.concurrent package defines three executor interfaces:
- Executor, a simple interface that supports launching new tasks.
- ExecutorService, a subinterface of Executor, which adds features that help manage the lifecycle, both of the individual tasks and of the executor itself.
- ScheduledExecutorService, a subinterface of ExecutorService, supports future and/or periodic execution of tasks.
An object that executes submitted Runnable tasks. This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. An Executor is normally used instead of explicitly creating threads. For example, rather than invoking new Thread(new(RunnableTask())).start() for each of a set of tasks, you might use:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...
We need to create an invoker to create the executor instance:
public class Invoker implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
Now, we can use this invoker to execute the task.
public void execute() {
Executor executor = new Invoker();
executor.execute( () -> {
// task to be performed
});
}
Point to note here is that if the executor can’t accept the task for execution, it will throw RejectedExecutionException.
The executor implementations in java.util.concurrent are designed to make full use of the more advanced ExecutorService and ScheduledExecutorService interfaces, although they also work with the base Executor interface.
The ExecutorService interface supplements execute with a similar, but more versatile submit method. Like execute, submit accepts Runnable objects, but also accepts Callable objects, which allow the task to return a value. The submit method returns a Future object, which is used to retrieve the Callable return value and to manage the status of both Callable and Runnable tasks.
ExecutorService also provides methods for submitting large collections of Callable objects. Finally, ExecutorService provides a number of methods for managing the shutdown of the executor. To support an immediate shutdown, tasks should handle interrupts correctly.
/**
* ExecutorService interface Example
* @author javaguides.net
*
*/
public class ExecutorServiceExample {
public static void main(String[] args) {
System.out.println("Thread main started");
// Create a task
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] " + "Message " + i);
}
};
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(task);
executorService.shutdown();
System.out.println("Thread main finished");
}
}
Output:
Thread main started
Thread main finished
[pool-1-thread-1] Message 0
[pool-1-thread-1] Message 1
[pool-1-thread-1] Message 2
[pool-1-thread-1] Message 3
[pool-1-thread-1] Message 4
The above example shows how to create an executor service and execute a task inside the executor. We use the Executors.newSingleThreadExecutor() method to create an ExecutorService that uses a single worker thread for executing tasks.
executorService.execute(task);
let's understand the different between execute and submit methods:
-
The main difference is submit() method returns Future object for tracking the results but execute() method does't return anthing.
-
Both submit() and execute() methods are used to submit a task to Executor framework for asynchronous execution.
-
The submit() can accept both Runnable and Callable task but execute() can only accept the Runnable task.
-
You can access submit() and execute() from the ExecutorService interface because it also extends the Executor interface which declares the execute() method.
If you run the above program, you will notice that the program never exits, because, the executor service keeps listening for new tasks until we shut it down explicitly.
ExecutorService provides two methods for shutting down an executor -
-
shutdown() - when shutdown() method is called on an executor service, it stops accepting new tasks, waits for previously submitted tasks to execute, and then terminates the executor.
-
shutdownNow() - this method interrupts the running task and shuts down the executor immediately.
We should add shutdown code at the end of our program so that it exits gracefully -
executorService.shutdown();
Let's use Executors.newFixedThreadPool(int nThreads) method creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue.
In the earlier example, we created an ExecutorService that uses a single worker thread. But the real power of ExecutorService comes when we create a pool of threads and execute multiple tasks concurrently in the thread pool.
Following example shows how you can create an executor service that uses a thread pool and execute multiple tasks concurrently -
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ExecutorService interface Example
*
* @author javaguides.net
*
*/
public class ExecutorServiceExample {
public static void main(String[] args) {
System.out.println("Thread main started");
// Create a task
Runnable task1 = () -> {
for (int i = 0; i < 5; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] " + "Message " + i);
}
};
// Create a task
Runnable task2 = () -> {
for (int i = 0; i < 5; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] " + "Message " + i);
}
};
// Create a task
Runnable task3 = () -> {
for (int i = 0; i < 5; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] " + "Message " + i);
}
};
// Create a task
Runnable task4 = () -> {
for (int i = 0; i < 5; i++) {
System.out.println("[" + Thread.currentThread().getName() + "] " + "Message " + i);
}
};
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(task1);
executorService.execute(task2);
executorService.execute(task3);
executorService.execute(task4);
System.out.println("Thread main finished");
executorService.shutdown();
}
}
Output:
Thread main started
Thread main finished
[pool-1-thread-1] Message 0
[pool-1-thread-1] Message 1
[pool-1-thread-1] Message 2
[pool-1-thread-1] Message 3
[pool-1-thread-1] Message 4
[pool-1-thread-1] Message 0
[pool-1-thread-1] Message 1
[pool-1-thread-1] Message 2
[pool-1-thread-1] Message 3
[pool-1-thread-1] Message 4
[pool-1-thread-1] Message 0
[pool-1-thread-1] Message 1
[pool-1-thread-1] Message 2
[pool-1-thread-1] Message 3
[pool-1-thread-1] Message 4
[pool-1-thread-2] Message 0
[pool-1-thread-2] Message 1
[pool-1-thread-2] Message 2
[pool-1-thread-2] Message 3
[pool-1-thread-2] Message 4
In this above example, we have created 2 thread and executing 4 tasks. Look at the output only 2 threads pool-1-thread-1 and pool-1-thread-2 are reused for executing 4 tasks.
An ExecutorService that can schedule commands to run after a given delay, or to execute periodically.
The schedule() methods create tasks with various delays and return a task object that can be used to cancel or check execution. The scheduleAtFixedRate() and scheduleWithFixedDelay() methods create and execute tasks that run periodically until cancelled.
Commands submitted using the Executor.execute(Runnable) and ExecutorService submit methods are scheduled with a requested delay of zero. Zero and negative delays (but not periods) are also allowed in schedule methods, and are treated as requests for immediate execution.
Let's understand the methods of ScheduledExecutorService Interface with an example.
public class SchedulingTasksWithScheduledThreadPool {
public static void main(String[] args) throws InterruptedException {
System.out.println("Thread main started");
// Create a task
Runnable task1 = () -> {
System.out.println("Executing the task1 at: " + new Date());
};
// Create a task
Runnable task2 = () -> {
System.out.println("Executing the task2 at: " + new Date());
};
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
System.out.println("Scheduling task to run after 5 seconds... " + new Date());
scheduledExecutorService.schedule(task1, 5, TimeUnit.SECONDS);
scheduledExecutorService.schedule(task2, 5, TimeUnit.SECONDS);
scheduledExecutorService.shutdown();
System.out.println("Thread main finished");
}
}
Output:
Thread main started
Scheduling task to run after 5 seconds... Sat Sep 01 10:56:40 IST 2018
Thread main finished
Executing the task1 at: Sat Sep 01 10:56:45 IST 2018
Executing the task2 at: Sat Sep 01 10:56:45 IST 2018
scheduledExecutorService.schedule() function takes a Runnable, a delay value, and the unit of the delay. The above program executes the task after 5 seconds from the time of submission.
Now let’s see an example where we execute the task periodically -
public class SchedulingTasksWithScheduledThreadPool {
public static void main(String[] args) throws InterruptedException {
System.out.println("Thread main started");
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
// Create a task
Runnable task1 = () -> {
System.out.println("Executing the task1 at: " + new Date());
};
scheduledExecutorService.scheduleAtFixedRate(task1, 0, 2, TimeUnit.SECONDS);
System.out.println("Thread main finished");
}
}
Output:
Thread main started
Thread main finished
Executing the task1 at: Sat Sep 01 11:03:16 IST 2018
Executing the task1 at: Sat Sep 01 11:03:18 IST 2018
Executing the task1 at: Sat Sep 01 11:03:20 IST 2018
Executing the task1 at: Sat Sep 01 11:03:22 IST 2018
Executing the task1 at: Sat Sep 01 11:03:24 IST 2018
......
Note that if the task encounters an exception, subsequent executions of the task are suppressed. Otherwise, the task will only terminate if you either shut down the executor or kill the program.
Factory and utility methods for _Executor, ExecutorService, ScheduledExecutorService, ThreadFactory, and Callable _classes defined in this package. This class supports the following kinds of methods:
- Methods that create and return an ExecutorService set up with commonly useful configuration settings.
- Methods that create and return a ScheduledExecutorService set up with commonly useful configuration settings.
- Methods that create and return a "wrapped" ExecutorService, that disables reconfiguration by making implementation-specific methods inaccessible.
- Methods that create and return a ThreadFactory that sets newly created threads to a known state.
- Methods that create and return a Callable out of other closure-like forms, so they can be used in execution methods requiring Callable.