When using CountDownLatch, we create a CountDownLatch with the number of tasks to execute. After a task is completed, we use countDown to signal that. The main thread uses await to wait for all tasks to finish.
Here we manually create the threads, you can also use ExecutorService.
When using CompletableFuture, we use supplyAsync to create new CompletableFutures, then use allOf to create a new CompletableFuture which is completed when all the provided CompletableFutures are completed.
We can use Flux and Mono from Project Reactor to run tasks. We create Monos from Callable objects. publishOn(Schedulers.elastic()) makes sure that emitting items from Monos are executed in different threads. Flux.blockLast waits for the Flux to emit all items.