Synchronization — synchronized, volatile, Atomic Classes, and Happens-Before
In this tutorial, you will learn about Synchronization. We cover key concepts, practical examples, and best practices to help you master this topic.
Java synchronization coordinates access to shared mutable data across threads using the synchronized keyword, volatile variables, and atomic classes. Without synchronization, multi-threaded programs exhibit race conditions, stale data, and visibility failures — one thread may never see changes made by another thread.
What You'll Learn
- The Java Memory Model and visibility problems
- synchronized methods and blocks
- volatile for visibility without locking
- AtomicInteger, AtomicLong, AtomicReference
- The happens-before relationship
Why It Matters
Concurrency bugs are notoriously hard to reproduce and debug. Understanding synchronization prevents race conditions (inconsistent data) and visibility issues (stale data) before they happen.
Real-World Use
Atomic counters track metrics in production. Synchronized blocks guard critical sections in shared caches. Volatile flags control thread termination.
The Problem: Race Condition
class Counter {
private int count = 0;
public void increment() {
count++; // read -> increment -> write — NOT atomic
}
public int getCount() { return count; }
}
With multiple threads calling increment(), the operation count++ is actually three steps: read count, increment, write back. Two threads can read the same value, both increment to the same result, and write back — losing an increment.
synchronized Keyword
Synchronized Methods
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
A synchronized method acquires the monitor lock (this) before executing and releases it after. This ensures mutual exclusion and visibility.
Synchronized Blocks
Finetune the locking scope:
class BankAccount {
private double balance;
private final Object lock = new Object();
public void deposit(double amount) {
synchronized (lock) {
balance += amount;
}
}
public void withdraw(double amount) {
synchronized (lock) {
if (balance >= amount) {
balance -= amount;
}
}
}
}
Static Synchronized Methods
Use the class's monitor:
class Config {
private static Properties props = new Properties();
public static synchronized void update(String key, String value) {
props.setProperty(key, value);
}
}
volatile
volatile ensures visibility without mutual exclusion — writes by one thread are visible to all other threads immediately:
class TaskRunner {
private volatile boolean running = true;
public void stop() {
running = false; // visible to other threads immediately
}
public void run() {
while (running) {
// do work
}
}
}
Without volatile, the loop may never see the updated value because the JIT compiler can optimize the read to a register.
When to Use volatile
- Status flags — boolean/running state
- Published values — one writer, multiple readers
- Not for compound actions —
count++still needs synchronization
Atomic Classes
For atomic operations on single variables:
import java.util.concurrent.atomic.*;
class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // atomic: read-increment-write
}
public int getCount() {
return count.get();
}
}
Common Atomic Methods
AtomicInteger ai = new AtomicInteger(10);
ai.get(); // 10
ai.set(20); // 20
ai.getAndSet(30); // returns 20, sets to 30
ai.compareAndSet(30, 40); // if current is 30, set to 40 — returns true
ai.incrementAndGet(); // 41
ai.addAndGet(10); // 51
AtomicLong al = new AtomicLong();
AtomicReference<String> ref = new AtomicReference<>("initial");
// Update with lambda (Java 8+)
ai.updateAndGet(x -> x * 2);
ai.accumulateAndGet(5, Integer::sum);
Atomic Fields in Classes
class SharedObject {
private final AtomicReference<String> config = new AtomicReference<>("default");
public void updateConfig(String newConfig) {
config.set(newConfig);
}
public String getConfig() {
return config.get();
}
}
The Happens-Before Relationship
Happens-before is a guarantee that one action's result is visible to another action. Key rules:
- Program order — within a single thread, each action happens-before the next in code order
- Monitor lock — unlocking a monitor happens-before every subsequent locking of the same monitor
- Volatile — a write to a volatile field happens-before every subsequent read of that field
- Thread start —
Thread.start()happens-before any action in the started thread - Thread join — all actions in a thread happen-before any thread returns from
join() - Transitivity — if A happens-before B, and B happens-before C, then A happens-before C
Common Mistakes
- Using
synchronizedon a method but the lock object is notfinal. If the lock reference changes, different threads use different locks. - Forgetting
volatilefor status flags. The JIT can cache the variable in a register, making the update invisible. - Using
AtomicIntegerfor compound operations that involve multiple variables. For multi-variable invariants, use locks. - Thinking
synchronizedis only about mutual exclusion. It also guarantees visibility — without it, the updated value may not be visible to other threads. - Using
Stringliterals as locks. String literals are interned — other code may unintentionally use the same lock.
Practice Questions
1. What is a race condition?
When multiple threads access shared data concurrently and the final result depends on the interleaving of operations.
2. What is the difference between synchronized and volatile?
synchronized provides mutual exclusion and visibility. volatile provides only visibility (no mutual exclusion). Use volatile for single-variable status flags.
3. What does AtomicInteger.incrementAndGet() do?
Atomically increments the value and returns the new value. It is equivalent to synchronized (this) { return ++count; } but faster.
4. What is happens-before?
A memory model concept that guarantees visibility — if A happens-before B, then all effects of A are visible to B.
5. Why is count++ not thread-safe even with volatile?
count++ is a read-modify-write operation (three steps). volatile ensures visibility but not atomicity. Use AtomicInteger or synchronized.
Challenge Question:
Implement a thread-safe BoundedBuffer with put() and take() methods. Use synchronized, wait(), and notifyAll(). The buffer has a fixed capacity. put() blocks when full, take() blocks when empty. Write a test with multiple producers and consumers.
FAQ
Mini Project
Write a program SynchronizationDemo.java that:
- Creates a shared
Counterclass — first without synchronization (demonstrate race condition) - Creates 10 threads that each increment the counter 1000 times
- Shows the final count is not 10,000 (due to race condition)
- Fixes it with
synchronized— shows the correct result - Replaces with
AtomicInteger— shows correct result - Uses
volatilefor a running flag (daemon thread) — shows immediate visibility - Implements a
synchronizedBoundedBufferwith wait/notify - Tests the buffer with multiple producers and consumers
What's Next
The synchronized keyword provides basic locking, but advanced scenarios need more flexible locks. Lesson 53 covers locks — ReentrantLock, ReadWriteLock, StampedLock, and Condition for fine-grained concurrency control.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro