ByteScrollGet the app
☰ Topics
volatile-synchronized-atomic16 / 200‹›
JAVA / CONCURRENCY3 minute read

volatile vs synchronized vs atomic — when each?

Hard

volatile makes one variable's writes visible to other threads but doesn't make count++ atomic. Atomic classes do single-variable read-modify-write with CAS, no lock. synchronized gives mutual exclusion plus visibility, for rules that span several fields.

How it works

  1. The problem. Threads may cache values in registers or CPU caches, and the compiler and CPU may reorder instructions. Without a happens-before link, one thread may never see another's write.
  2. volatile
    • Every read sees the latest write to that variable, and a write also publishes everything the writer did before it.
    • No locking, so no blocking.
    • It does not make compound actions atomic: count++ is read, add, write, and two threads can interleave.
  3. AtomicInteger, AtomicLong, AtomicReference…
    • Use compare-and-set (CAS) hardware instructions: "set to new value only if it's still the old value", retrying on failure.
    • incrementAndGet(), compareAndSet(), updateAndGet(), accumulateAndGet().
    • Safe for one variable. Two atomics updated one after the other are not one atomic step.
  4. synchronized
    • Only one thread at a time holds the monitor; others block.
    • Leaving the block publishes all writes to the next thread that enters with the same lock.
    • Reentrant. Needed when an invariant links several fields (e.g. balance and history must change together).
noyesnoyesyesnoShared mutablestate?Only onevariable?synchronizedor a LockUpdates dependon the currentvalue? (++,max,check-then-set)volatile(flags,publishedreferences)Heavycontention ona counter?LongAdderAtomicInteger/AtomicReference
noyesnoyesyesnoShared mutablestate?Only onevariable?synchronizedor a LockUpdates dependon the currentvalue? (++,max,check-then-set)volatile(flags,publishedreferences)Heavycontention ona counter?LongAdderAtomicInteger/AtomicReference

Example

Example.javaJava
class Downloader {
    private volatile boolean cancelled;           // one writer, many readers
    private final AtomicLong bytes = new AtomicLong();
    private final List<String> failed = new ArrayList<>();
    private int failures;

    void cancel() { cancelled = true; }           // seen by worker threads promptly

    void onChunk(int size) {
        if (cancelled) return;
        bytes.addAndGet(size);                    // atomic, no lock
    }

    synchronized void onFailure(String url) {     // two fields must stay in step
        failed.add(url);
        failures++;
    }
}

// Lost updates with volatile alone
class BadCounter { volatile int n; void hit() { n++; } } // 2 threads × 1M hits → often < 2M

Edge cases

  • Plain long and double writes may be split into two 32-bit halves on some JVMs. Declaring them volatile makes each read and write whole.
  • volatile on an array or object reference covers the reference, not the elements or fields inside. Use AtomicIntegerArray or immutable objects.
  • CAS loops can spin under heavy contention. LongAdder spreads updates across cells and sums them on read, trading exact snapshots for throughput.
  • Before Java 24, a virtual thread blocking inside synchronized pinned its carrier thread. JEP 491 (Java 24) removed that limitation; on Java 21 prefer ReentrantLock around blocking calls in virtual threads.

Common mistakes

  • Marking a counter volatile and assuming ++ is now thread-safe.
  • if (map.get(k) == null) map.put(k, v) on separate atomic operations: each step is safe, the combination isn't. Use putIfAbsent/computeIfAbsent.
  • Synchronizing on different lock objects in different methods that guard the same data.

Likely follow-up

"Is double-checked locking safe?" Only if the field is volatile. Without it, another thread can see a non-null reference to an object whose constructor writes aren't visible yet.

Get every deep dive in the app

Coming soon to the App StoreComing soon to Google Play