Core Java

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();
    }
}
Fig. 1: Notification Using Java Runnable Implementation.
Fig. 1: Notification Using Java Runnable Implementation.

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/ConceptDescription
Asynchronous ExecutionExecute tasks asynchronously, allowing operations to run in the background without blocking the main thread.
CompositionChain multiple asynchronous operations together using methods like thenApply, thenCompose, and thenCombine.
Callbacks and ActionsAttach callbacks or actions to a future, specifying code to be executed upon completion or exceptional completion.
Exception HandlingHandle exceptions that might occur during the execution of asynchronous tasks using methods like exceptionally.
Combining FuturesCombine results from multiple futures using methods like thenCombine and thenCompose.
Asynchronous Execution ControlSpecify an executor for tasks using methods like supplyAsync and runAsync, controlling concurrency.
Waiting for CompletionWait for the completion of a future using blocking methods like get, but be cautious to avoid deadlocks.
Non-blocking ChecksPerform non-blocking checks to determine if a future has completed using methods like isDone and isCompletedExceptionally.
Handling Multiple FuturesUse 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.

Fig. 2: Notification Utilizing Java CompletableFuture.
Fig. 2: Notification Utilizing Java CompletableFuture.

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:

AspectExplanation
Core Pool SizeInitial 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 SizeMaximum 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 QueueHolds tasks that are waiting to be executed. Can be bounded or unbounded depending on the type of queue used.
Thread ReuseThreads in the core pool are reused for executing multiple tasks. This avoids the overhead of creating and destroying threads repeatedly.
Task ExecutionTasks are assigned to available threads in the core pool as they become available. Threads execute tasks from the queue in order.
Thread TerminationThreads 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 PolicySpecifies 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 ThreadsThreads that execute tasks from the queue. They are kept alive and reused, reducing the overhead of thread creation and destruction.
Task SchedulingThe order in which tasks are executed depends on the queue type and thread availability. Tasks in the queue wait their turn for execution.
Thread LifespanTemporary 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:

ParameterExplanation
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.
Fig. 3: Notification Using Java Executors.
Fig. 3: Notification Using Java Executors.

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:

ConceptDescription
Asynchronous ExecutionFutureTask allows executing tasks asynchronously in a separate thread.
Task TypesRunnable Tasks: Tasks that don’t return a result.
Callable Tasks: Tasks that return a result.
StatusIndicates whether the task is running, completed, or cancelled.
Result RetrievalThe get() method retrieves the task’s result. If the task isn’t done yet, it blocks until it’s finished.
CancellabilityTasks can be cancelled using the cancel(boolean mayInterruptIfRunning) method.
Exception HandlingExceptions 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();
    }
}
Fig. 4: Notification by Leveraging Java FutureTask.
Fig. 4: Notification by Leveraging Java FutureTask.

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!

Download
You can download the full source code of this example here: Notification when a Task is Completed using Java Executors

Odysseas Mourtzoukos

Mourtzoukos Odysseas is studying to become a software engineer, at Harokopio University of Athens. Along with his studies, he is getting involved with different projects on gaming development and web applications. He is looking forward to sharing his knowledge and experience with the world.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button