Java+Concurrency+eBook
Java+Concurrency+eBook
Guide
V 1.0.2
1. Life Cycle of a Thread in Java
1. Introduction 2
2. Multithreading in Java 3
3.1. New 4
3.2. Runnable 5
3.3. Blocked 6
3.4. Waiting 7
3.6. Terminated 10
4. Conclusion 12
6.1. Timer 20
6.2. ScheduledThreadPoolExecutor 21
8. Conclusions 24
3.1. wait() 28
4.1. notify() 30
4.2. notifyAll() 30
6. Conclusion 36
4. The Thread.join() Method in Java
1. Overview 38
5. Conclusion 43
2. Why Synchronization? 46
3.4. Reentrancy 51
4. Conclusion 52
3.2. Reordering 57
6. Happens-Before Ordering 61
6.1. Piggybacking 61
7. Conclusion 63
2. Instantiating ExecutorService 66
7. ExecutorService vs Fork/Join 74
8. Conclusion 75
8. Guide To CompletableFuture
1. Introduction 77
6. Combining Futures 84
7.1. thenApply() 86
7.2. thenCompose() 86
9. Handling Errors 88
12. Conclusion 92
1. Life Cycle of a Thread in Java
1
1. Introduction
In this chapter, we’ll discuss a core concept in Java in detail: the lifecycle of
a thread.
2
2. Multithreading in Java
3
3. Life Cycle of a Thread in Java
The java.lang.Thread class contains a static State enum that defines its
potential states. During any given point in time, the thread can only be in
one of these states:
NEW – a newly created thread that hasn’t yet started the execution
RUNNABLE – either running or ready for execution but waiting for resource
allocation
BLOCKED – waiting to acquire a monitor lock to enter or re-enter a
synchronized block/method
WAITING – waiting for some other thread to perform a particular action
without any time limit
TIMED_WAITING – waiting for some other thread to perform a specific action
for a specified period
TERMINATED – has completed its execution
All of these states are covered in the diagram above; let’s discuss each in
detail.
3.1. New
A NEW Thread (or a Born Thread) is a thread that’s been created but not
yet started. It remains in this state until we start it using the start() method.
The following code snippet shows a newly created thread that’s in the NEW
state:
Since we’ve not yet started the mentioned thread, the method t.getState()
prints:
NEW
4
3.2. Runnable
Once we’ve created a new thread and called the start() method on it, it’s
moved from the NEW to RUNNABLE state. Threads in this state are either
running or ready to run, but they’re waiting for resource allocation from
the system.
For example, let’s add the t.start() method to our previous code and try to
access its current state:
RUNNABLE
Note that in this example, it’s not always guaranteed that by the time our
control reaches t.getState(), it will still be in the RUNNABLE state.
5
3.3. Blocked
A thread is in the BLOCKED state when it’s not currently eligible to run. It
enters this state while waiting for a monitor lock and trying to access a
section of code that’s locked by some other thread.
6
In this code:
Being in this state, if we call t2.getState(), we’ll get the output as:
BLOCKED
3.4. Waiting
A thread is in the WAITING state when it’s waiting for some other thread to
perform a particular action. According to JavaDocs, any thread can enter
this state by calling any one of the following three methods:
object.wait()
thread.join() or
LockSupport.park()
Note that in wait() and join(), we don’t define any timeout period, as that
scenario is covered in the next section.
7
1. public class ingState implements Runnable {
2. public static Thread t1;
3.
4. public static void main(String[] args) {
5. t1 = new Thread(new ingState());
6. t1.start();
7. }
8.
9. public void run() {
10. Thread t2 = new Thread(new DemoingStateRunnable());
11. t2.start();
12.
13. try {
14. t2.join();
15. } catch (InterruptedException e) {
16. Thread.currentThread().interrupt();
17. e.printStackTrace();
18. }
19. }
20. }
21.
22. class DemoingStateRunnable implements Runnable {
23. public void run() {
24. try {
25. Thread.sleep(1000);
26. } catch (InterruptedException e) {
27. Thread.currentThread().interrupt();
28. e.printStackTrace();
29. }
30.
31. System.out.println(ingState.t1.getState());
32. }
33. }
WAITING
8
3.5. Timed Waiting
thread.sleep(long millis)
(int timeout) or (int timeout, int nanos)
thread.join(long millis)
LockSupport.parkNanos
LockSupport.parkUntil
To read more about the differences between wait() and sleep() in Java, look
at this dedicated article here.
9
Here, we’ve created and started a thread t1, which is entered into the sleep
state with a timeout period of 5 seconds. The output here will be:
TIMED_WAITING
3.6. Terminated
This is the state of a dead thread. It’s in the TERMINATED state when it has
either finished execution or was terminated abnormally.
Here, while we’ve started thread t1, the very next statement,
Thread.sleep(1000), gives enough time for t1 to complete, so this program
gives us the output:
TERMINATED
10
In addition to the thread state, we can check the isAlive() method to
determine whether the thread is alive. For instance, if we call the isAlive()
method on this thread, we’ll get:
Assert.assertFalse(t1.isAlive());
It returns false. Simply put, a thread is alive if, and only if, it’s been started
and hasn’t yet died.
11
4. Conclusion
In this chapter, we learned about the life cycle of a thread in Java. We looked
at all six states defined by the Thread.State enum, and reproduced them
with quick examples.
Although the code snippets will give the same output in almost every
machine, in some exceptional cases, we may get different outputs, as the
exact behavior of Thread Scheduler can’t be determined.
13
1. Introduction
In this chapter, we’ll explore different ways to start a thread and execute
parallel tasks.
To learn more about the details of threads, read our chapter on the Life
Cycle of a Thread in Java.
14
2. The Basics of Running a Thread
We can easily write some logic that runs in a parallel thread by using the
Thread framework.
Next, we can write a second class to initialize and start our thread:
We should call the start() method on threads in the NEW state (the
equivalent of not started). Otherwise, Java will throw an instance of
IllegalThreadStateException exception.
15
Now, let’s assume we need to start multiple threads:
Our code still looks quite simple and very similar to the examples we can
find online.
We can write our code for all of these case scenarios if we want to, but
why should we reinvent the wheel.
16
3. The ExecutorService Framework
For more details about the ExecutorService, please read our Guide to the
Java ExecutorService.
17
4. Starting a Task With Executors
There are two methods we can use: execute, which returns nothing, and
submit, which returns a Future encapsulating the computation’s result.
For more information about Futures, please read our Guide to java.util.
concurrent.Future.
18
5. Starting a Task With CompletableFutures
To retrieve the final result from a Future object, we can use the get method
available in the object, but this would block the parent thread until the end
of the computation.
Alternatively, we could avoid the block by adding more logic to our task, but
we’d have to increase the complexity of our code.
19
6. Running Delayed or Periodic Tasks
When working with complex web applications, we may need to run tasks
at specific times, maybe even regularly.
Java has a few tools that can help us to run delayed or recurring
operations:
• java.util.Timer
• java.util.concurrent.ScheduledThreadPoolExecutor
6.1. Timer
Let’s see what the code looks like if we want to run a task after one second
of delay:
20
Now, let’s add a recurring schedule:
This time, the task will run after the delay specified, and it’ll be recurrent
after the period of time has passed.
6.2. ScheduledThreadPoolExecutor
1. ScheduledFuture<Object> resultFuture
2. = executorService.scheduleAtFixedRate(runnableTask, 100, 450,
3. TimeUnit.MILLISECONDS);
The code above will execute a task after an initial delay of 100
milliseconds, and after that, it’ll execute the same task every 450
milliseconds.
If the processor can’t finish processing the task in time before the next
occurrence, the ScheduledExecutorService will until the current task is
completed before starting the next.
21
6.3. Which Tool is Better?
If we run the examples above, the computation’s result looks the same.
Timer:
ScheduledThreadPoolExecutor:
22
7. Difference Between Future and ScheduledFuture
23
8. Conclusions
24
3. wait and notify() Methods in Java
25
1. Overview
Then, we’ll develop a simple application where we’ll deal with concurrency
issues, with the goal of better understanding wait() and notify().
26
2. Thread Synchronization in Java
One tool we can use to coordinate the actions of multiple threads in Java is
guarded blocks. Such blocks keep a check for a particular condition before
resuming the execution.
We can better understand this through the following diagram depicting the
life cycle of a Thread:
Please note that there are many ways of controlling this life cycle. However,
in this article, we will focus only on wait() and notify().
27
3. The wait() Method
Simply put, calling wait() forces the current thread to until some other
thread invokes notify() or notifyAll() on the same object.
For this, the current thread must own the object’s monitor. According to
Javadocs, this can happen in the following ways:
• when we’ve executed the synchronized instance method for the given
object
• when we’ve executed the body of a synchronized block on the given
object
• by executing synchronized static methods for objects of type Class
Note that only one active thread can own an object’s monitor at a time.
This wait() method comes with three overloaded signatures. Let’s have a
look at these.
3.1. wait()
The wait() method causes the current thread to indefinitely until another
thread either invokes notify() for this object or notifyAll().
We can specify a timeout using this method, after which a thread will be
woken up automatically. A thread can be woken up before reaching the
timeout using notify() or notifyAll().
28
3.3. wait(long timeout, int nanos)
This is yet another signature providing the same functionality. The only
difference here is that we can provide higher precision.
29
4. notify() and notifyAll()
We use the notify() method to wake up threads waiting for access to this
object’s monitor.
4.1. notify()
For all threads waiting on this object’s monitor (by using any one of the wait()
methods), the method notify() notifies any one of them to wake up arbitrarily.
The choice of which thread to wake is nondeterministic and depends on
implementation.
4.2. notifyAll()
This method simply wakes all threads waiting on this object’s monitor.
The awakened threads will compete in the usual manner, like any other
thread trying to synchronize on this object.
30
5. Sender-Receiver Synchronization Problem
First, Let’s create a Data class consisting of the data packet sent from
the Sender to the Receiver. We’ll use wait() and notifyAll() to set up
synchronization between them:
31
1. public class Data {
2. private String packet;
3.
4. // True if receiver should
5. // False if sender should
6. private boolean transfer = true;
7.
8. public synchronized String receive() {
9. while (transfer) {
10. try {
11. ();
12. } catch (InterruptedException e) {
13. Thread.currentThread().interrupt();
14. System.err.println(“Thread Interrupted”);
15. }
16. }
17. transfer = true;
18.
19. String returnPacket = packet;
20. notifyAll();
21. return returnPacket;
21. }
22.
23. public synchronized void send(String packet) {
24. while (!transfer) {
25. try {
26. ();
27. } catch (InterruptedException e) {
28. Thread.currentThread().interrupt();
29. System.err.println(“Thread Interrupted”);
30. }
31. }
32. transfer = false;
33.
34. this.packet = packet;
35. notifyAll();
36. }
37. }
32
Let’s break down what’s going on here:
• The packet variable denotes the data that’s being transferred over the
network.
• We have a boolean variable transfer, which the Sender and Receiver will
use for synchronization:
• If this variable is true, the Receiver should for the Sender to send the
message.
• If it’s false, the Sender should for the Receiver to receive the message.
• The Sender uses the send() method to send data to the Receiver:
• If the transfer is false, we’ll by calling wait() on this thread.
• But when it’s true, we toggle the status, set our message, and call notifyAll()
to wake up other threads to specify that a significant event has occurred,
and they can check if they can continue execution.
• Similarly, the Receiver will use the receive() method:
• If the transfer was set to false by the Sender, only then will it proceed;
otherwise, we’ll call wait() on this thread.
• When the condition is met, we toggle the status, notify all waiting threads
to wake up, and return the received data packet.
33
We’ll now create the Sender and Receiver and implement the Runnable
interface on both so that their instances can be executed by a thread.
• We’re creating some random data packets that will be sent across the
network in packets[] array.
• For each packet, we’re merely calling send().
• Then, we call Thread.sleep() with random intervals to mimic heavy
server-side processing.
34
1. public class Receiver implements Runnable {
2. private Data load;
3.
4. // standard constructors
5.
public void run() {
6. for(String receivedMessage = load.receive();
7. !”End”.equals(receivedMessage);
8. receivedMessage = load.receive()) {
9.
10. System.out.println(receivedMessage);
11.
12. //Thread.sleep() to mimic heavy server-side processing
13. try {
14. Thread.sleep(ThreadLocalRandom.current().nextInt(1000,
15. 5000));
} catch (InterruptedException e) {
16.
Thread.currentThread().interrupt();
17. System.err.println(“Thread Interrupted”);
18. }
19. }
20. }
21. }
Here, we’re simply calling load.receive() in the loop until we get the last
“End” data packet. Let’s now see this application in action:
First packet
Second packet
Third packet
Fourth packet
And here we are. We’ve received all data packets in the right, sequential
order and successfully established the correct communication between
our sender and receiver.
35
6. Conclusion
Before we close, it’s worth mentioning that all these low-level APIs, such
as wait(), notify() and notifyAll(), are traditional methods that work well.
Still, higher-level mechanisms are often simpler and better, such as Java’s
native Lock and Condition interfaces (available in java.util.concurrent.locks
package).
36
4. The Thread.join() Method in Java
37
1. Overview
In this chapter, we’ll discuss the different join() methods in the Thread class.
We’ll go into the details of these methods and some example codes.
Like the wait() and notify() methods, join() is another mechanism of inter-
thread synchronization.
You can quickly look at this chapter to read more about wait() and notify().
38
2. The Thread.join() Method
When we invoke the join() method on a thread, the calling thread goes into
a waiting state. It remains in a waiting state until the referenced thread
terminates.
39
We should expect results similar to the following when executing the code:
The join() method may also return if the referenced thread is interrupted.
In this case, the method throws an InterruptedException.
40
3. Thread.join() Methods With Timeout
The join() method will keep waiting if the referenced thread is blocked or
takes too long to process. This can become an issue, as the calling thread will
become non-responsive. To handle these situations, we’ll use overloaded
versions of the join() method that allows us to specify a timeout period.
There are two timed versions that overload the join() method:
1. @Test
2. public void givenStartedThread_whenTimedJoinCalled_
3. sUntilTimedout()
4. throws InterruptedException {
5. Thread t3 = new SampleThread(10);
6. t3.start();
7. t3.join(1000);
8. assertTrue(t3.isAlive());
9. }
In this case, the calling thread s roughly 1 second for the thread t3 to finish.
If the thread t3 doesn’t finish in this period, the join() method returns control
to the calling method.
Timed join() is dependent on the OS for timing. So, we can’t assume that
join() will exactly as long as specified.
41
4. Thread.join() Methods and Synchronization
This means that when a thread t1 calls t2.join(), all changes done by t2
are visible in t1 on return. However, if we don’t invoke join() or use other
synchronization mechanisms, then we don’t have any guarantee that
changes in the other thread will be visible to the current thread, even if the
other thread has been completed.
Therefore, even though the join() method calls to a thread in the terminated
state returns immediately, we still need to call it in some situations.
To properly synchronize the above code, we can add timed t4.join() inside
the loop or use another synchronization mechanism.
42
5. Conclusion
43
5. Guide to the Synchronized Keyword in Java
44
1. Overview
In this chapter, we’ll learn how to use the synchronized block in Java.
45
2. Why Synchronization?
Let’s consider a typical race condition where we calculate the sum, and
multiple threads execute the calculate() method:
If we executed this serially, the expected output would be 1000, but our
multi-threaded execution fails almost every time with an inconsistent
actual output:
46
Of course, we don’t find this result unexpected.
A simple way to avoid the race condition is to make the operation thread-
safe using the synchronized keyword.
47
3. The Synchronized Keyword
• Instance methods
• Static methods
• Code blocks
Notice that once we synchronize the method, the test case passes with the
actual output as 1000:
48
Instance methods are synchronized over the instance of the class owning
the method, which means only one thread per instance of the class can
execute this method.
These methods are synchronized on the Class object associated with the
class. Since only one Class object exists per JVM per class, only one thread
can execute inside a static synchronized method per class, irrespective of
the number of instances it has.
1. @Test
2. public void givenMultiThread_whenStaticSyncMethod() {
3. ExecutorService service = Executors.newCachedThreadPool();
4.
5. IntStream.range(0, 1000)
6. .forEach(count ->
7. service.submit(SynchronizedMethods::syncStaticCalculate));
8. service.aTermination(100, TimeUnit.MILLISECONDS);
9.
10. assertEquals(1000, SynchronizedMethods.staticSum);
11. }
49
1. public void performSynchronisedTask() {
2. synchronized (this) {
3. setCount(getCount()+1);
4. }
5. }
1. @Test
2. public void givenMultiThread_whenBlockSync() {
3. ExecutorService service = Executors.newFixedThreadPool(3);
4. SynchronizedBlocks synchronizedBlocks = new
5. SynchronizedBlocks();
6.
7. IntStream.range(0, 1000)
8. .forEach(count ->
9. service.
10. submit(synchronizedBlocks::performSynchronisedTask));
11. service.aTermination(100, TimeUnit.MILLISECONDS);
12.
13. assertEquals(1000, synchronizedBlocks.getCount());
14. }
Notice that we passed the parameter this to the synchronized block. This
is the monitor object. The code inside the block gets synchronized on the
monitor object. Simply put, only one thread per monitor object can execute
inside that code block.
If the method was static, we would pass the class name in place of the
object reference, and the class would be a monitor for synchronization of
the block:
50
Let’s test the block inside the static method:
1. @Test
2. public void givenMultiThread_whenStaticSyncBlock() {
3. ExecutorService service = Executors.newCachedThreadPool();
4.
5. IntStream.range(0, 1000)
6. .forEach(count ->
7. service.
8. submit(SynchronizedBlocks::performStaticSyncTask));
9. service.aTermination(100, TimeUnit.MILLISECONDS);
10.
11. assertEquals(1000, SynchronizedBlocks.getStaticCount());
12. }
3.4. Reentrancy
The lock behind the synchronized methods and blocks is a reentrant. This
means the current thread can acquire the same synchronized lock over and
over again while holding it:
51
4. Conclusion
We also learned how a race condition can impact our application and how
synchronization helps us avoid that. For more about thread safety using
locks in Java, refer to our java.util.concurrent.Locks article.
52
6. Guide to the Volatile Keyword in Java
53
1. Overview
54
2. Shared Multiprocessor Architecture
As CPUs can carry many instructions per second, fetching from RAM isn’t
ideal. To improve this situation, processors use tricks like Out of Order
Execution, Branch Prediction, Speculative Execution, and Caching.
As different cores execute more instructions and manipulate more data, they
fill their caches with more relevant data and instructions. This will improve
the overall performance, at the expense of introducing cache coherence
challenges.
We should think twice about what happens when one thread updates a
cached value.
55
3. Cache Coherence Challenges
The TaskRunner class maintains two simple variables. Its main method
creates another thread that spins on the ready variable as long as it’s false.
When the variable becomes true, the thread prints the number variable.
When “ready=true” is finally written in the cache, the Reader thread will
execute and “get back” its resources.
56
Many may expect this program to print 42 after a short delay; however, the
delay may be much longer. It may even hang forever or print zero.
The cause of these anomalies is the lack of proper memory visibility and
reordering. Let’s evaluate them in more detail.
This simple example has two application threads: the main and reader
threads. Let’s imagine a scenario in which the OS schedules those threads
on two different CPU cores, where:
• The main thread has its copy of ready and number variables in its core
cache.
• The reader thread ends up with its copies too.
• The main thread updates the cached values.
Most modern processors write requests that won’t be applied right after
they’re issued. Processors tend to queue those writes in a special write
buffer. After a while, they’ll apply those writes to the main memory all at
once.
With all that being said, when the main thread updates the number and
ready variables, there’s no guarantee about what the reader thread
may see. In other words, the reader thread may see the updated value
immediately, with some delay, or never at all.
3.2. Reordering
To make matters even worse, the reader thread may see those writes in
an order other than the actual program order. For instance, since we first
update the number variable:
57
1. public static void main(String[] args) {
2. new Reader().start();
3. number = 42;
4. ready = true;
5. }
We may expect the reader thread to print 42, but it’s actually possible to
see zero as the printed value.
• The processor may flush its write buffer in an order other than the program
order.
• The processor may apply an out-of-order execution technique.
• The JIT compiler may optimize via reordering.
58
4. volatile Memory Order
This way, we can communicate with runtime and processor to not reorder
any instruction involving the volatile variable. Also, processors understand
that they should immediately flush any updates to these variables:
59
5. volatile and Thread Synchronization
volatile is quite a useful keyword because it can help ensure the visibility
aspect of the data change without providing mutual exclusion. Thus, it’s
useful where we’re ok with multiple threads executing a block of code in
parallel, but we need to ensure the visibility property.
60
6. Happens-Before Ordering
The memory visibility effects of volatile variables extend beyond the volatile
variables themselves.
6.1. Piggybacking
61
Anything prior to writing true to the ready variable is visible to anything after
reading the ready variable. Therefore, the number variable piggybacks on the
memory visibility enforced by the ready variable. Simply put, even though
it’s not a volatile variable, it’s exhibiting a volatile behavior.
Using these semantics, we can define only a few variables in our class as
volatile and optimize the visibility guarantee.
62
7. Conclusion
63
7. A Guide to the Java ExecutorService
64
1. Overview
65
2. Instantiating ExecutorService
For example, the following line of code will create a thread pool with 10
threads:
1. ExecutorService executorService =
2. new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
3. new LinkedBlockingQueue<Runnable>());
You may notice that the code above is very similar to the source code of
the factory method newSingleThreadExecutor(). For most cases, a detailed
manual configuration isn’t necessary.
66
3. Assigning Tasks to the ExecutorService
The execute() method is void and doesn’t give any possibility to get the
result of a task’s execution or to check the task’s status (is it running):
1. executorService.execute(runnableTask);
1. Future<String> future =
2. executorService.submit(callableTask);
67
invokeAny() assigns a collection of tasks to an ExecutorService, causing
each to run, and returns the result of successful execution of one task (if
there was a successful execution):
Before going further, we need to discuss two more items: shutting down
an ExecutorService and dealing with Future return types.
68
4. Shutting Down an ExecutorService
In some cases, this is very helpful, such as when an app needs to process
tasks that appear on an irregular basis or the task quantity isn’t known at
compile time.
On the other hand, an app could reach its end but not be stopped because
a waiting ExecutorService will cause the JVM to keep running.
This method returns a list of tasks that are waiting to be processed. It’s up
to the developer to decide what to do with these tasks.
69
1. executorService.shutdown();
2. try {
3. if (!executorService.aTermination(800, TimeUnit.MILLISECONDS))
4. {
5. executorService.shutdownNow();
6. }
7. } catch (InterruptedException e) {
8. executorService.shutdownNow();
9. }
With this approach, the ExecutorService will first stop taking new tasks
and then up to a specified time for all tasks to be completed. If that time
expires, the execution is stopped immediately.
70
5. The Future Interface
The Future interface provides a special blocking method, get(), which returns
an actual result of the Callable task’s execution, or null in the case of a
Runnable task:
Calling the get() method while the task is still running will cause execution to
block until the task is properly executed and the result is available.
If the execution period is longer than specified (in this case, 200
milliseconds), a TimeoutException will be thrown.
We can use the isDone() method to check whether the assigned task is
already processed.
The Future interface also allows for cancelling task execution with the
cancel() method and checking the cancellation with the isCancelled()
method:
71
6. The ScheduledExecutorService Interface
To schedule a single task’s execution after a fixed delay, we can use the
scheduled() method of the ScheduledExecutorService.
1. Future<String> resultFuture =
2. executorService.schedule(callableTask, 1, TimeUnit.SECONDS);
The following block of code will run a task after an initial delay of 100
milliseconds; after that, it will run the same task every 450 milliseconds:
72
If it’s necessary to have a fixed length delay between iterations of the task,
scheduleWithFixedDelay() should be used.
73
7. ExecutorService vs Fork/Join
However, this isn’t always the right decision. Despite the simplicity and
frequent performance gains associated with fork/join, it reduces developer
control over concurrent execution.
74
8. Conclusion
Wrong thread-pool capacity while using fixed length thread pool: It’s
very important to determine how many threads the application will need to
run tasks efficiently. A thread pool that is too large will cause unnecessary
overhead just to create threads that will mostly be in the waiting mode. Too
few can make an application seem unresponsive because of long waiting
periods for tasks in the queue.
As always, the code for this chapter is available in the GitHub repository.
75
8. Guide To CompletableFuture
76
1. Introduction
77
2. Asynchronous Computation in Java
78
3. Using CompletableFuture as a Simple Future
For example, we can create an instance of this class with a no-arg constructor
to represent some future result, hand it out to the consumers, and complete
it at some time in the future using the complete method. The consumers may
use the get method to block the current thread until this result is provided.
To spin off the computation, we use the Executor API. This method of creating
and completing a CompletableFuture can be used with any concurrency
mechanism or API, including raw threads.
79
We simply call the method, receive the Future instance, and call the get
method on it when we’re ready to block for the result.
Also, we can observe that the get method throws some checked exceptions,
namely ExecutionException (encapsulating an exception that occurred
during a computation) and InterruptedException (an exception signifying
that a thread was interrupted either before or during an activity):
1. Future<String> completableFuture =
2. CompletableFuture.completedFuture(“Hello”);
3.
4. // ...
5.
6. String result = completableFuture.get();
7. assertEquals(“Hello”, result);
80
4. CompletableFuture With Encapsulated Computation Logic
Runnable and Supplier are functional interfaces that allow passing their
instances as lambda expressions thanks to the new Java 8 feature.
The Runnable interface is the same old interface used in threads and
doesn’t allow us to return a value.
1. CompletableFuture<String> future
2. = CompletableFuture.supplyAsync(() -> “Hello”);
3.
4. // ...
5.
6. assertEquals(“Hello”, future.get());
5. Processing Results of Asynchronous Computations
1. CompletableFuture<String> completableFuture
webClient.get()
2. .uri(“/products”)
= CompletableFuture.supplyAsync(() -> “Hello”);
3. .retrieve()
4. CompletableFuture<String>
.bodyToMono(String.class)future = completableFuture
5. .block();
.thenApply(s -> s + “ World”);
6.
7. verifyCalledUrl(“/products”);
assertEquals(“Hello World”, future.get());
If we don’t need to return a value down the Future chain, we can use an
instance of the Consumer functional interface. Its single method takes a
parameter and returns void.
1. CompletableFuture<String> completableFuture
webClient.get()
2. .uri(“/products”)
= CompletableFuture.supplyAsync(() -> “Hello”);
3. .retrieve()
4. CompletableFuture<Void>
.bodyToMono(String.class)
future = completableFuture
5. .block();
.thenAccept(s -> System.out.println(“Computation returned: “ +
6. s));
7. verifyCalledUrl(“/products”);
8. future.get();
Finally, if we neither need the value of the computation nor want to return
some value at the end of the chain, then we can pass a Runnable lambda to
the thenRun method. In the following example, we simply print a line in the
console after calling the future.get():
82
1. CompletableFuture<String> completableFuture
webClient.get()
2. .uri(“/products”)
= CompletableFuture.supplyAsync(() -> “Hello”);
3. .retrieve()
4. CompletableFuture<Void>
.bodyToMono(String.class)
future = completableFuture
5. .block();
.thenRun(() -> System.out.println(“Computation finished.”));
6.
7. verifyCalledUrl(“/products”);
future.get();
83
6. Combining Futures
1. CompletableFuture<String> completableFuture
webClient.get()
2. .uri(“/products”)
= CompletableFuture.supplyAsync(() -> “Hello”)
3. .retrieve()
.thenCompose(s -> CompletableFuture.supplyAsync(() -> s + “
4. World”));
.bodyToMono(String.class)
5. .block();
6. assertEquals(“Hello World”, completableFuture.get());
7. verifyCalledUrl(“/products”);
Both methods receive a function and apply it to the computation result, but
the thenCompose (flatMap) method receives a function that returns another
object of the same type. This functional structure allows us to compose the
instances of these classes as building blocks.
84
1. CompletableFuture<String> completableFuture
2. = CompletableFuture.supplyAsync(() -> “Hello”)
3. .thenCombine(CompletableFuture.supplyAsync(
4. () -> “ World”), (s1, s2) -> s1 + s2));
5.
6. assertEquals(“Hello World”, completableFuture.get());
85
7. Difference Between thenApply() and thenCompose()
7.1. thenApply()
We can use this method to work with the result of the previous call.
However, a key point to remember is that the return type will be a
combination of all the calls.
7.2. thenCompose()
86
8. Running Multiple Futures in Parallel
1. CompletableFuture<String> future1
2. = CompletableFuture.supplyAsync(() -> “Hello”);
3. CompletableFuture<String> future2
4. = CompletableFuture.supplyAsync(() -> “Beautiful”);
5. CompletableFuture<String> future3
6. = CompletableFuture.supplyAsync(() -> “World”);
7.
8. CompletableFuture<Void> combinedFuture
9. = CompletableFuture.allOf(future1, future2, future3);
10. // ...
11.
12. combinedFuture.get();
13.
14. assertTrue(future1.isDone());
15. assertTrue(future2.isDone());
16. assertTrue(future3.isDone());
87
9. Handling Errors
In the following example, we’ll use the handle method to provide a default
value when the asynchronous computation of a greeting was finished with
an error because no name was provided:
88
1. CompletableFuture<String> completableFuture = new
2. CompletableFuture<>();
3.
4. // ...
5.
6. completableFuture.completeExceptionally(
7. new RuntimeException(“Calculation failed!”));
8.
9. // ...
10.
11. completableFuture.get(); // ExecutionException
89
10. Async Methods
Most methods of the fluent API in the CompletableFuture class have two
additional variants with the Async postfix. These methods are usually
intended for running a corresponding execution step in another thread.
The methods without the Async postfix run the next execution stage using a
calling thread. In contrast, the Async method without the Executor argument
runs a step using the common fork/join pool implementation of Executor,
accessed with the ForkJoinPool.commonPool(), as long as parallelism > 1.
Finally, the Async method with an Executor argument runs a step using the
passed Executor.
1. CompletableFuture<String> completableFuture
2. = CompletableFuture.supplyAsync(() -> “Hello”);
3.
4. CompletableFuture<String> future = completableFuture
5. .thenApplyAsync(s -> s + “ World”);
6.
7. assertEquals(“Hello World”, future.get());
90
11. JDK 9 CompletableFuture API
• Executor defaultExecutor()
• CompletableFuture<U> newIncompleteFuture()
• CompletableFuture<T> copy()
• CompletionStage<T> minimalCompletionStage()
• CompletableFuture<T> completeAsync(Supplier<? extends T> supplier,
Executor executor)
• CompletableFuture<T> completeAsync(Supplier<? extends T> supplier)
• CompletableFuture<T> orTimeout(long timeout, TimeUnit unit)
• CompletableFuture<T> completeOnTimeout(T value, long timeout,
TimeUnit unit)
Finally, to address timeout, Java 9 has introduced two more new functions:
• orTimeout()
• completeOnTimeout()
Here’s the detailed article for further reading: Java 9 CompletableFuture API
Improvements.
91
12. Conclusion
In this chapter, we described the methods and typical use cases of the
CompletableFuture class.
92