Mastering Java CompletableFuture
Introduction to Concurrency
What Is Concurrency?
Imagine you're making breakfast. You could do everything one step at a time: put bread in the toaster, wait for it to toast, take it out, then put a pan on the stove, wait for it to heat up, crack an egg, and so on. This is a sequential process. It works, but it's slow.
Alternatively, you could start the toast, and while it's toasting, you heat the pan and start cooking the egg. This is concurrency. It’s the art of managing multiple tasks that are in progress at the same time. These tasks might run on a single processor, switching back and forth, or truly in parallel on multiple processor cores.
Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
In modern software, concurrency is not a luxury; it's a necessity. Your computer has multiple CPU cores, and concurrent programming is how we put them all to work. It’s what keeps an application's user interface responsive while it downloads a large file in the background. It's how a web server can handle requests from thousands of users simultaneously.
Benefits and Bumps in the Road
Writing concurrent programs offers significant advantages. The most obvious is improved performance. By breaking a large task into smaller pieces that can run at the same time, we can complete the work much faster, especially on multi-core systems.
Another key benefit is responsiveness. In a desktop or mobile application, a long-running task like processing an image shouldn't freeze the entire program. By running that task in the background, the user interface remains active and responsive.
However, concurrency introduces its own set of challenges. It makes code harder to reason about. When tasks run out of sequence, bugs can appear that are difficult to find and fix.
Race Condition
noun
A situation where the outcome of a program depends on the unpredictable sequence or timing of threads accessing shared data.
A classic problem is the race condition. Imagine two operations trying to update your bank balance at the same time. One is depositing $100, and the other is withdrawing $50. If both read the initial balance of $200 simultaneously, one might calculate $300 and the other $150. Depending on which one writes its result back last, your final balance could be wrong.
Another common issue is deadlock. This happens when two or more tasks are stuck waiting for each other to release a resource. It's like two people meeting in a narrow hallway, where each is waiting for the other to step aside. Neither can move, and the program grinds to a halt.
Java's Approach to Concurrency
Java was designed with concurrency in mind from the very beginning. The fundamental unit of execution in Java's concurrency model is the thread.
A thread is a lightweight process, a single path of execution within a larger program. A Java application runs as a single process, but it can have multiple threads.
Every Java program starts with a main thread, the one that executes the main method. This main thread can then create and start other threads. These threads all live within the same process and share the same memory space, which is both powerful and dangerous. It's powerful because they can easily share information, but dangerous because it's where issues like race conditions arise.
The Rules of the Road
Because threads share memory, Java needs a strict set of rules to ensure that operations in one thread are visible to others in a predictable way. These rules are known as the Java Memory Model (JMM).
The JMM addresses two fundamental problems:
-
Visibility: For performance, each CPU core has its own memory cache, which is faster than main memory. A thread running on one core might update a value in its local cache, but that change might not be immediately written back to main memory. The JMM defines when and how changes made by one thread become visible to others.
-
Ordering: To optimize performance, compilers and processors are allowed to reorder instructions. In a single-threaded program, you'd never notice this. But in a multi-threaded program, this reordering can cause bizarre and incorrect behaviour. The JMM specifies the ordering guarantees that prevent this from breaking your code.
The Java Memory Model is a contract between the Java Virtual Machine and your code, ensuring that your concurrent program behaves predictably across different hardware architectures.
You don't need to memorise the entire JMM specification. Instead, you use Java's concurrency utilities, like the synchronized and volatile keywords, which are built to enforce the JMM's rules for you. Understanding that these rules exist is the first step toward writing correct and robust concurrent applications.
What is the primary advantage of making breakfast concurrently instead of sequentially, as described in the analogy?
A race condition occurs when...