Concurrency · LLD
Atomicity and memory visibility
Atomicity means another thread never sees a half-done update. Visibility means a finished write on one thread becomes readable on another. Example: Gate A marks spot 101 occupied. Gate B on another core still prints “101 is free.” Or worse: Gate B sees occupied = true but the ticket id is still null, because the two writes did not become visible together.
Those two problems are easy to mix up. means the other thread never sees a half-done change. means a finished write on one thread becomes readable on another. You can fail either one and still get two cars in 101, or a crash on a null field.
The real question
Did I do one step — and can the other gate see it?
Problem A
Atomicity
Check-then-act is not one step. Reading occupied and then writing occupied is two steps, even on one line of English. increment++, ticket.status = PAID after a read, map.get then map.put — all of these can split.
Atomicity is about the whole decision, not the assignment. A boolean write may be one word. “If free, occupy” is still two words.
Problem B
Visibility
Thread A wrote occupied = true. Thread B may still see false, for a long time, unless something creates a hand-off: a lock both threads use, a volatile field, or an atomic variable.
A is quieter than a double ticket. Tests on one machine with one core often hide it. Production has many cores.
A finished picture, or a torn one
Gate A writes two fields: occupied = true, then ticketId = T-1. Without a handshake, Gate B can see any mix: both old, both new, or occupied true with ticketId still null. That last mix is how a null-pointer “impossible” crash is born.
Both old
occupied false, ticketId null. Gate B thinks 101 is free. Stale, but consistent.
Torn
occupied true, ticketId null. Gate B crashes or prints a blank slip.
Both new
occupied true, ticketId T-1. Only this is a finished occupy.
A lock around both writes, or a single atomic status that implies “occupied with this ticket,” keeps the picture whole. Publishing one volatile flag after the other fields is a pattern; publishing the flag first is a bug.
Visible, still a race
makes one field’s writes visible. It does not glue two actions into one. Reason about that before you reach for AtomicBoolean: if both gates still pass the if, you have a clean view of a lost update.
class ParkingSpot {
private volatile boolean occupied = false;
boolean tryOccupy() {
if (occupied) return false; // Gate A and Gate B both see false
occupied = true; // both write true
return true;
}
}One field, one step
After the story is “only one occupy wins,” is a fair first Java sentence. You name the expected state and the next state. If the current value is not expected, you fail. The loser does not pretend it won.
class ParkingSpot {
private final AtomicBoolean occupied = new AtomicBoolean(false);
boolean tryOccupy() {
return occupied.compareAndSet(false, true);
}
}
// Gate: if (!spot.tryOccupy()) try the next rectangle.A lock around “if !occupied then occupied = true” is the pessimistic twin. Both treat the decision as one step. Neither is “set the boolean and hope.” Visibility comes with the CAS or the lock; you do not add volatile on top “to be safe.”
The handshake, in plain language
Java will not promise that Gate B sees Gate A’s writes unless a edge exists. You do not need to lecture the JMM. You need one honest handshake:
- Same lock. Unlock after the write. The next lock sees the write. Both gates must lock the same object — the spot or the lot, not “this” on a random controller.
- Volatile / atomic field. A write to that field publishes. A later read of that field sees it. Other fields written before the volatile write can travel with it if you ordered them correctly.
- Safe publication. A final field set in a constructor, then the object handed to another thread through a concurrent queue, is a finished picture. Mutating after the hand-off reopens the story.
Questions that come up
Is volatile enough for occupy?
Is a long write atomic?
I assigned occupied on Gate A. Why does Gate B disagree?
How do I publish occupied and ticketId together?
increment++ is not one step either
A parking lot that counts free spots with a plain int and free++ has the same two problems. The add is a read, an add, and a write. Two gates can both read 12 and both write 13. Even if the store of the int is one word, the decision is three. And without a handshake, Gate B may not see Gate A’s 13 at all.
int free = 12; // shared, not atomic, not visible
free++; // read 12, add, write 13 — two gates both write 13
AtomicInteger freeSpots = new AtomicInteger(12);
freeSpots.decrementAndGet(); // one step, handshake includedCounting free spots is a second shared fact besides occupancy. Prefer not keeping a redundant counter you must keep in sync with the board. If you must, the counter’s story is the same as occupy: one step, visible.
How to explain this in an interview
“Check-then-act is two steps, so I will CAS occupy. volatile would only publish the flag; both gates could still pass the if. If I write ticketId and occupied, I write the payload first and publish status last, or I CAS a snapshot.” That sentence beats a JMM lecture. If they ask how cores disagree, say: without a handshake the other core is allowed to keep a stale picture, including a torn mix of two fields.
Common mistakes
Treating volatile as a lock
volatile publishes a write. It does not make “if free then occupy” one step. Two gates can still both pass the if.
Assuming the other thread sees your assignment
Without a lock, a volatile, or an atomic, Gate B may keep reading the old occupancy. “I set it” is not “they can see it.”
Reciting the JMM instead of the parking story
The round is LLD. Say: one step, and a handshake so the other gate sees it. Save happens-before graphs for a follow-up if they ask.
Two cores, one Saturday
Picture Gate A on core 0 and Gate B on core 1. Each core has a cached copy of occupied. A writes true into its cache. Until a handshake, B’s cache may still hold false. That is visibility. Now picture A writing ticketId then occupied, and B reading occupied then ticketId. B can see true and null. That is a torn picture. CAS on a single AtomicReference to a small snapshot of occupied plus ticketId glues the pair. A lock around both writes glues the pair. Two volatiles do not, unless you order payload first and flag last, and B reads flag first then payload.
You do not need to draw CPU store buffers in an interview. You need to refuse the claim “I assigned it, so everyone sees it.” The machine did not promise Gate B a finished occupy.
Exercises
- volatile occupied plus if (!occupied) occupied = true. Which problem did you fix? Which remains?
- Gate A writes ticketId then occupied. Gate B reads occupied then ticketId. When is the torn picture possible?
- Replace the boolean with AtomicReference of a snapshot that holds both occupancy and ticket id. What did you glue?
Read next
Where the handshake still slips
Double-checked locking on a non-volatile singleton still publishes a torn lot. A volatile array of spots does not make each ParkingSpot’s fields visible. You published the array reference, not occupy. Final fields in a constructor, then unsafely publishing this, still escape. The handshake has to cover the writes you care about.
Takeaway
Atomicity is one step. Visibility is a handshake so the other core sees a finished picture. volatile publishes; it does not occupy. CAS or a lock can do both for a single flag. Two fields still need an order, a snapshot, or a monitor.