Notification when a Task is Completed using Java Executors
In this article, we’ll explore different approaches to achieve Efficient Notification when a Task is Completed using Java Executors. In the realm of modern Java programming, executing tasks asynchronously has become a necessity to achieve optimal performance and responsiveness in applications. Java provides us with a rich set of tools, particularly Executors, to manage and streamline asynchronous task execution. However, understanding when a specific task or a group of tasks has completed execution can be equally crucial.
1. Introduction
Asynchronous programming is at the heart of responsive and efficient Java applications. In many scenarios, it’s essential to be informed when an asynchronous task reaches completion. Whether it’s to update the user interface, trigger further actions, or simply log the outcome, having a reliable mechanism to be notified about task completion is invaluable.
2. Defining the Foundation
Let’s establish the foundation for our exploration by defining a simple task and a callback interface for notifications.
2.1 Task Implementation
We’ll start by implementing a basic Task
class that implements the Runnable
interface. This class represents the unit of work that we want to execute asynchronously.
class Task implements Runnable { @Override public void run() { System.out.println("Task in progress"); // Business logic goes here } }
2.2 Callback Interface
Next, we’ll define a CallbackInterface
that encapsulates the method to be called upon task completion. This approach allows us to create flexible and reusable notification mechanisms.
interface CallbackInterface { void taskDone(String details); }
And its implementation:
class Callback implements CallbackInterface { void taskDone(String details){ System.out.println("task complete: " + details); // Alerts/notifications go here } }
3. Runnable Implementation
This example introduces the concept of running a task by implementing the Runnable
interface.
We’ll create a class named RunnableImpl
that accepts a task, a callback, and additional details for better customization.
class RunnableImpl implements Runnable { private Runnable task; private CallbackInterface callback; private String taskDoneMessage; RunnableImpl(Runnable task, CallbackInterface callback, String taskDoneMessage) { this.task = task; this.callback = callback; this.taskDoneMessage = taskDoneMessage; } @Override public void run() { task.run(); callback.taskDone(taskDoneMessage); } }
By running it we can see the notification
public class Main { public static void main(String[] args) { Task task = new Task(); Callback callback = new Callback(); RunnableImpl ri = new RunnableImpl(task,callback,"Task Done"); ri.run(); } }
4. CompletableFuture Approach
In this example, we will cover the CompletableFuture
, we’ll provide a more comprehensive exploration of its capabilities for task execution and completion notification.
CompletableFuture
is a class introduced in Java 8 as part of the java.util.concurrent
package. It is designed to provide a higher-level, more flexible way to work with asynchronous tasks and manage their outcomes. The name “CompletableFuture” comes from the idea that it represents a future result that will be completed at some point in the future.
Here are some key features and concepts related to CompletableFuture
:
Feature/Concept | Description |
Asynchronous Execution | Execute tasks asynchronously, allowing operations to run in the background without blocking the main thread. |
Composition | Chain multiple asynchronous operations together using methods like thenApply , thenCompose , and thenCombine . |
Callbacks and Actions | Attach callbacks or actions to a future, specifying code to be executed upon completion or exceptional completion. |
Exception Handling | Handle exceptions that might occur during the execution of asynchronous tasks using methods like exceptionally . |
Combining Futures | Combine results from multiple futures using methods like thenCombine and thenCompose . |
Asynchronous Execution Control | Specify an executor for tasks using methods like supplyAsync and runAsync , controlling concurrency. |
Waiting for Completion | Wait for the completion of a future using blocking methods like get , but be cautious to avoid deadlocks. |
Non-blocking Checks | Perform non-blocking checks to determine if a future has completed using methods like isDone and isCompletedExceptionally . |
Handling Multiple Futures | Use methods like allOf and anyOf to manage and wait for multiple futures collectively. |
CompletableFuture
simplifies asynchronous programming by providing a clean way to chain tasks and handle their results or exceptions.
import java.util.concurrent.CompletableFuture; public class Main { public static void main(String[] args) { Task task = new Task(); Callback callback = new Callback(); CompletableFuture<Void> future = CompletableFuture.runAsync(task) .thenRun(() -> callback.taskDone("Task completed")); } }
By calling the .thenRun(() -> callback.taskDone("Task completed"));
, after the completion of the task, the callback.taskDone()
method is automatically called.
5. Advanced Task Execution with ThreadPoolExecutor
For scenarios requiring more fine-grained control over thread pool execution, we’ll explore extending ThreadPoolExecutor
and FutureTask
.
ThreadPoolExecutor
is a class in Java that provides a powerful and flexible way to manage and control a pool of worker threads for executing tasks concurrently. It’s part of the java.util.concurrent
package, which offers advanced concurrency utilities and higher-level abstractions for dealing with multi-threading and asynchronous operations.
Here’s an overview of how ThreadPoolExecutor
works:
Aspect | Explanation |
Core Pool Size | Initial number of threads in the pool. Threads in the core pool are kept alive and ready to execute tasks as long as there are tasks to process. |
Maximum Pool Size | Maximum number of threads that can be created if the task queue becomes full. Temporary threads beyond the core pool size are created and terminated as needed. |
Task Queue | Holds tasks that are waiting to be executed. Can be bounded or unbounded depending on the type of queue used. |
Thread Reuse | Threads in the core pool are reused for executing multiple tasks. This avoids the overhead of creating and destroying threads repeatedly. |
Task Execution | Tasks are assigned to available threads in the core pool as they become available. Threads execute tasks from the queue in order. |
Thread Termination | Threads beyond the core pool size can be terminated after being idle for a certain duration, freeing up system resources. Core threads remain active. |
Rejected Execution Policy | Specifies the action to take when a task cannot be accommodated due to a full queue and maximum pool size reached. Common policies include discarding the task, executing it in the caller’s thread, or blocking the caller. |
Worker Threads | Threads that execute tasks from the queue. They are kept alive and reused, reducing the overhead of thread creation and destruction. |
Task Scheduling | The order in which tasks are executed depends on the queue type and thread availability. Tasks in the queue wait their turn for execution. |
Thread Lifespan | Temporary threads (beyond the core pool size) are terminated if they remain idle for a specified duration. Core threads remain alive as long as the pool is active. |
Extending ThreadPoolExecutor
allows us to customize task execution and notification behavior. We’ll create AlertingThreadPoolExecutor
that triggers the callback upon task completion.
package org.example; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; class AlertingThreadPoolExecutor extends ThreadPoolExecutor { private CallbackInterface callback; public AlertingThreadPoolExecutor(CallbackInterface callback) { super(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10)); this.callback = callback; } @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); if (t == null) { callback.taskDone("Task completed Executor"); } else { // Handle task failure } } public static void main(String[] args) { Task task = new Task(); Callback callback = new Callback(); AlertingThreadPoolExecutor atpe = new AlertingThreadPoolExecutor(callback); atpe.execute(task); atpe.close(); } }
By calling super(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
, we instantiate a custom ThreadPoolExecutor
class. Let’s break down the parameters and their meanings:
Parameter | Explanation |
Core Pool Size (1) | The minimum number of threads the pool will maintain, even if they are idle. |
Maximum Pool Size (1) | The maximum number of threads the pool can create. |
Keep-Alive Time (60) | The amount of time that excess idle threads will wait for new tasks before being terminated. |
TimeUnit (TimeUnit.SECONDS) | The unit of time for the keep-alive time. |
Blocking Queue (new LinkedBlockingQueue<>(10)) | The queue used to hold tasks waiting to be executed. |
6. Leveraging FutureTask for Custom Notifications
FutureTask
is a class in Java that represents a computation that can be performed asynchronously and can produce a result. It implements the Future
interface and can be used to wrap a Callable
or Runnable
task and manage its execution, cancellation, and result retrieval.
Here’s an explanation of some key concepts related to FutureTask
:
Concept | Description |
Asynchronous Execution | FutureTask allows executing tasks asynchronously in a separate thread. |
Task Types | – Runnable Tasks: Tasks that don’t return a result.– Callable Tasks: Tasks that return a result. |
Status | Indicates whether the task is running, completed, or cancelled. |
Result Retrieval | The get() method retrieves the task’s result. If the task isn’t done yet, it blocks until it’s finished. |
Cancellability | Tasks can be cancelled using the cancel(boolean mayInterruptIfRunning) method. |
Exception Handling | Exceptions thrown by the task are captured and can be accessed when calling get() . |
Extending FutureTask
provides an elegant way to incorporate custom logic upon task completion. We’ll create AlertingFutureTask
to achieve this.
package org.example; import java.util.concurrent.FutureTask; class AlertingFutureTask extends FutureTask<String> { private CallbackInterface callback; AlertingFutureTask(Runnable runnable, CallbackInterface callback) { super(runnable, null); this.callback = callback; } @Override protected void done() { callback.taskDone("Task completed FutureTask"); } public static void main(String[] args) { Task task = new Task(); Callback callback = new Callback(); AlertingFutureTask aft = new AlertingFutureTask(task,callback); aft.run(); } }
7. Conclusion
In this comprehensive exploration, we’ve delved into the efficient notification of task completion using Java Executors. By extending the original concepts and showcasing more versatile implementations, we’ve demonstrated how to receive notifications upon task completion across different scenarios. Whether through enhanced Runnable
implementations, the streamlined power of CompletableFuture
, or customizations with ThreadPoolExecutor
and FutureTask
, Java developers now have a versatile toolkit for managing asynchronous tasks while being promptly notified about their completion.
8. Download the Source Code
This was an example of how to get Notification when a Task is Completed using Java Executors!
You can download the full source code of this example here: Notification when a Task is Completed using Java Executors