Understanding Race Conditions in Java: Problem and Solutions

Introduction

In multi-threaded programming, a race condition occurs when two or more threads access and modify a shared resource concurrently without proper synchronization. This can lead to unpredictable behavior and incorrect results.

This article demonstrates a simple race condition in Java and presents four solutions:

  1. Using the synchronized keyword.

  2. Using AtomicInteger.

  3. Using a Mutex (explicit object lock).

  4. Using a ReentrantLock.


The Problem: Race Condition Example

Consider a Counter class that maintains a simple integer value and allows incrementing it:

Two threads increment this counter 10000 times each:

Why It Fails

The operation value++ is not atomic; it consists of three steps:

  1. Read the current value.

  2. Add 1.

  3. Write the new value back.

If two threads execute this simultaneously, one increment can overwrite the other, causing lost updates. As a result, the final value may be less than 20000.

On fast processors or with JVM optimizations, the race condition may not always manifest, but the problem remains.


Solution 1: Using synchronized

The simplest way to fix the race condition is to make the increment() method synchronized:

This ensures that only one thread can execute the increment() method at a time, preventing lost updates.

Pros of synchronized:

  • Easy to understand.

  • Works for complex critical sections, not just integers.

  • Prevents multiple threads from entering critical sections simultaneously.

Cons of synchronized:

  • Can reduce performance due to thread blocking.

  • May cause contention on multi-core systems.

  • Risk of deadlocks if multiple locks are used incorrectly.


Solution 2: Using AtomicInteger

Another approach is to use the AtomicInteger class, which provides atomic operations without locking:

AtomicInteger uses low-level atomic hardware instructions (Compare-And-Swap) to ensure thread-safe updates.

Pros of AtomicInteger:

  • Lock-free and non-blocking.

  • Highly efficient for simple atomic operations.

  • Scales better under high contention.

Cons of AtomicInteger:

  • Limited to simple operations on primitives.

  • Not suitable for protecting complex critical sections involving multiple variables.

  • Code readability may decrease with multiple atomic operations combined.


Solution 3: Using a Mutex

A mutex can be implemented in Java using an explicit Object lock. This makes locking intention clear:

Pros of Mutex:

  • Explicit lock object makes synchronization intention clear.

  • Safer than synchronizing on this when sharing instances across code bases.

  • Very simple to implement.

Cons of Mutex:

  • Functionally similar to synchronized.

  • Inherits the same limitations regarding performance and blocking.

  • Less feature-rich compared to ReentrantLock.

Solution 4: Using ReentrantLock

ReentrantLock is part of java.util.concurrent.locks and provides more flexibility than synchronized.

Pros of ReentrantLock:

  • More flexible than synchronized (supports fairness, tryLock, multiple conditions).

  • Can be re-entered safely by the same thread.

  • Useful for advanced concurrency patterns.

Cons of ReentrantLock:

  • More verbose, requires explicit unlock() in finally.

  • Slightly more overhead in simple cases compared to synchronized.


Conclusion

Race conditions are a common problem in multi-threaded programming. The example above shows how incrementing a shared counter without synchronization can produce incorrect results.

We explored four main solutions:

  • synchronized — simple and reliable for general use.

  • AtomicInteger — lock-free and highly efficient for counters and simple atomic updates.

  • Mutex — explicit object lock, clear intent, suitable for straightforward critical sections.

  • ReentrantLock — powerful and flexible for advanced concurrency requirements.

You can check this code on github

0
Subscribe to my newsletter

Read articles from Luis Gustavo Souza directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Luis Gustavo Souza
Luis Gustavo Souza