synchronized and volatile: visibility vs. atomicity -- two different problems
volatile guarantees visibility (a write from one thread is immediately visible to others -- no thread
caches a stale value) but does nothing for atomicity of compound operations like count++ (which is
really read-modify-write, three separate steps). synchronized gives you both: mutual exclusion makes the
compound operation atomic, and entering/exiting a monitor also establishes the same visibility guarantee.
(The volatile field is declared inside a small wrapper class below rather than as a loose top-level
variable -- a JShell/notebook-specific detail: JShell's REPL-style top-level variables aren't backed by real
class fields, so volatile on one of them doesn't actually get the real semantics. Once it's a genuine field
of a genuine class, it behaves exactly as documented. The spin loop is also bounded so it can't hang forever
even if something unexpected happens -- it should flip almost instantly.)
class Flags {
volatile boolean ready = false;
}
Flags flags = new Flags();
int maxSpins = 200_000_000;
Thread reader = new Thread(() -> {
int spins = 0;
while (!flags.ready && spins < maxSpins) spins++;
System.out.println(" reader saw ready=" + flags.ready + " after " + spins + " spins");
});
reader.start();
Thread.sleep(50);
flags.ready = true; // without volatile, the reader could keep spinning on a cached stale value
reader.join();
But volatile alone does NOT make counter++ safe under concurrent writers -- watch real lost updates:
class Counters {
volatile int volatileCounter = 0;
int synchronizedCounter = 0;
final Object lock = new Object();
}
Counters c = new Counters();
int nThreads = 8, iterations = 50_000;
Thread[] threads = new Thread[nThreads];
for (int i = 0; i < nThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) c.volatileCounter++;
});
}
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
int expected = nThreads * iterations;
System.out.println("expected: " + expected + ", volatile counter actual: " + c.volatileCounter
+ (c.volatileCounter != expected ? " <- LOST UPDATES, volatile did not help" : ""));
Thread[] threads2 = new Thread[nThreads];
for (int i = 0; i < nThreads; i++) {
threads2[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
synchronized (c.lock) { c.synchronizedCounter++; }
}
});
}
for (Thread t : threads2) t.start();
for (Thread t : threads2) t.join();
System.out.println("expected: " + expected + ", synchronized counter actual: " + c.synchronizedCounter + " <- correct");