Synchronization in Java: synchronized and volatile

synchronized gives mutual exclusion and visibility. volatile gives visibility only, and knowing the difference prevents most concurrency bugs.

The two problems of shared state

  • Atomicity. An operation that looks like one step is several, and another thread can interleave.
  • Visibility. A write by one thread may not be seen by another, because each may work with cached values and the compiler may reorder instructions.

synchronized solves both. volatile solves only the second.

The classic broken counter

public class Counter {

    private int count = 0;

    public void increment() {
        count++;        // read, add one, write back: three steps
    }

    public int get() {
        return count;
    }
}
Counter counter = new Counter();
ExecutorService pool = Executors.newFixedThreadPool(4);

for (int i = 0; i < 4; i++) {
    pool.submit(() -> {
        for (int j = 0; j < 100_000; j++) {
            counter.increment();
        }
    });
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);

System.out.println(counter.get());   // usually well below 400000
Thread A reads 10        Thread B reads 10
Thread A writes 11       Thread B writes 11
Two increments happened, but the count rose by one.

synchronized

public class Counter {

    private int count = 0;

    public synchronized void increment() {   // locks on this
        count++;
    }

    public synchronized int get() {
        return count;
    }
}

Every object has an intrinsic lock, or monitor. A thread entering a synchronized method or block acquires it and releases it on exit, including when an exception is thrown. Only one thread can hold it at a time.

Synchronized blocks

public class Inventory {

    private final Object lock = new Object();     // a private, dedicated lock
    private final Map<String, Integer> stock = new HashMap<>();

    public void add(String item, int quantity) {
        synchronized (lock) {
            stock.merge(item, quantity, Integer::sum);
        }
    }
}

A block locks less code than a whole method, which reduces contention. Using a private lock object rather than this stops outside code from locking on your instance and interfering.

Static synchronization

public static synchronized void register() { }   // locks on Inventory.class

public static void register() {
    synchronized (Inventory.class) { }           // the same lock, written out
}

An instance method and a static method lock different monitors, so they do not exclude each other. That surprise causes real bugs.

Reentrancy

public synchronized void outer() {
    inner();          // no deadlock: the same thread already holds the lock
}

public synchronized void inner() { }

Java locks are reentrant. A thread holding a lock can acquire it again, and the lock is released only when the outermost block exits.

volatile

public class Worker implements Runnable {

    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {          // without volatile, this may loop forever
            doWork();
        }
    }

    public void stop() {
        running = false;
    }
}

volatile guarantees that a write is visible to every subsequent read from any thread, and it prevents the compiler from caching the field in a register or reordering around it. It is exactly right for a flag.

What volatile does not do

private volatile int count = 0;

public void increment() {
    count++;      // still broken: volatile does not make this atomic
}
volatile makes a single read and a single write atomic and visible. A compound operation such as count++ is a read followed by a write, and another thread can interleave between them. For counters use AtomicInteger; for anything larger use a lock.

synchronized compared with volatile

Aspectsynchronizedvolatile
Mutual exclusionYesNo
VisibilityYesYes
Prevents reorderingYesYes
Applies toMethods and blocksFields only
Can block a threadYesNever
Compound operationsSafeNot safe
CostHigherLow

When each is right

SituationUse
A stop flag written by one threadvolatile
A counterAtomicInteger
Several fields that must change togethersynchronized or a Lock
A check then act sequencesynchronized
An immutable object published onceNothing, final is enough

Check then act needs a lock

// Broken even with a thread safe map: two threads can both pass the check
if (!cache.containsKey(key)) {
    cache.put(key, load(key));
}

// Correct
cache.computeIfAbsent(key, this::load);

Double checked locking

public class Registry {

    private static volatile Registry instance;    // volatile is essential here

    public static Registry getInstance() {
        Registry local = instance;
        if (local == null) {
            synchronized (Registry.class) {
                local = instance;
                if (local == null) {
                    instance = local = new Registry();
                }
            }
        }
        return local;
    }
}

Without volatile, another thread could see a non null reference to a partially constructed object, because the assignment can be reordered ahead of the constructor finishing. An enum singleton or a static holder class avoids the whole problem.

Common mistakes

  • Using volatile for a counter.
  • Synchronising on a mutable field, so the lock identity changes.
  • Synchronising on a boxed Integer or an interned String, which may be shared globally.
  • Assuming instance and static synchronized methods exclude each other.
  • Holding a lock while performing I/O or calling unknown code.
  • Synchronising everything and destroying throughput.

Best practices

  • Prefer immutable objects, which need no synchronisation at all.
  • Keep synchronised sections as short as possible.
  • Use a private final lock object rather than this.
  • Use volatile only for a simple flag or a safely published reference.
  • Use the atomic classes and concurrent collections instead of hand written locking.
  • Never call unknown code while holding a lock.

Practice

  1. Run the unsynchronised counter with four threads and explain the result.
  2. Why does a volatile counter still lose increments?
  3. Show that an instance and a static synchronized method do not exclude each other.
  4. Rewrite a check then act on a map using computeIfAbsent.
  5. Explain why double checked locking needs volatile.

Conclusion

synchronized provides mutual exclusion and visibility; volatile provides visibility alone. Use volatile for flags, atomics for counters, locks for multi field invariants, and immutability wherever you can, because it removes the problem instead of managing it.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.