Warm-up

Watch volatile lose two thirds of the increments

10 minjunior18 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 Starter

Done 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?