Hello! In our last lesson, we learned how to create and run multiple threads, opening the door to concurrent execution in Java. As we concluded, this power comes with its own set of challenges. When multiple threads interact with the same data, the seemingly orderly world of sequential programming can quickly become chaotic and unpredictable.
This lesson tackles that very chaos head-on. We will explore two of the most fundamental concepts in concurrent programming: race conditions and critical sections. Understanding these concepts is not just academic; it's an essential prerequisite for building the robust, reliable backend systems you aim to create. By the end of this lesson, you will be able to identify where and why concurrency can cause errors in your applications.
1. The Problem: When Threads Collide
Imagine you have a single bank account with a balance of ₹10,000. You and a friend both have debit cards for this account and go to different ATMs at the exact same moment to withdraw ₹7,000. In a sequential world, one transaction would complete before the other starts. But in a concurrent world, what might happen?
- Thread 1 (You): Reads the balance (₹10,000). The check passes.
- Thread 2 (Friend): Reads the balance (₹10,000) before you can update it. The check passes.
- Thread 1 (You): Dispenses ₹7,000 and updates the balance to ₹3,000.
- Thread 2 (Friend): Dispenses ₹7,000 and updates the balance to ₹3,000.
The final balance is ₹3,000, but ₹14,000 has been withdrawn. The bank has lost money because both threads "raced" to act on old information. This scenario is called a race condition.
This flowchart illustrates the same problem. Both processes read the initial balance of 100. Process 2 performs a subtraction and updates the balance to 90. Then, Process 1, which is still working with the old value of 100, performs an addition and updates the balance to 110, overwriting the change made by Process 2. The final result is incorrect.

A race condition occurs when a program's correctness depends on the unpredictable sequence or timing of threads being scheduled by the operating system. The result becomes non-deterministic, meaning you might get a different, incorrect answer every time you run the code.
2. A Race Condition in Java Code
This isn't just a theoretical problem. It happens frequently in code. Let's consider a simple counter.
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
If we create a single Counter object and share it between two threads, each calling increment() one million times, we'd expect the final count to be two million. Let's see what actually happens.
Race Conditions in Java Multithreading
This video from Jakob Jenkov provides an excellent practical demonstration of a race condition using a Counter class very similar to the one above.
Watch from the beginning of the example at the read-modify-write problem. Pay close attention to the code setup and, more importantly, the inconsistent and incorrect final count printed at the end.
As you saw in the video, the final count is not two million, and it varies with each run. Why does this happen? The reason is that the seemingly simple operation count++ is not atomic. It's not one indivisible instruction. The JVM breaks it down into three distinct steps:
- Read: Get the current value of
count. - Modify: Add one to the value.
- Write: Store the new value back into
count.
Now, let's see how the thread scheduler can interleave these operations and cause an error.
8 Race Conditions and Critical Sections · Java-Concurrency
This resource provides a clear, step-by-step breakdown of how two threads can interfere with each other when executing a non-atomic operation.
Start from the code example under the heading "Critical Sections", which begins with public class Counter. Read the paragraph immediately following it, and then focus on the mixed execution example. This text-based walkthrough perfectly explains the interleaving that leads to the wrong result.
When Thread A reads the count (e.g., 0), the scheduler might pause it and let Thread B run. Thread B also reads the count (still 0), increments it to 1, and writes 1 back. Then, Thread A resumes. It already read 0, so it increments that value to 1 and writes 1 back. Two increment operations were performed, but the count only increased by one. An update was lost.
3. The Critical Section
The specific part of the code that accesses a shared resource and can lead to a race condition is known as a critical section.
Formally, a critical section is a piece of code that must be executed by at most one thread at a time to ensure correctness. In our Counter example, the increment() method, and specifically the line count++, is the critical section.
Problems only arise when multiple threads attempt to write to a shared resource within a critical section. If multiple threads are only reading a shared, unchanging resource, there is no risk of a race condition.
A critical section is not always a single line of code. It can be a sequence of operations that must be treated as a single, atomic unit.
🛡️ What is a Race Condition in Java, and how it can be ...
This DEV Community article illustrates a more complex scenario where even using a thread-safe AtomicInteger isn't enough to prevent a race condition. This highlights the importance of identifying the entire critical section.
Start at the heading "Problem 3 — AtomicInteger may be not enough". Read through the code example, which involves checking if enough donuts are available and then consuming them. Pay close attention to the table under the Explanation subheading. It shows how two threads can pass the if-check before either one has updated the value, leading to an incorrect state.
This example is crucial. The individual operations (donutsNumber.get() and donutsNumber.addAndGet()) might be atomic, but the logic if (check) { act } is not. The entire block—the check and the action—constitutes the critical section. If another thread can modify the state between the check and the action, a race condition occurs. This is a classic "check-then-act" race condition pattern.
4. A Preview of the Solution: Mutual Exclusion
So, how do we protect our critical sections and prevent race conditions? The solution is to enforce mutual exclusion, which means ensuring that only one thread can execute the code inside a critical section at any given time.
If Thread 1 enters a critical section, any other thread that tries to enter must be blocked and forced to wait until Thread 1 has finished and exited the critical section. This makes the entire block of code effectively atomic.

In Java, the most common way to achieve mutual exclusion is by using the synchronized keyword. By marking a method or a block of code as synchronized, you tell the JVM to acquire an intrinsic lock before executing it, thereby preventing other threads from entering at the same time.
Race Conditions in Java Multithreading
Let's revisit the Jakob Jenkov video to see a preview of how synchronized solves the problem.
Watch the segment from critical section. The video first identifies the incAndGet method as the critical section and then shows how adding the synchronized keyword makes the operation atomic and fixes the race condition, producing the correct result.
We will dive deep into the mechanics of the synchronized keyword in our next lesson. For now, the key takeaway is that you must first identify your critical sections before you can protect them.
5. Coding Exercise: See the Race Condition for Yourself
Theory is one thing, but seeing is believing. Let's reproduce the race condition.
- In your IDE, create the
Counterclass as defined earlier in this lesson. - Create a new
Runnabletask. In its constructor, accept aCounterobject. - In the
run()method of yourRunnable, create aforloop that iterates 100,000 times, calling thecounter.increment()method in each iteration. - In your
mainmethod:- Create a single instance of
Counter. - Create two
Threadobjects. Pass a new instance of yourRunnabletask to each, making sure to give both of them the sameCounterobject. - Start both threads.
- After starting them, you need to wait for them to finish. Use the
join()method for this:
The// Assuming your thread objects are named t1 and t2 try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); }join()method causes the main thread to pause until the thread it's called on has completed its execution. - After the
join()calls, print the final value fromcounter.getCount().
- Create a single instance of
Run your program multiple times. What do you observe? You should see that the final count is almost never the expected 200,000 and that the result changes with each run. You have now successfully created and witnessed a race condition.
Conclusion
In this lesson, we have moved from simply creating threads to understanding the fundamental dangers of concurrent modification. This is a critical step towards writing correct and robust multithreaded applications.
Key Takeaways:
- A race condition is an error that occurs when the correctness of a program depends on the unpredictable scheduling of multiple threads accessing shared resources.
- The primary cause is non-atomic operations, like the
read-modify-writepattern (e.g.,count++) or thecheck-then-actpattern. - A critical section is the specific block of code that accesses a shared resource and must be protected to prevent race conditions.
- The conceptual solution is to enforce mutual exclusion, ensuring only one thread can be in a critical section at a time, making it an atomic operation.
Now that we have thoroughly diagnosed the problem, our next lesson will focus entirely on the cure. We will dive deep into Java's synchronized keyword to learn how to enforce mutual exclusion and make our critical sections thread-safe.