Skip to content

Expert · Multithreading and concurrency · topic 42 of 60 · 30 min

Race conditions and synchronized

Explanation

When threads share changing data, their steps can interleave and corrupt it: count++ is really read, add, write, so two threads can lose updates. This is a race condition. synchronized lets only one thread at a time run a block guarded by a lock, volatile makes a variable's latest value visible to all threads, and acquiring locks in different orders can cause deadlock.

A race condition

java
class Counter {
    private int count = 0;
    void increment() { count++; }       // NOT atomic: read → add 1 → write
    int get() { return count; }
}

Counter c = new Counter();
Runnable task = () -> { for (int i = 0; i < 100_000; i++) c.increment(); };
Thread a = new Thread(task), b = new Thread(task);
a.start(); b.start(); a.join(); b.join();
System.out.println(c.get());   // expected 200000, often prints less, e.g. 137512

Both threads read the same old value (say 41), both add one, both write 42: one increment is lost. Whether and how often this happens depends on timing, so the bug may hide during testing and appear under real load.

synchronized: one thread at a time

java
class SafeCounter {
    private int count = 0;
    synchronized void increment() { count++; }   // the lock is "this" object
    synchronized int get() { return count; }
}

class Inventory {
    private final Object lock = new Object();
    private int stock = 10;
    boolean reserve(int qty) {
        synchronized (lock) {                     // a synchronized block on a private lock
            if (stock < qty) return false;
            stock -= qty;                         // check-then-act happens as one unit
            return true;
        }
    }
}

Every Java object can act as a lock (a monitor). Only one thread can hold a given lock at a time; others wait (BLOCKED) until it is released at the end of the block. Synchronizing the check AND the update together prevents two customers from both buying the last item.

Visibility and volatile

For speed, each CPU core may keep a cached copy of variables, so one thread may not see another thread's update for a while. synchronized also guarantees visibility. For a simple flag that one thread writes and others read, volatile is enough: every read sees the latest write. volatile does NOT make count++ safe; it only fixes visibility.

java
class Worker implements Runnable {
    private volatile boolean running = true;   // without volatile, the loop might never see the change
    public void run() { while (running) { /* work */ } }
    public void stop() { running = false; }
}

Deadlock

A deadlock happens when two threads each hold one lock and wait forever for the other's lock. Thread 1 locks account A then waits for B; thread 2 locks B then waits for A. Neither can continue. The standard fix is to always acquire locks in the same global order (for example by account number).

java
void transfer(Account from, Account to, long paise) {
    Account first = from.id() < to.id() ? from : to;     // consistent order prevents deadlock
    Account second = first == from ? to : from;
    synchronized (first) {
        synchronized (second) {
            from.debit(paise);
            to.credit(paise);
        }
    }
}
Careful: Common mistakes: synchronizing only the write but not the read; synchronizing on different lock objects in different methods (so they do not actually exclude each other); holding a lock while doing slow I/O (everyone waits); and assuming volatile makes compound actions atomic. The simplest safe design is to share as little mutable data as possible.
Think of it like this: A race condition is like two clerks updating the same paper register without talking: both read "42 seats left", both sell one, both write 41, and one seat is sold twice. synchronized is the rule that only the person holding the register's single pen may write in it.
Tip: Prefer higher-level tools (atomic variables, concurrent collections, executors, immutable objects; topics 3 and 4) over hand-written synchronized blocks. They are faster to write and harder to get wrong.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Race conditions and synchronized · 1/6

Race conditions

In this lesson you will see why shared data breaks with threads, and how to fix it.

Prefer a video by a teacher? Find free videos on “java race condition synchronized volatile deadlock explained” on YouTube ↗

In real life: Overselling the last train seats

During a tatkal booking rush, two booking threads checked "seats > 0" at the same moment for the last seat, and both booked it. Making the check and the decrement one atomic operation (a synchronized block, or a database row lock / conditional update in production) fixed the double booking.

java
synchronized boolean book(String pnr) {
    if (seatsLeft == 0) return false;
    seatsLeft--;
    bookings.add(pnr);
    return true;
}
// In a real system: UPDATE coach SET seats_left = seats_left - 1 WHERE id = ? AND seats_left > 0

Practice (0/3 done)

Task 1 · Question

Two threads each call an unsynchronized count++ 1,000 times on a shared int starting at 0. What is true?

Task 2 · Question

Thread 1: synchronized(a) { synchronized(b) { … } } Thread 2: synchronized(b) { synchronized(a) { … } } What can happen?

Task 3 · Practice

Write the unsafe Counter with two threads doing 100,000 increments each and print the result several times (note the wrong totals). Then fix it in two ways: (1) with synchronized methods and (2) with AtomicInteger, and confirm 200000 every time.

Quick quiz

Question 1

1.What is a race condition?

Question 2

2.Which object is the lock for a synchronized instance method?

Question 3

3.What does volatile guarantee?

Question 4

4.How do you prevent the classic two-lock deadlock?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is a race condition?

Answer: A bug where the result depends on the timing of threads

Interleavings change the result.

Which object is the lock for a synchronized instance method?

Answer: this (the object)

Static synchronized methods lock the Class object instead.

What does volatile guarantee?

Answer: Visibility of the latest write to all threads

Only visibility (and ordering), not atomicity of compound actions.

How do you prevent the classic two-lock deadlock?

Answer: Always acquire locks in the same order

A global lock order breaks the cycle.

Why use std::lock_guard instead of calling m.lock() and m.unlock() yourself?Related

Answer: It unlocks automatically, even when an exception is thrown (RAII)

Manual unlock is easy to miss on an early return or exception.

From Concurrency 1: threads, mutexes and data races

Many threads read a config and rarely update it. Which is a good fit?Related

Answer: RwLock

RwLock allows many readers at once.

From Shared state: Arc, Mutex, Send and Sync

What is a data race?Related

Answer: Two threads accessing the same data at once, at least one writing, without synchronisation

Rust prevents data races at compile time in safe code.

From Threads

What happens when a virtual thread blocks on I/O?Related

Answer: The JVM parks it and runs another virtual thread on the carrier

That is what makes blocking cheap.

From Virtual threads (Java 21)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Multithreading and concurrency” before its exam: Executors, futures and CompletableFuture, Atomic variables and concurrent collections, Virtual threads (Java 21).
  2. Module 9 exam: Multithreading and concurrency · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, JDBC, Maven, Gradle and JUnit. 60 topics left in the course before the final exam.

← Previous: Threads: doing things at the same time