Expert · Multithreading and concurrency · topic 42 of 60 · 30 min
Race conditions and synchronized
Explanation
A race condition
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. 137512Both 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
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.
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).
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);
}
}
}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
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.
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 > 0Practice (0/3 done)
Two threads each call an unsynchronized count++ 1,000 times on a shared int starting at 0. What is true?
Thread 1: synchronized(a) { synchronized(b) { … } } Thread 2: synchronized(b) { synchronized(a) { … } } What can happen?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Executors, futures and CompletableFutureMultithreading and concurrency · same module
- Atomic variables and concurrent collectionsMultithreading and concurrency · same module
- Concurrency 1: threads, mutexes and data racesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
- Shared state: Arc, Mutex, Send and SyncRust Programming · Concurrency and async
- ThreadsRust Programming · Concurrency and async
- Concurrency 2: atomics, async, futures and condition variablesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
Questions people ask about this
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.
Many threads read a config and rarely update it. Which is a good fit?Related
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.
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
- 3 more topics in “Multithreading and concurrency” before its exam: Executors, futures and CompletableFuture, Atomic variables and concurrent collections, Virtual threads (Java 21).
- Module 9 exam: Multithreading and concurrency · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, JDBC, Maven, Gradle and JUnit. 60 topics left in the course before the final exam.