In this blog I’m going to talk about Futures in Java. Futures are used when threads in a system are being blocked. This blocking is due to computations that take a long period of time to execute, the most common sources of blocking are accessing a database, accessing a cloud service and reading from a file. Blocking calls also prevents the code following the blocking from executing until the blocking is completed.
To solve this blocking problem, we need to isolate blocking requests in their own thread pool. This allows the fast operations to execute in a normal manner without any delay by blocking requests. Here’s where we use Futures; Futures allow us to isolate blocking operations in a separate thread, A callback mechanism then handles the result of this future. Meaning the value of this future isn’t available yet but will be available at some point when its execution is complete.
How To Use Futures
CompletableFuture is the Java class representing futures, CompletableFuture is the only implementation of CompletionStage. When the value of the CompletableFuture is available, it will either be the value of the future, or an exception that occurred while resolving the value. The first way of creating a CompletableFuture is using completedFuture() method.
CompletableFuture<MyClass> future = CompletableFuture.completedFuture(new MyClass(...));
This method however is blocking so it takes a value that is already known and doesn’t perform the operation in a separate thread. The more conventional method is supplyAsync() which takes a lambda function that will return the value.
CompletableFuture<MyClass> future = CompletableFuture.supplyAsync(()->new MyClass(...));
If at some point we want to get access to the value of that future, we can use the join() or get() methods which will block the current thread until a value is retrieved. So it’s best practice to avoid using them.
Instead of waiting for a future to complete, we can use transformations to handle the result of the future. This can only work as long as the pattern doesn’t break (no blocking). We transform futures using methods like thenApply() which takes a lambda function that transforms the value of the future when it’s completed.
When we isolate blocking operations into separate threads, this creates a problem if those threads are part of the same pool, which is why we need to use executors. Executors handle the management of these threads. When we don’t provide an executor for the future, a default is used which creates competition for resources. We need to leave this default executor for the fast operations and isolate blocking operations to their own executor.
Futures are extremely powerful. However, you need to use them properly without breaking the pattern or using blocking calls. This is just a brief blog about futures, I encourage you to read more about this interesting topic.