31

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?

Dwarak
  • 311
  • 1
  • 3
  • 3

4 Answers4

23

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);
Didier L
  • 18,905
  • 10
  • 61
  • 103
  • 4
    There is a difference. `CompletableFuture` uses `ForkJoinPool#commonPool` behind what can lead to unexpected behaviour. Please read more here https://dzone.com/articles/be-aware-of-forkjoinpoolcommonpool – walv Nov 12 '21 at 19:33
  • @walv this is already mentioned in the answer – Didier L Nov 14 '21 at 16:32
  • yes, it says `so it will use the common ForkJoinPool` but it doesn't explain that using ForkJoinPool under the hood might lead to unexpected behaviour that's why I added my comment with the link so people can read. Please take my comment as an enhancement to your answer. – walv Nov 15 '21 at 16:28
4

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.

Alexey Soshin
  • 16,718
  • 2
  • 31
  • 40
  • 2
    It's less boilerplate because it uses the common `ForkJoinPool`. If you want to use your own executor, it becomes more boilerplate. Concerning the ability to wait for all futures, it's also possible to do it with the first approach: just call `get()` on each of them. `allOf()` is more useful when you want to chain more calls on it, or return it. – Didier L Sep 13 '18 at 18:18
0

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--

ImGroot
  • 796
  • 1
  • 6
  • 17
  • The readability improvement is only due to the way the OP formatted his code and inlined the lambda's in the `CompletableFuture` example. It does not come from the API – quite the opposite in fact. – Didier L Sep 13 '18 at 18:14
0

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().

reddy
  • 7
  • 2