Pick by the operation you need: volatile for visibility of a single read or write, atomics for a lock-free read-modify-write on one variable, and synchronized (or an explicit lock) when you must keep multiple variables or steps consistent as one unit. They form a ladder of increasing power and cost.
What each one guarantees
volatile— visibility and ordering for a single field. A write is seen by later reads, and it prevents reordering. It does not make compound operations atomic:volatile int x; x++is still read-modify-write and races.- Atomic classes — visibility plus atomic compound updates on one variable via CAS.
incrementAndGet,compareAndSet,updateAndGetare atomic and lock-free. synchronized— mutual exclusion over an arbitrary block, so you can keep several fields invariant together. It also gives visibility (lock release/acquire is a happens-before edge), at the cost of blocking and possible contention/deadlock.
The decision
// 1. volatile: a flag written by one thread, read by others
private volatile boolean running = true; // just visibility
// 2. atomic: lock-free counter, single variable
private final AtomicInteger hits = new AtomicInteger();
hits.incrementAndGet(); // atomic read-modify-write
// 3. synchronized: TWO fields must stay consistent together
private double balance;
private List<String> ledger = new ArrayList<>();
synchronized void withdraw(double amt) { // multi-step invariant
balance -= amt;
ledger.add("withdraw " + amt);
}
The bank-transfer case is the giveaway that atomics are not enough: debiting one account and crediting another must be one atomic unit across two variables — no single CAS covers that, so you need a lock.
Cost ordering
Roughly cheapest to most expensive: plain read < volatile < uncontended CAS < contended CAS < uncontended lock < contended lock (which can deschedule the thread). Reach for the weakest tool that is correct — but never trade correctness for speed.