Race Conditions & Critical Sections in Java

In multithreaded programming, multiple threads often access shared resources such as variables, objects, or data structures. While this enables powerful concurrent execution, it also introduces one of the most common and dangerous problems: race conditions.

Understanding race conditions and identifying critical sections is the first step toward writing safe and correct concurrent programs.

What is a Race Condition?

A race condition occurs when multiple threads access and modify shared data at the same time, and the final result depends on the timing and order of execution.

Since thread scheduling is unpredictable, the outcome becomes inconsistent and often incorrect.

In simple terms: "Multiple threads are racing to update shared data, and the result depends on who wins."

What is a Critical Section?

A critical section is a part of the code where shared resources are accessed or modified.

If multiple threads execute this section simultaneously without control, it can lead to data corruption or inconsistent results.
Example of a critical section:
count++; // NOT an atomic operation 
Even though it looks like a single line, it actually involves multiple steps:
- Read value of count
- Increment value
- Write value back

If two threads execute this simultaneously, updates can be lost. Both threads may read the same initial value before either writes the updated result, causing one update to overwrite the other.

As a result, even though two increments occurred logically, only one is reflected in the final value.

What Goes Wrong Without Synchronization

Without proper synchronization, multiple issues can occur:

1. Lost Updates: Two threads read the same value, increment it, and write it back. One update overwrites the other.
2. Inconsistent Data: Threads may see partially updated or stale values due to lack of memory visibility guarantees.
3. Non-Deterministic Behavior: The program produces different results each time it runs, making debugging extremely difficult.
4. Data Corruption: Shared data structures can become invalid if multiple threads modify them simultaneously.

Example: Race Condition in Action

Consider a simple counter incremented by multiple threads.
class Counter {
    int count = 0;

    void increment() {
        count++;
    }
}

public class Main {
    static void main(String[] args) throws Exception {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final count: " + counter.count);
    }
}
Expected Output:
Final count: 2000 
Actual Output (may vary):
Final count: 1737
The result is inconsistent because multiple threads are modifying count without synchronization.

Why Does This Happen?

The operation count++ is not atomic. Internally, it works like this:

// simplified breakdown 
int temp = count; 
temp = temp + 1; 
count = temp; 
Now imagine:
- Thread A reads count = 5
- Thread B reads count = 5
- Thread A writes 6
- Thread B also writes 6
One increment is lost.

Another Example: Bank Account Problem

class BankAccount {
    int balance = 100;

    void withdraw(int amount) {
        if (balance >= amount) {
            balance = balance - amount;
        }
    }
}
If two threads withdraw money simultaneously:
- Both may see sufficient balance
- Both proceed to withdraw
- Balance may go negative or incorrect
This is a classic race condition leading to data inconsistency.

Race conditions occur because:
- Threads share memory
- Execution order is unpredictable
- Operations are not atomic

How to Identify Critical Sections

A code section is critical if:
- It accesses shared data
- It modifies shared state
- Multiple threads can execute it simultaneously

Such sections must be protected using synchronization mechanisms (covered in next article).

Race conditions are one of the most fundamental problems in concurrent programming. They arise when multiple threads access shared data without proper coordination, leading to unpredictable and incorrect results.

By identifying critical sections and understanding what goes wrong without synchronization, developers can take the first step toward writing safe multithreaded applications.

In the next article, we will explore synchronization in Java and learn how to protect critical sections using the synchronized keyword and locks.
Nagesh Chauhan
Nagesh Chauhan
Principal Engineer | Java ยท Spring Boot ยท Python ยท Microservices ยท AI/ML

Principal Engineer with 14+ years of experience in designing scalable systems using Java, Spring Boot, and Python. Specialized in microservices architecture, system design, and machine learning.

Share this Article

๐Ÿ’ฌ Comments

Join the discussion