Java Executors: How to Be Notified, Without Blocking, When a Task Completes

Java executors: how to be notified, without blocking, when a task completes?

Define a callback interface to receive whatever parameters you want to pass along in the completion notification. Then invoke it at the end of the task.

You could even write a general wrapper for Runnable tasks, and submit these to ExecutorService. Or, see below for a mechanism built into Java 8.

class CallbackTask implements Runnable {

private final Runnable task;

private final Callback callback;

CallbackTask(Runnable task, Callback callback) {
this.task = task;
this.callback = callback;
}

public void run() {
task.run();
callback.complete();
}

}

With CompletableFuture, Java 8 included a more elaborate means to compose pipelines where processes can be completed asynchronously and conditionally. Here's a contrived but complete example of notification.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

public class GetTaskNotificationWithoutBlocking {

public static void main(String... argv) throws Exception {
ExampleService svc = new ExampleService();
GetTaskNotificationWithoutBlocking listener = new GetTaskNotificationWithoutBlocking();
CompletableFuture<String> f = CompletableFuture.supplyAsync(svc::work);
f.thenAccept(listener::notify);
System.out.println("Exiting main()");
}

void notify(String msg) {
System.out.println("Received message: " + msg);
}

}

class ExampleService {

String work() {
sleep(7000, TimeUnit.MILLISECONDS); /* Pretend to be busy... */
char[] str = new char[5];
ThreadLocalRandom current = ThreadLocalRandom.current();
for (int idx = 0; idx < str.length; ++idx)
str[idx] = (char) ('A' + current.nextInt(26));
String msg = new String(str);
System.out.println("Generated message: " + msg);
return msg;
}

public static void sleep(long average, TimeUnit unit) {
String name = Thread.currentThread().getName();
long timeout = Math.min(exponential(average), Math.multiplyExact(10, average));
System.out.printf("%s sleeping %d %s...%n", name, timeout, unit);
try {
unit.sleep(timeout);
System.out.println(name + " awoke.");
} catch (InterruptedException abort) {
Thread.currentThread().interrupt();
System.out.println(name + " interrupted.");
}
}

public static long exponential(long avg) {
return (long) (avg * -Math.log(1 - ThreadLocalRandom.current().nextDouble()));
}

}

ExecutorService - How to wait for completition of all tasks in non-blocking style

You are trying to solve a problem that doesn’t exist. Consider the documentation of ExecutorService.shutdown():

Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. …

This method does not wait for previously submitted tasks to complete execution.

In other words, just calling shutdown() does already everything you want

  • It finishes all previously submitted tasks
  • It initiates a shutdown
  • It does not wait

The only obstacle is that you are calling awaitTermination despite the fact that you don’t want to wait, which has a trivial solution: don’t call awaitTermination.

The confusion arises because in your question you are asking “how I can be notified when all tasks are done so I could call the shutdown() method” but that is in contradiction to what you are actually doing in the code. You are calling awaitTermination after shutDown so you are not waiting in order to initiate the shutdown, but initiating the shutdown first and waiting for its completion then, which is the purpose of awaitTermination, waiting for the completion of the shutdown.


To put it in one sentence, just call shutDown after submission in order to shutdown the service after completion of all submitted jobs and don’t call awaitTermination unless you really want to wait for the termination.

Executing in-parallel groups of in-order blocking tasks

After a bit more time on this, I've come up with a few possible solutions using either Executors or Futures. Not sure yet which would be better than the other, but since I know I can extend ThreadPoolExecutor (say, to add a pause feature) I'll probably lean towards Executors.

Otherwise, if anyone has comments they're always welcome!

I'm keeping both solutions in my GH for now (), but I'll put them below as well.
https://github.com/TinaTiel/concurrency-learning

Futures Implementation

package futures;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.stream.Collectors;

public class CommandActionExample {

public static void main(String[] args) {

// Initialize some starting params
Random random = new Random();
int maxActions = 20;
int maxCommands = 5;

// Generate some commands, with a random number of actions.
// We'll use the indexes as the command and action names to keep it simple/readable
List<Command> commands = new ArrayList<>();
for(Integer c = 0; c < maxCommands; c++) {
Command command = new Command(String.format("%d", c+1));
for(Integer a = 0; a < random.nextInt(maxActions); a++) {
Action action = new Action(random, String.format("%d", a+1));
command.addAction(action);
}
commands.add(command);
}

// Print out the commands we'll execute, again to keep the results readable/understandable
System.out.println("Commands to execute: \n" + commands.stream().map(Command::toString).collect(Collectors.joining("\n")) + "\n");

// Build a Future that tries to execute all commands (supplied as Futures) in an arbitrary order
try {
CompletableFuture.allOf(commands.stream()
.map((Function<Command, CompletableFuture<Void>>) CompletableFuture::runAsync)
.collect(Collectors.toList())
.toArray(CompletableFuture[]::new)
).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// commands.get(0).run(); // sanity check one of the command's actions run as expected

// When we execute the results, the actions should be executed in-order within a command at some point in the future
// (not started all at once), so something like:
// 0 Command-2:Action-1 scheduled at 34
// 0 Command-1:Action-1 scheduled at 21
// 0 Command-3:Action-1 scheduled at 4
// 4 Command-3:Action2 scheduled at ...
// 21 Command-1:Action-2 scheduled at ...
// 34 Command-1-Action-2 scheduled at ...
// ...
// Now how to test this...Maybe with JUnit inOrder.verify(...).run() ?

}

public static class Action implements Runnable {

private Command command;
private final Random random;
private final String name;

public Action(Random random, String name) {
this.random = random;
this.name = name;
}

public void setCommand(Command command) {
this.command = command;
}

@Override
public void run() {

// Simply sleep for a random period of time. This simulates pieces of work being done (network request, etc.)
long msTime = random.nextInt(1000);
System.out.println(new Timestamp(System.currentTimeMillis()) + ": Command-" + command.name + ":Action-" + name + " executing on Thread '" + Thread.currentThread().getName() + "' executing for " + msTime + "ms");
try {
Thread.sleep(msTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString() {
return "Action{" +
"name='" + name + '\'' +
'}';
}
}

public static class Command implements Runnable {

private final String name;
private final List<Action> actions = new ArrayList<>();

public Command(String name) {
this.name = name;
}

public void addAction(Action action) {
action.setCommand(this);
actions.add(action);
}

@Override
public void run() {
// If there are no actions, then do nothing
if(actions.isEmpty()) return;

// Build up a chain of futures.
// Looks like we have to build them up in reverse order, so start with the first action...
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(actions.remove(0));

// ...And then reverse the list and build the rest of the chain
// (yes we could execute backwards...but it's not common and I/others probably don't like to reason about it)
Collections.reverse(actions);
for(int i=0; i< actions.size(); i++) {
completableFuture.thenRun(actions.get(i));
}

// Execute our chain
try {
completableFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}

@Override
public String toString() {
return "Command{" +
"name='" + name + '\'' +
", actions=" + actions +
'}';
}
}

}

Results

Output and schedule is as-expected, but it appears Futures use the ForkJoinPool.

Commands to execute: 
Command{name='1', actions=[Action{name='1'}, Action{name='2'}, Action{name='3'}]}
Command{name='2', actions=[Action{name='1'}, Action{name='2'}, Action{name='3'}, Action{name='4'}, Action{name='5'}, Action{name='6'}]}
Command{name='3', actions=[Action{name='1'}, Action{name='2'}, Action{name='3'}, Action{name='4'}, Action{name='5'}, Action{name='6'}, Action{name='7'}]}
Command{name='4', actions=[Action{name='1'}, Action{name='2'}, Action{name='3'}, Action{name='4'}, Action{name='5'}, Action{name='6'}]}
Command{name='5', actions=[Action{name='1'}, Action{name='2'}, Action{name='3'}, Action{name='4'}, Action{name='5'}, Action{name='6'}]}

2020-12-30 21:17:27.11: Command-2:Action-1 executing on Thread 'ForkJoinPool.commonPool-worker-5' executing for 207ms
2020-12-30 21:17:27.11: Command-4:Action-1 executing on Thread 'ForkJoinPool.commonPool-worker-9' executing for 930ms
2020-12-30 21:17:27.11: Command-1:Action-1 executing on Thread 'ForkJoinPool.commonPool-worker-3' executing for 948ms
2020-12-30 21:17:27.11: Command-3:Action-1 executing on Thread 'ForkJoinPool.commonPool-worker-7' executing for 173ms
2020-12-30 21:17:27.11: Command-5:Action-1 executing on Thread 'ForkJoinPool.commonPool-worker-11' executing for 348ms
2020-12-30 21:17:27.314: Command-3:Action-2 executing on Thread 'ForkJoinPool.commonPool-worker-7' executing for 890ms
2020-12-30 21:17:27.345: Command-2:Action-2 executing on Thread 'ForkJoinPool.commonPool-worker-5' executing for 178ms
2020-12-30 21:17:27.485: Command-5:Action-2 executing on Thread 'ForkJoinPool.commonPool-worker-11' executing for 702ms
2020-12-30 21:17:27.485: Command-5:Action-3 executing on Thread 'ForkJoinPool.commonPool-worker-15' executing for 161ms
2020-12-30 21:17:27.532: Command-2:Action-3 executing on Thread 'ForkJoinPool.commonPool-worker-5' executing for 201ms
2020-12-30 21:17:27.657: Command-5:Action-4 executing on Thread 'ForkJoinPool.commonPool-worker-15' executing for 257ms
2020-12-30 21:17:27.735: Command-2:Action-4 executing on Thread 'ForkJoinPool.commonPool-worker-5' executing for 518ms
2020-12-30 21:17:27.919: Command-5:Action-5 executing on Thread 'ForkJoinPool.commonPool-worker-15' executing for 258ms
2020-12-30 21:17:28.06: Command-4:Action-2 executing on Thread 'ForkJoinPool.commonPool-worker-9' executing for 926ms
2020-12-30 21:17:28.075: Command-1:Action-2 executing on Thread 'ForkJoinPool.commonPool-worker-3' executing for 413ms
2020-12-30 21:17:28.184: Command-5:Action-6 executing on Thread 'ForkJoinPool.commonPool-worker-15' executing for 77ms
2020-12-30 21:17:28.216: Command-3:Action-3 executing on Thread 'ForkJoinPool.commonPool-worker-7' executing for 487ms
2020-12-30 21:17:28.263: Command-2:Action-5 executing on Thread 'ForkJoinPool.commonPool-worker-5' executing for 570ms
2020-12-30 21:17:28.497: Command-1:Action-3 executing on Thread 'ForkJoinPool.commonPool-worker-3' executing for 273ms
2020-12-30 21:17:28.716: Command-3:Action-4 executing on Thread 'ForkJoinPool.commonPool-worker-7' executing for 302ms
2020-12-30 21:17:28.833: Command-2:Action-6 executing on Thread 'ForkJoinPool.commonPool-worker-5' executing for 202ms
2020-12-30 21:17:28.992: Command-4:Action-3 executing on Thread 'ForkJoinPool.commonPool-worker-9' executing for 733ms
2020-12-30 21:17:29.024: Command-3:Action-5 executing on Thread 'ForkJoinPool.commonPool-worker-7' executing for 756ms
2020-12-30 21:17:29.727: Command-4:Action-4 executing on Thread 'ForkJoinPool.commonPool-worker-9' executing for 131ms
2020-12-30 21:17:29.78: Command-3:Action-6 executing on Thread 'ForkJoinPool.commonPool-worker-7' executing for 920ms
2020-12-30 21:17:29.858: Command-4:Action-5 executing on Thread 'ForkJoinPool.commonPool-worker-9' executing for 305ms
2020-12-30 21:17:30.168: Command-4:Action-6 executing on Thread 'ForkJoinPool.commonPool-worker-9' executing for 612ms
2020-12-30 21:17:30.715: Command-3:Action-7 executing on Thread 'ForkJoinPool.commonPool-worker-7' executing for 330ms

Executors Implementation

package executors;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class CommandActionExample {

public static void main(String[] args) {

// Initialize some starting params
Random random = new Random();
int maxActions = 20;
int maxCommands = 5;

// Generate some commands, with a random number of actions.
// We'll use the indexes as the command and action names to keep it simple/readable
List<Command> commands = new ArrayList<>();
for(Integer c = 0; c < maxCommands; c++) {
Command command = new Command(String.format("%d", c+1));
for(Integer a = 0; a < random.nextInt(maxActions); a++) {
Action action = new Action(random, String.format("%d", a+1));
command.addAction(action);
}
commands.add(command);
}

// Print out the commands we'll execute, again to keep the results readable/understandable
System.out.println("Commands to execute: \n" + commands.stream().map(Command::toString).collect(Collectors.joining("\n")) + "\n");

ExecutorService executorService = Executors.newFixedThreadPool(20);
for(Command command:commands) executorService.submit(command);

// When we execute the results, the actions should be executed in-order within a command at some point in the future
// (not started all at once), so something like:
// 0 Command-2:Action-1 scheduled at 34
// 0 Command-1:Action-1 scheduled at 21
// 0 Command-3:Action-1 scheduled at 4
// 4 Command-3:Action2 scheduled at ...
// 21 Command-1:Action-2 scheduled at ...
// 34 Command-1-Action-2 scheduled at ...
// ...
// Now how to test this...Maybe with JUnit inOrder.verify(...).run() ?

}

public static class Action implements Runnable {

private Command command;
private final Random random;
private final String name;

public Action(Random random, String name) {
this.random = random;
this.name = name;
}

public void setCommand(Command command) {
this.command = command;
}

@Override
public void run() {

// Simply sleep for a random period of time. This simulates pieces of work being done (network request, etc.)
long msTime = random.nextInt(1000);
System.out.println(new Timestamp(System.currentTimeMillis()) + ": Command-" + command.name + ":Action-" + name + " executing on Thread '" + Thread.currentThread().getName() + "' executing for " + msTime + "ms");
try {
Thread.sleep(msTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString() {
return "Action{" +
"name='" + name + '\'' +
'}';
}
}

public static class Command implements Runnable {

private final String name;
private final List<Action> actions = new ArrayList<>();

public Command(String name) {
this.name = name;
}

public void addAction(Action action) {
action.setCommand(this);
actions.add(action);
}

@Override
public void run() {
// If there are no actions, then do nothing
if(actions.isEmpty()) return;

ExecutorService executor = Executors.newSingleThreadExecutor(); // Because there is only one thread, this has the effect of executing sequentially and blocking
for(Action action:actions) executor.submit(action);

}

@Override
public String toString() {
return "Command{" +
"name='" + name + '\'' +
", actions=" + actions +
'}';
}
}

}

Result

Output and schedule are as expected

2020-12-31 09:43:09.952: Command-3:Action-1 executing on Thread 'pool-4-thread-1' executing for 632ms
2020-12-31 09:43:09.952: Command-4:Action-1 executing on Thread 'pool-3-thread-1' executing for 932ms
2020-12-31 09:43:09.952: Command-2:Action-1 executing on Thread 'pool-6-thread-1' executing for 586ms
2020-12-31 09:43:09.952: Command-5:Action-1 executing on Thread 'pool-5-thread-1' executing for 987ms
2020-12-31 09:43:09.952: Command-1:Action-1 executing on Thread 'pool-2-thread-1' executing for 706ms
2020-12-31 09:43:10.562: Command-2:Action-2 executing on Thread 'pool-6-thread-1' executing for 329ms
2020-12-31 09:43:10.608: Command-3:Action-2 executing on Thread 'pool-4-thread-1' executing for 503ms
2020-12-31 09:43:10.891: Command-2:Action-3 executing on Thread 'pool-6-thread-1' executing for 443ms
2020-12-31 09:43:10.9: Command-4:Action-2 executing on Thread 'pool-3-thread-1' executing for 866ms
2020-12-31 09:43:10.955: Command-5:Action-2 executing on Thread 'pool-5-thread-1' executing for 824ms
2020-12-31 09:43:11.346: Command-2:Action-4 executing on Thread 'pool-6-thread-1' executing for 502ms
2020-12-31 09:43:11.766: Command-4:Action-3 executing on Thread 'pool-3-thread-1' executing for 638ms
2020-12-31 09:43:11.779: Command-5:Action-3 executing on Thread 'pool-5-thread-1' executing for 928ms
2020-12-31 09:43:11.848: Command-2:Action-5 executing on Thread 'pool-6-thread-1' executing for 179ms
2020-12-31 09:43:12.037: Command-2:Action-6 executing on Thread 'pool-6-thread-1' executing for 964ms
2020-12-31 09:43:12.412: Command-4:Action-4 executing on Thread 'pool-3-thread-1' executing for 370ms
2020-12-31 09:43:12.709: Command-5:Action-4 executing on Thread 'pool-5-thread-1' executing for 204ms
2020-12-31 09:43:12.783: Command-4:Action-5 executing on Thread 'pool-3-thread-1' executing for 769ms
2020-12-31 09:43:12.913: Command-5:Action-5 executing on Thread 'pool-5-thread-1' executing for 188ms
2020-12-31 09:43:13.102: Command-5:Action-6 executing on Thread 'pool-5-thread-1' executing for 524ms
2020-12-31 09:43:13.555: Command-4:Action-6 executing on Thread 'pool-3-thread-1' executing for 673ms
2020-12-31 09:43:13.634: Command-5:Action-7 executing on Thread 'pool-5-thread-1' executing for 890ms
2020-12-31 09:43:14.23: Command-4:Action-7 executing on Thread 'pool-3-thread-1' executing for 147ms
2020-12-31 09:43:14.527: Command-5:Action-8 executing on Thread 'pool-5-thread-1' executing for 538ms

I need not wait until executing async method call()

The main thread is blocked on the call to future.get() until the task completes. Remove this call and your second log statement will print immediately.

That addresses what you asked exactly. But I doubt it is what you want.

The purpose of submitting an asynchronous task is to permit the main thread to carry on immediately with other work. If the main thread requires the result of the task to proceed, the task is not a candidate for performing asynchronously.

Instead, add the processing of the result to the asynchronous task itself. Then the main thread does not have to wait for the result.

Asynchronous single task executor

Perhaps I’ve misunderstood the problem, but it seems you are over complicating the situation. Rather than keeping track of the task, keep track of the Future returned by ExecutorService#submit. A Future object is your tether leading back to the task being executed.

Define a member field for the Future.

Future future ;

Test the Future when request to process is made. Call Future#isDone to test. Javadoc says:

Returns true if this task completed. Completion may be due to normal termination, an exception, or cancellation -- in all of these cases, this method will return true.

if( Objects.isNull( this.future ) || this.future.isDone() ) {
… proceed with request to process data.
this.future = executorService.submit( … ) ;
return Optional.of( this.future ) ;
} else {
… Refuse the request to process data.
… Do *not* submit any task to the executor service.
return Optional.empty() ;
}

TaskMaster solution

In various comments, you presented more details of your problem.

You want to submit tasks from various threads to a single object. Let's call that object TaskMaster for clarity. That TaskMaster instance tracks whether its nested executor service is currently working on a task or not.

  • If busy working on a task, any incoming task tendered is rejected. That rejection takes the form of an Optional< Future >.
  • If not currently working on a task, the tendered task is accepted, and assigned to the executor service for immediate execution. This acceptance takes the form of a loaded Optional< Future >.

Since the code shown above discussed here will be accessed across threads, we must protect the Future future ; in a thread-safe manner. One easy way to do that is to mark synchronized on the one and only method for tendering a task to the TaskMaster.

package singletask;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

// Runs one task at a time, rejecting any task tendered while already executing a task.
public class TaskMaster
{
// Member fields.
private final ExecutorService executorService;
private Future future;

// Constructor
public TaskMaster ( )
{
this.executorService = Executors.newSingleThreadExecutor();
}

public synchronized Optional < Future > tender ( Runnable task )
{
if ( Objects.isNull( this.future ) || this.future.isDone() )
{
// Proceed with immediate execution of the tendered task.
this.future = executorService.submit( task );
return Optional.of( this.future );
} else
{
// Already busy on a task. Reject this tendered task.
return Optional.empty();
}
}

public void shutdownAndAwaitTerminationOfExecutorService ( )
{
if ( Objects.isNull( this.executorService ) ) { return; }
this.executorService.shutdown(); // Stop new tasks from being submitted.
try
{
// Wait a while for existing tasks to terminate
if ( ! this.executorService.awaitTermination( 60 , TimeUnit.SECONDS ) )
{
this.executorService.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if ( ! this.executorService.awaitTermination( 60 , TimeUnit.SECONDS ) )
System.err.println( "Pool did not terminate." );
}
}
catch ( InterruptedException ex )
{
// (Re-)Cancel if current thread also interrupted
this.executorService.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
}

Usage shown next. Beware: Multithreaded calls to System.out.println do not always appear on the console chronologically. Always include, and inspect, timestamps to verify the order.

package singletask;

import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.Future;

public class App
{
public static void main ( String[] args )
{
App app = new App();
app.demo();
}

private void demo ( )
{
Runnable task = ( ) -> {
UUID taskID = UUID.randomUUID();
System.out.println( "Starting task " + taskID + " at " + Instant.now() );
// Pretend to do some long hard work, by sleeping.
try { Thread.sleep( Duration.ofSeconds( 5 ).toMillis() ); } catch ( InterruptedException e ) { e.printStackTrace(); }
System.out.println( "Ending task " + taskID + " at " + Instant.now() );
};

TaskMaster taskMaster = new TaskMaster();

Optional < Future > f1 = taskMaster.tender( task ); // We expect acceptance, showing `Optional[java.util.concurrent.FutureTask@…`.
try { Thread.sleep( Duration.ofMillis( 500 ).toMillis() ); } catch ( InterruptedException e ) { e.printStackTrace(); }
System.out.println( "f1 = " + f1 );

Optional < Future > f2 = taskMaster.tender( task ); // We expect rejection, showing `Optional.empty`.
System.out.println( "f2 = " + f2 );

try { Thread.sleep( Duration.ofSeconds( 7 ).toMillis() ); } catch ( InterruptedException e ) { e.printStackTrace(); }
Optional < Future > f3 = taskMaster.tender( task ); // We expect acceptance, showing `Optional[java.util.concurrent.FutureTask@…`.
System.out.println( "f3 = " + f3 );

// Attempt a graceful shutwdown.
taskMaster.shutdownAndAwaitTerminationOfExecutorService();
System.out.println( "Demo ending at " + Instant.now() );
}
}

When run.

Starting task cc48efaa-390b-414d-9f3a-539e2be249b9 at 2022-02-03T06:42:58.516852Z
f1 = Optional[java.util.concurrent.FutureTask@1fb3ebeb[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@31befd9f[Wrapped task = singletask.App$$Lambda$14/0x0000000800c01208@1c20c684]]]
f2 = Optional.empty
Ending task cc48efaa-390b-414d-9f3a-539e2be249b9 at 2022-02-03T06:43:03.530138Z
Starting task a3de548c-b068-435c-a6cb-856d2f539042 at 2022-02-03T06:43:06.011485Z
f3 = Optional[java.util.concurrent.FutureTask@816f27d[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@1218025c[Wrapped task = singletask.App$$Lambda$14/0x0000000800c01208@1c20c684]]]
Ending task a3de548c-b068-435c-a6cb-856d2f539042 at 2022-02-03T06:43:11.013576Z
Demo ending at 2022-02-03T06:43:11.014180Z

Custom ExecutorService

While that TaskMaster code above works, and offers the Optional objects you asked for, I would recommend another approach.

You can make your own version of an ExecutorService. Your implementation could do something similar to what we saw above, tracking a single task’s execution.

Rather than returning an Optional< Future >, the more orthodox approach would be to provide a submit method implementation that either:

  • Returns a Future if the tendered task can be immediately executed, or …
  • Throws a RejectedExecutionException because a task is already running.

This behavior is defined in the Javadoc of ExecutorService. Any methods of your which tender tasks to this custom executor service would trap for this exception rather than examine an Optional.

In other words, to modify an excerpt from your Comment:

If two users simultaneously try to request data processing, only one of them will succeed and receive a Future, and another will see an exception thrown, indicating that the request was rejected.



Related Topics



Leave a reply



Submit