4

I am trying to implement multi threading in my Spring Boot app. I am just beginner on multi threading in Java and after making some search and reading articles on various pages, I need to be clarified about the following points. So;

  1. As far as I see, I can use Thread, Runnable or CompletableFuture in order to implement multi threading in a Java app. CompletableFuture seems a newer and cleaner way, but Thread may have more advantages. So, should I stick to CompletableFuture or use all of them based on the scenario?

  2. Basically I want to send 2 concurrent requests to the same service method by using CompletableFuture:

    CompletableFuture<Integer> future1 = fetchAsync(1);
    CompletableFuture<Integer> future2 = fetchAsync(2);
    
    Integer result1 = future1.get();
    Integer result2 = future2.get();
    

How can I send these request concurrently and then return result based on the following condition:

  • if the first result is not null, return result and stop process
  • if the first result is null, return the second result and stop process

How can I do this? Should I use CompletableFuture.anyOf() for that?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Jack
  • 1
  • 21
  • 118
  • 236
  • Does anybody else have never used `CompletableFuture` in Java multi threading? – Jack Jan 05 '23 at 07:17
  • 1
    When you say “but Thread may have more advantages” what advantages are you assuming? And what exactly do you expect for the second point? Your code example is already complete, just change the type of `result1` to `Integer`, as otherwise, it can’t be `null` at all. Then, `if(result1 == null) return future2.get(); else return result1;` There is no way to “stop process” with `CompletableFuture`. – Holger Jan 05 '23 at 10:27
  • I want to get the first response among two requests rather than a specific one. For example, sometimes I get `future1`, sometimes `future2` as there is no guarantee. So, I want to get the first response and check its value. Then based on this value get the second response or stop the second thread. I do not have to solve the problem only using `CompletableFuture`. If you have any suggestion with Thread or Runnable, I would be appreciated if you share by posting a sample code. – Jack Jan 05 '23 at 11:21
  • 2
    A `CompletableFuture` *is* using `Runnable` and `Thread` behind the scenes. I don’t know what advantage you think to gain by dealing with `Runnable` and `Thread` manually. If you want to stop tasks, you need an `ExecutorService` which returns `Future` objects truly supporting cancellation. However, that still requires active support for interruption in your tasks, i.e. the operations encapsulated by `fetchAsync`. – Holger Jan 05 '23 at 13:27
  • @Holger Thanks for explanations. Why do not you post an example as answer as the other friends? – Jack Jan 05 '23 at 20:13

3 Answers3

4

So, should I stick to CompletableFuture or use all of them based on the scenario?

Use the one that is most appropriate to the scenario. Obviously, we can't be more specific unless you explain the scenario.

There are various factors to take into account. For example:

  • Thread + Runnable doesn't have a natural way to wait for / return a result. (But it is not hard to implement.)
  • Repeatedly creating bare Thread objects is inefficient because thread creation is expensive. Thread pooling is better but you shouldn't implement a thread pool yourself.
  • Solutions that use an ExecutorService take care of thread pooling and allow you to use Callable and return a Future. But for a once-off async computation this might be over-kill.
  • Solutions that involve ComputableFuture allow you to compose and combine asynchronous tasks. But if you don't need to do that, using ComputableFuture may be overkill.

As you can see ... there is no single correct answer for all scenarios.


Should I use CompletableFuture.anyOf() for that?

No. The logic of your example requires that you must have the result for future1 to determine whether or not you need the result for future2. So the solution is something like this:

Integer i1 = future1.get();
if (i1 == null) {
    return future2.get();
} else {
    future2.cancel(true);
    return i1;
}

Note that the above works with plain Future as well as CompletableFuture. If you were using CompletableFuture because you thought that anyOf was the solution, then you didn't need to do that. Calling ExecutorService.submit(Callable) will give you a Future ...

It will be more complicated if you need to deal with exceptions thrown by the tasks and/or timeouts. In the former case, you need to catch ExecutionException and the extract its cause exception to get the exception thrown by the task.

There is also the caveat that the second computation may ignore the interrupt and continue on regardless.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • 1
    `ExecutorService` operations return a `Future`, not `CompletableFuture`. But you can use an `ExecutorService` as argument to `CompletableFuture`’s `…Async` methods. Further, `cancel()` requires a `boolean` argument but here, it has no effect anyway. – Holger Jan 05 '23 at 10:30
  • @StephenC Thanks get a lot for these wonderful explanations. At this stage, I think we have to get future1 or future2, because it is impossible get the first one among them, right? Another issue, how should I also add exception to this solution? – Jack Jan 05 '23 at 11:41
  • No. It is not because of that. The reason is that your conditions say that you only (need to) get future2 if future1 is `null`. So you need the `future1` answer before you decide if you need to call `future2.get`. – Stephen C Jan 05 '23 at 13:09
4

CompletableFuture is a tool which settles atop the Executor/ExecutorService abstraction, which has implementations dealing with Runnable and Thread. You usually have no reason to deal with Thread creation manually. If you find CompletableFuture unsuitable for a particular task you may try the other tools/abstractions first.

If you want to proceed with the first (in the sense of faster) non‑null result, you can use something like

CompletableFuture<Integer> future1 = fetchAsync(1);
CompletableFuture<Integer> future2 = fetchAsync(2);

Integer result = CompletableFuture.anyOf(future1, future2)
    .thenCompose(i -> i != null?
        CompletableFuture.completedFuture((Integer)i):
        future1.thenCombine(future2, (a, b) -> a != null? a: b))
    .join();

anyOf allows you to proceed with the first result, but regardless of its actual value. So to use the first non‑null result we need to chain another operation which will resort to thenCombine if the first result is null. This will only complete when both futures have been completed but at this point we already know that the faster result was null and the second is needed. The overall code will still result in null when both results were null.

Note that anyOf accepts arbitrarily typed futures and results in a CompletableFuture<Object>. Hence, i is of type Object and a type cast needed. An alternative with full type safety would be

CompletableFuture<Integer> future1 = fetchAsync(1);
CompletableFuture<Integer> future2 = fetchAsync(2);

Integer result = future1.applyToEither(future2, Function.identity())
    .thenCompose(i -> i != null?
        CompletableFuture.completedFuture(i):
        future1.thenCombine(future2, (a, b) -> a != null? a: b))
    .join();

which requires us to specify a function which we do not need here, so this code resorts to Function.identity(). You could also just use i -> i to denote an identity function; that’s mostly a stylistic choice.


Note that most complications stem from the design that tries to avoid blocking threads by always chaining a dependent operation to be executed when the previous stage has been completed. The examples above follow this principle as the final join() call is only for demonstration purposes; you can easily remove it and return the future, if the caller expects a future rather than being blocked.

If you are going to perform the final blocking join() anyway, because you need the result value immediately, you can also use

Integer result = future1.applyToEither(future2, Function.identity()).join();
if(result == null) {
    Integer a = future1.join(), b = future2.join();
    result = a != null? a: b;
}

which might be easier to read and debug. This ease of use is the motivation behind the upcoming Virtual Threads feature. When an action is running on a virtual thread, you don’t need to avoid blocking calls. So with this feature, if you still need to return a CompletableFuture without blocking the your caller thread, you can use

CompletableFuture<Integer> resultFuture = future1.applyToEitherAsync(future2, r-> {
    if(r != null) return r;
    Integer a = future1.join(), b = future2.join();
    return a != null? a: b;
}, Executors.newVirtualThreadPerTaskExecutor());

By requesting a virtual thread for the dependent action, we can use blocking join() calls within the function without hesitation which makes the code simpler, in fact, similar to the previous non-asynchronous variant.


In all cases, the code will provide the faster result if it is non‑null, without waiting for the completion of the second future. But it does not stop the evaluation of the unnecessary future. Stopping an already ongoing evaluation is not supported by CompletableFuture at all. You can call cancel(…) on it, but this will will only set the completion state (result) of the future to “exceptionally completed with a CancellationException

So whether you call cancel or not, the already ongoing evaluation will continue in the background and only its final result will be ignored.

This might be acceptable for some operations. If not, you would have to change the implementation of fetchAsync significantly. You could use an ExecutorService directly and submit an operation to get a Future which support cancellation with interruption.

But it also requires the operation’s code to be sensitive to interruption, to have an actual effect:

  • When calling blocking operations, use those methods that may abort and throw an InterruptedException and do not catch-and-continue.

  • When performing a long running computational intense task, poll Thread.interrupted() occasionally and bail out when true.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Veeerry veeery good, actually perfect explanation. That's what I would expect such kind of experienced users. Thanks a lot. – Jack Jan 06 '23 at 21:19
3

So, should I stick to CompletableFuture or use all of them based on the scenario?

Well, they all have different purposes and you'll probably use them all either directly or indirectly:

  • Thread represents a thread and while it can be subclassed in most cases you shouldn't do so. Many frameworks maintain thread pools, i.e. they spin up several threads that then can take tasks from a task pool. This is done to reduce the overhead that thread creation brings as well as to reduce the amount of contention (many threads and few cpu cores mean a lot of context switches so you'd normally try to have fewer threads that just work on one task after another).
  • Runnable was one of the first interfaces to represent tasks that a thread can work on. Another is Callable which has 2 major differences to Runnable: 1) it can return a value while Runnable has void and 2) it can throw checked exceptions. Depending on your case you can use either but since you want to get a result, you'll more likely use Callable.
  • CompletableFuture and Future are basically a way for cross-thread communication, i.e. you can use those to check whether the task is done already (non-blocking) or to wait for completion (blocking).

So in many cases it's like this:

  • you submit a Runnable or Callable to some executor
  • the executor maintains a pool of Threads to execute the tasks you submitted
  • the executor returns a Future (one implementation being CompletableFuture) for you to check on the status and results of the task without having to synchronize yourself.

However, there may be other cases where you directly provide a Runnable to a Thread or even subclass Thread but nowadays those are far less common.

How can I do this? Should I use CompletableFuture.anyOf() for that?

CompletableFuture.anyOf() wouldn't work since you'd not be able to determine which of the 2 you'd pass in was successful first.

Since you're interested in result1 first (which btw can't be null if the type is int) you basically want to do the following:

Integer result1 = future1.get(); //block until result 1 is ready
if( result1 != null ) {
  return result1;
} else {
  return future2.get(); //result1 was null so wait for result2 and return it
}

You'd not want to call future2.get() right away since that would block until both are done but instead you're first interested in future1 only so if that produces a result you wouldn't have for future2 to ever finish.

Note that the code above doesn't handle exceptional completions and there's also probably a more elegant way of composing the futures like you want but I don't remember it atm (if I do I'll add it as an edit).

Another note: you could call future2.cancel() if result1 isn't null but I'd suggest you first check whether cancelling would even work (e.g. you'd have a hard time really cancelling a webservice request) and what the results of interrupting the service would be. If it's ok to just let it complete and ignore the result that's probably the easier way to go.

Thomas
  • 87,414
  • 12
  • 119
  • 157
  • Thanks get a lot for these wonderful explanations. At this stage, I think we have to get future1 or future2, because it is impossible get the first one among them, right? Another issue, how should I also add exception to this solution? – Jack Jan 05 '23 at 11:40
  • I'm not sure about the first part of your question but you basically can do the following: if you want to use the result of whatever future completes first, then `CompletableFuture.anyOf()` can be used. If you want to favor future 1 over 2 you need to use the approach above. As for exceptions: `get()` could throw an exception if the future failed or was interrupted so you'd need to handle it. – Thomas Jan 05 '23 at 12:44