ExerciseWarm-up
Warm-up
Name the edge
10 minintermediate2–10 yrs
One concept, guided. Near-impossible to fail.
What this teaches
- happens-before is a guarantee about visibility, not about clock order
- Thread.start() and Thread.join() are edges, so plain fields cross them
- Transitivity means one edge carries everything written before it
- Where you cannot name an edge, there is no guarantee at all
Starter
Starter.java
import java.util.*;
import java.util.concurrent.*;
/**
* Warm-up: for each snippet, name the edge — or say there isn't one.
*
* This is the whole skill. Not "is this thread-safe?" but "which specific
* happens-before edge carries this write to that read?". If you can name it,
* the code is correct. If you cannot, no amount of running it will tell you.
*/
public class Starter {
static class Payload {
int size;
String name;
String result;
}
/* ─────────── 1 ─────────── */
static void beforeStart() throws Exception {
Payload p = new Payload();
p.size = 42; // plain field
p.name = "invoice"; // plain field
Thread t = new Thread(() -> System.out.println(" 1: " + p.name + "/" + p.size));
t.start();
t.join();
// TODO: which edge makes the two writes visible inside the thread?
}
/* ─────────── 2 ─────────── */
static void afterJoin() throws Exception {
Payload p = new Payload();
Thread t = new Thread(() -> p.result = "done"); // plain field
t.start();
t.join();
System.out.println(" 2: " + p.result);
// TODO: which edge makes the worker's write visible to this thread?
}
/* ─────────── 3 ─────────── */
static void throughALatch() throws Exception {
Payload p = new Payload();
CountDownLatch ready = new CountDownLatch(1);
Thread producer = new Thread(() -> {
p.size = 7; // plain
p.name = "report"; // plain
ready.countDown();
});
Thread consumer = new Thread(() -> {
try {
ready.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
System.out.println(" 3: " + p.name + "/" + p.size);
});
producer.start();
consumer.start();
producer.join();
consumer.join();
// TODO: name THREE edges chained together here. The latch is only one
// of them, and it does not know these fields exist.
}
/* ─────────── 4 ─────────── */
static volatile boolean unused = false;
static void noEdge() throws Exception {
Payload p = new Payload();
Thread writer = new Thread(() -> p.name = "late");
Thread reader = new Thread(() -> {
// No edge with `writer` at all. Whatever this prints proves nothing.
System.out.println(" 4: " + p.name);
});
writer.start();
reader.start();
writer.join();
reader.join();
// TODO: this one has NO edge between the write and the read. Say what
// the JVM is permitted to do, and why the output you get is not
// evidence either way.
}
public static void main(String[] args) throws Exception {
beforeStart();
afterJoin();
throughALatch();
noEdge();
// TODO 5: three of the four are guaranteed. Add `volatile` to every
// field in Payload and confirm nothing changes about the guarantees.
// Then remove it again and say what the annotation would have been
// communicating to the next reader.
// TODO 6: in snippet 4, insert Thread.sleep(100) before the read so
// it "works". Explain in one sentence why the code is no more correct
// than it was.
// TODO 7: fix snippet 4 properly. There are at least three ways —
// pick the cheapest and say why the others are heavier than needed.
}
}Run it locally:
cd exercises/java/concurrency/happens-before/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterDone when
- You named the edge for each of the four snippets before running anything
- You removed a volatile that an existing edge already made redundant
- You found the one snippet with no edge and can say what may happen
- You can state why adding a sleep is not a fix