0

I execute a few callables through ThreadPoolExecutor. If thread list contains only 1 callable then I directly call call method of my CallableService. If list contains more than 1 callables then I execute all those threads in parallel via thread pool executor.

How can I achieve this with Java 8 CompletableFuture? And if future.get() is enhanced to avoid blocking, that will be a plus.

private static ThreadPoolExecutor myThreadPoolExecutor = new ThreadPoolExecutor(0, 100, 5L, TimeUnit.SECONDS, new SynchronousQueue<>());

public static void execute(List<Callable<Boolean>> threadList) throws Exception {

    List<Future<Boolean>> futureList = null;
    CallableService singleService = (CallableService) threadList.get(0);
    if (1 == threadList.size()) {
        singleService.call();
    }
    else {
        try {
            futureList = myThreadPoolExecutor.invokeAll(threadList);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    if (null != futureList) {
        for (Future<Boolean> future : futureList) {
            try {
                future.get();
            }
            catch (Exception e) {
                //do some calculations here and then throw exception
                throw new Exception(e.getMessage(), e);
            }
        }
    }
}
Tishy Tash
  • 357
  • 2
  • 12
  • Below link has some good suggestions. https://stackoverflow.com/questions/31092067/method-call-to-future-get-blocks-is-that-really-desirable – Mr Nobody Jan 14 '20 at 13:48
  • 2
    "And if `future.get()` is enhanced to avoid blocking, that will be a plus" – note that [`#invokeAll(Collection)`](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/concurrent/ExecutorService.html#invokeAll(java.util.Collection)) won't return until all the tasks are complete, which means the calls to `#get()` will return quickly. – Slaw Jan 14 '20 at 15:26

3 Answers3

3

There is no need for CompletableFuture, as the way you use the ExecutorService is sufficient, though, there are some aspects of the code flow which could be improved. You fetch the first element, even when not needed, and you cast it to CallableService for no reason, as you can call the method via the Callable interface already. In the other branch you are catching InterruptedException and proceeding, so the caller would never know that not all jobs have been executed. And in a straight-forward code flow, you don't need to check the list for null:

public static void execute(List<Callable<Boolean>> threadList) throws Exception {
    if(1 == threadList.size()) {
        Callable<Boolean> singleService = threadList.get(0);
        singleService.call();
    }
    else {
        List<Future<Boolean>> futureList = myThreadPoolExecutor.invokeAll(threadList);
        for(Future<Boolean> future : futureList) {
            try {
                future.get();
            }
            catch(Exception e) {
                //do some calculations here and then throw exception
                throw new Exception(e.getMessage(), e);
            }
        }
    }
}

You could shorten it further to

public static void execute(List<Callable<Boolean>> threadList) throws Exception {
    if(1 == threadList.size()) {
        threadList.get(0).call();
    }
    else {
        for(Future<Boolean> future : myThreadPoolExecutor.invokeAll(threadList)) {
            try {
                future.get();
            }
            catch(Exception e) {
                //do some calculations here and then throw exception
                throw new Exception(e.getMessage(), e);
            }
        }
    }
}

But that's a matter of preferred coding style. But note that it caught my eye that in the single element case, you're not performing the same exception handling.


To use CompletableFuture, we need an adapter method, as the convenience method supplyAsync requires a Supplier instead of a Callable. Using a modified variant of this answer, we get

public static void execute(List<Callable<Boolean>> threadList) throws Exception {
    if(1 == threadList.size()) {
        threadList.get(0).call();
    }
    else {
        CompletableFuture<?> all = CompletableFuture.allOf(
            threadList.stream()
                .map(c -> callAsync(c, myThreadPoolExecutor))
                .toArray(CompletableFuture<?>[]::new));
        try {
            all.get();
        }
        catch(Exception e) {
            //do some calculations here and then throw exception
            throw new Exception(e.getMessage(), e);
        }
    }
}
public static <R> CompletableFuture<R> callAsync(Callable<R> callable, Executor e) {
    CompletableFuture<R> cf = new CompletableFuture<>();
    CompletableFuture.runAsync(() -> {
        try { cf.complete(callable.call()); }
        catch(Throwable ex) { cf.completeExceptionally(ex); }
    }, e);
    return cf;
}

So we have no invokeAll which takes care of submitting all jobs. We have to do this manually, either with a loop or a stream operation. On the other hand, we get a single future via allOf representing the completion status, exceptionally if at least one job failed.

Unlike invokeAll, which waits for the completion, allOf only returns the future so it is the all.get() call which waits for the completion. We could do other things before it or even use this property to always perform the first job in the caller thread:

public static void execute(List<Callable<Boolean>> threadList) throws Exception {
    CompletableFuture<?> tail = CompletableFuture.allOf(
        threadList.stream().skip(1)
            .map(c -> callAsync(c, myThreadPoolExecutor))
            .toArray(CompletableFuture<?>[]::new)),
        head = callAsync(threadList.get(0), Runnable::run);
    try {
        head.get();
        tail.get();
    }
    catch(Exception e) {
        //do some calculations here and then throw exception
        throw new Exception(e.getMessage(), e);
    }
}

This will always call the first callable in the current thread, as Runnable::run used as Executor will perform the action immediately in the calling thread. But it's treated uniformly in all other aspects, especially the exception handling. When there is only one job, allOf invoke with an empty array will do nothing and return an already completed future, which will have the desired effect.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks for providing both ways. I'll opt for the first one. 1 question. Is `future.get()` not redundant here as well? Because I just learnt this about `invokeAll()`. "Executes the given tasks, returning a list of Futures holding their status and results when all complete. Future.isDone() is true for each element of the returned list." – Tishy Tash Jan 15 '20 at 08:06
  • 1
    @TishyTash `future.get()` after `invokeAll`, will not wait, that’s right, but its invocation will throw an exception if the job failed, which is caught and handled in a specific way in your code (“`//do some calculations here and then throw exception␍␊ throw new Exception(e.getMessage(), e);`”), so I assume, this is intentional. – Holger Jan 15 '20 at 11:45
  • Oh that's necessary. I got it. Thanks! – Tishy Tash Jan 15 '20 at 13:34
0

Future.isDone() tells us if the executor has finished processing the task. If the task is completed, it will return true otherwise, it returns false.

 for (Future<Boolean> future : futureList) {
   while(!future.isDone()) 
   {
          doSOmethingElse();
          Thread.sleep(300);//Optional
    }
 try {
                future.get();
        }
    catch (Exception e) 
 {
                //do some calculations here and then throw exception
                throw new Exception(e.getMessage(), e);
    }
}

But we don't have to worry about that since we get to the point where get() is called after making sure that the task is finished.

Mr Nobody
  • 144
  • 4
0

I execute a few callables through ThreadPoolExecutor. If thread list contains only 1 callable then I directly call call method of my CallableService. If list contains more than 1 callables then I execute all those threads in parallel via thread pool executor.

I guess you have already implemented this part. (You might run into memory usage issues if your jobs are heavy and you have 100 threads running as configured. But that is a different problem.)

And if future.get() is enhanced to avoid blocking, that will be a plus.

For this, you may take this approach:

  1. Create another ExecutorService whose job will be just to run the Future.get() calls.
  2. Submit your Future.get() to that service as shown below.
  3. Shut it down and await termination.

    if (null != futureList) {
        ExecutorService waitSvc = Executors.newCachedThreadPool();
        for (Future<Boolean> future : futureList) {
            try {
                waitSvc.submit( () -> future.get() );
            }
            catch (Exception e) {
                //do some calculations here and then throw exception
                throw new Exception(e.getMessage(), e);
            }
        }
        waitSvc.shutdown(); //This may take some time. You may want to call awaitTermination() after this.
    }
    

However, I feel that you should redesign the overall approach of using so many threads, unless this is only a for-learning application.

Sree Kumar
  • 2,012
  • 12
  • 9