ExerciseWarm-up
Warm-up
Watch volatile lose two thirds of the increments
10 minjunior1–8 yrs
One concept, guided. Near-impossible to fail.
What this teaches
- Atomicity, visibility and ordering are three separate problems
- volatile fixes the last two and never the first
- count++ is read, add, write — three operations, not one
- AtomicInteger and synchronized both give atomicity, and both give visibility too
- The loss is not a rare interleaving; it is the normal case
Starter
Starter.java
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/**
* Warm-up: the counter everyone has written, and why it is wrong.
*
* Three problems live in shared-memory concurrency — atomicity, visibility and
* ordering. volatile addresses two of them. Almost every "but I made it
* volatile" bug is someone reaching for it to solve the third.
*/
public class Starter {
static final int THREADS = 8;
static final int PER_THREAD = 100_000;
static class VolatileCounter {
volatile int count = 0;
void increment() {
// Read the field, add one, write it back. volatile makes each of
// those visible to other threads. Nothing here is indivisible.
count++;
}
}
/** Runs body on THREADS threads, PER_THREAD times each, released together. */
static void hammer(Runnable body) throws Exception {
Thread[] threads = new Thread[THREADS];
CountDownLatch start = new CountDownLatch(1);
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int n = 0; n < PER_THREAD; n++) body.run();
});
threads[i].start();
}
start.countDown();
for (Thread t : threads) t.join();
}
public static void main(String[] args) throws Exception {
int expected = THREADS * PER_THREAD;
// TODO 1: predict the printed number before you run this. Write your
// prediction down — most people guess "expected, or maybe one or two
// short".
VolatileCounter counter = new VolatileCounter();
hammer(counter::increment);
System.out.println("volatile int");
System.out.println(" expected : " + expected);
System.out.println(" actual : " + counter.count);
System.out.println(" lost : " + (expected - counter.count));
// TODO 2: run it three more times. The number changes every run and
// is never right. Say which of the three problems volatile failed to
// solve, and why the other two were never the issue here.
// TODO 3: make it exact with an AtomicInteger and incrementAndGet.
// Print it. Then say, in one sentence, what compare-and-swap does that
// volatile cannot.
// TODO 4: make it exact a second way — a plain int guarded by a
// synchronized method. Print it.
// TODO 5: now delete `volatile` from your synchronized version's
// field. The result stays exact. Explain why the monitor already gave
// you what volatile was providing.
// TODO 6: the one case volatile IS right. Write a worker whose loop
// is `while (!shutdown) { ... }` and a stop() that sets the flag.
// Say why this needs volatile and why it does NOT need a lock.
}
}Run it locally:
cd exercises/java/concurrency/volatile-vs-synchronized/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterDone when
- You saw a volatile counter lose most of its increments
- You made it exact twice — once with an atomic, once with a lock
- You can say why volatile did not help, in terms of the three problems
- You removed volatile from the working versions and explained why it changed nothing
← Back to What does volatile guarantee, and what does it not?