ExecutorService vs CompletableFuture

Viewed 25823

I have been trying to implement an asynchronous process, where the parent method calls a child method which would in-turn call three different methods. I want all of this process to be done asynchronously i.e. after these three calls in the child method are made in parallel the control should go back to the parent method and continue with the rest of its execution.

I have this code which when tested works fine.

public ReturnSomething parent(){
    child();
    ...//rest to UI
}

private void child(){
  ExecutorService executorService = Executors.newFixedThreadPool(3);

  Runnable service1 = () -> {
     MyFileService.service1();
  };

  Runnable service2 = () -> {
      MyFileService.service2();
  };

  Runnable service3 = () -> {
      MyFileService.service3();
  };

  executorService.submit(service1);
  executorService.submit(service2);
  executorService.submit(service3);
}

Now, my lead is asking me to use this rather.

public ReturnSomething parent(){
    child();
    ...//rest to UI
}

private void child(){
    CompletableFuture.supplyAsync(() ->  MyFileService.service1();
    CompletableFuture.supplyAsync(() ->  MyFileService.service2();
    CompletableFuture.supplyAsync(() ->  MyFileService.service3();
}

I understand that that CompletableFuture is new from Java 8, but how is the 2nd code better than the 1st? Since, for ExecutorService, I am not calling the "get()" method I would not be waiting for the aysnc response. So, can some one please explain what is the difference?

4 Answers

Functionally, the two approaches are more or less the same:

  • you submit your tasks for execution;
  • you don't wait for the result.

Technically, however, there are some subtle differences:

  • In the second approach, you didn't specify an executor, so it will use the common ForkJoinPool. You would have to pass an executor as second argument of supplyAsync() if you don't want that;
  • The CompletableFuture API allows to easily chain more calls with thenApply(), thenCompose() etc. It is thus more flexible than the simple Future returned by ExecutorService.submit();
  • Using CompletableFuture allows to easily return a future from your child() method using return CompletableFuture.allOf(the previously created futures).

Concerning readability, it's a matter of preference, but if you want equivalent code the CompletableFuture approach might be considered a bit less readable once you have formatted it similarly. Compare:

executorService.submit(MyFileService::service1);
executorService.submit(MyFileService::service2);
executorService.submit(MyFileService::service3);

with

CompletableFuture.supplyAsync(MyFileService::service1, executorService);
CompletableFuture.supplyAsync(MyFileService::service2, executorService);
CompletableFuture.supplyAsync(MyFileService::service3, executorService);

You're not waiting for results in both cases.

The advantage of the second approach is simply less boilerplate. That's what runAsync() and supplyAsync() are good for.

But if you don't actually return any value, you should use runAsync()

What second approach also provides, is the ability to wait for all futures with CompletableFuture.allOf(). Which is also doesn't exist in the first scenario.

First of all, the latter improves readability. Secondly, I'm not sure whether you were in some kind of a hurry (to explain your problem in a generic way) that you created a new instance of ExecutorService every time the parent() makes a call to child().

Otherwise as well, the CompletableFuture.supplyAsync returns a much convenient way to get a reference to a common shared pool which will ease your life unless you want to do specific customizations on the way threads in the pool should service your request.

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html#commonPool--

if you are using executorservice, don't forget to call shutdown() on the executor. Also you can as well make use of runAsync() instead of supplyAsync().

Related