Skip to content

Synchronization — synchronized, volatile, Atomic Classes, and Happens-Before

DodaTech Updated 2026-06-28 6 min read

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 actionscount++ 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:

  1. Program order — within a single thread, each action happens-before the next in code order
  2. Monitor lock — unlocking a monitor happens-before every subsequent locking of the same monitor
  3. Volatile — a write to a volatile field happens-before every subsequent read of that field
  4. Thread startThread.start() happens-before any action in the started thread
  5. Thread join — all actions in a thread happen-before any thread returns from join()
  6. Transitivity — if A happens-before B, and B happens-before C, then A happens-before C

Common Mistakes

  1. Using synchronized on a method but the lock object is not final. If the lock reference changes, different threads use different locks.
  2. Forgetting volatile for status flags. The JIT can cache the variable in a register, making the update invisible.
  3. Using AtomicInteger for compound operations that involve multiple variables. For multi-variable invariants, use locks.
  4. Thinking synchronized is only about mutual exclusion. It also guarantees visibility — without it, the updated value may not be visible to other threads.
  5. Using String literals 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

What is the difference between `synchronized` and `Lock`?

synchronized is simpler (implicit lock release), but less flexible. Lock (ReentrantLock) supports try-lock, timed lock, and multiple condition variables.

Can I use synchronized on a constructor?

No. A constructor cannot be synchronized — only the thread creating the object runs the constructor. Synchronize the code inside if needed.

What is a deadlock?

Two or more threads each holding a lock the other needs, and neither can proceed. Use lock ordering to prevent deadlocks.

What is `compareAndSet`?

An atomic operation that checks if the current value equals an expected value, and if so, updates to a new value. Returns true if successful. The foundation of non-blocking algorithms.

What is the performance cost of synchronization?

Synchronization has overhead: acquiring/releasing the lock, cache coherence traffic, and potential context switching. However, uncontended synchronization in modern JVMs is very cheap (optimized with biased locking).

Mini Project

Write a program SynchronizationDemo.java that:

  1. Creates a shared Counter class — first without synchronization (demonstrate race condition)
  2. Creates 10 threads that each increment the counter 1000 times
  3. Shows the final count is not 10,000 (due to race condition)
  4. Fixes it with synchronized — shows the correct result
  5. Replaces with AtomicInteger — shows correct result
  6. Uses volatile for a running flag (daemon thread) — shows immediate visibility
  7. Implements a synchronized BoundedBuffer with wait/notify
  8. 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