1. Advanced threading patterns
Welcome to this video on advanced threading patterns in Java. We'll explore how to manage threads more efficiently using the `ExecutorService` interface and implement asynchronous operations with `CompletableFuture`. These tools help us design responsive applications capable of handling multiple operations simultaneously.
2. Thread pool fundamentals
Creating a new thread for every task can be expensive in terms of system resources. Until now, we created `Threads` manually, which made us responsible for efficiently managing resources.
Thread pools can be used as a way to let Java help with that, by reusing threads efficiently and controlling the number of concurrent threads. This is done through `ExecutorService`.
3. Creating thread pools
Java provides several factory methods to create different types of thread pools. A fixed thread pool maintains a specific number of threads regardless of the workload, which is ideal for limiting resource usage, as we know we'll never go above that number of threads.
A cached thread pool adjusts based on demand, creating new threads as needed and reusing idle ones. A single-threaded executor uses one thread to process tasks sequentially, which is useful for tasks that shouldn't run concurrently.
4. Submitting tasks
After creating an executor, we can submit tasks using the `execute()` or `submit()` methods.
The `execute()` method accepts tasks with no return value, like a print statement.
The `submit()` method accepts tasks that return values - those tasks are called `Callables`. This method returns a `Future` object representing the result of the computation. We can call the `get()` method on `Future` to retrieve the result, though be aware that this will block our main thread until the task completes.
5. Shutting down executors
Properly shutting down executors is crucial to prevent resource leaks. The `shutdown()` method initiates an orderly shutdown, allowing previously submitted tasks to execute but not accepting new ones. If we need to wait for all tasks to complete, we can use `awaitTermination()` with a specified timeout. If we need to stop immediately, `shutdownNow()` attempts to cancel all running tasks.
6. The completable future
Now let's turn to `CompletableFuture`, which represents a significant improvement over traditional `Futures`. Introduced in Java 8, `CompletableFuture` offers a modern approach to asynchronous programming. Unlike regular `Futures`, a `CompletableFuture` can be completed manually or through a `Function`. Its most powerful feature is the ability to chain operations, allowing us to express complex asynchronous workflows clearly.
7. Creating completable futures
We can create `CompletableFutures` in several ways. To run a task asynchronously that doesn't return anything, we use `runAsync()`. For tasks that produce a result we can use `supplyAsync()`. Both asynchronous methods accept an optional `Executor` parameter, allowing us to specify which thread pool should run the task.
8. Chaining operations
What makes `CompletableFuture` truly powerful is its ability to chain operations. Here, we fetch user data asynchronously, then transform it with `thenApply()`, which runs in the same thread when the previous stage completes. If any stage fails, the `exceptionally` method provides error handling. Finally, `thenAccept()` lets us consume the final result when it's ready.
9. Let's practice!
Now, let's apply what we learned in some exercises!