Problem 17 Traffic-Light-Controlled Intersection Synchronization
Mutual exclusion across directions — read the state under the lock
An intersection must never let conflicting directions move at once. The shared "which direction is green" state is
read by every vehicle thread and written by the controller, so every access must be synchronized.
The earlier version read a plain (non-volatile) array without the lock, so a vehicle could act on
a stale or torn value and two directions could cross together — the exact accident the lock exists to prevent. The fix:
guard the state with a lock and let vehicles wait on a condition until their direction is green.
Correct Java
import java.util.concurrent.locks.*;
class Intersection {
private final Lock lock = new ReentrantLock();
private final Condition changed = lock.newCondition();
private int green = 0; // 0 = North-South, 1 = East-West
// controller flips the light atomically
public void switchLight() {
lock.lock();
try { green = 1 - green; changed.signalAll(); }
finally { lock.unlock(); }
}
// a vehicle is *admitted* only when its direction is green; state read UNDER the lock
public void cross(int direction, Runnable drive) throws InterruptedException {
lock.lock();
try {
while (green != direction) changed.await();
// Intentional model A: hold lock through drive → single-car intersection
// (even same-direction cars fully serialized). Simple and safe for the puzzle.
drive.run();
} finally { lock.unlock(); }
}
// Model B: concurrent same-direction flow — release before long drive
public void crossParallelSameDir(int direction, Runnable drive) throws InterruptedException {
lock.lock();
try {
while (green != direction) changed.await();
// light is green for us; do NOT hold lock during slow drive
} finally { lock.unlock(); }
drive.run(); // many NS cars may drive together; EW still blocked until switch
// Caveat: a switchLight during drive may open the other direction while we still move —
// real systems use "all red" clearance time or a generation counter checked under lock.
}
}
Both the read (green != direction) and the write (green = 1 - green) happen while holding
the same lock, so there is a single, consistent view for the decision. Trade-off: holding the lock
for the entire drive models a single-car intersection (safe, easy). Releasing before drive allows
same-direction parallelism but needs a clearance protocol so a switch cannot green the other way mid-cross.
Worked schedule (NS waits, EW drives, then switch)
| # | Event | green | Notes |
|---|---|---|---|
| 1 | Controller starts NS green | 0 (NS) | — |
| 2 | NS car A: green matches → drive (or await if EW) | 0 | under lock in model A |
| 3 | EW car B arrives | 0 | B awaits (green≠EW) |
| 4 | EW car C arrives | 0 | C awaits |
| 5 | switchLight → EW green, signalAll | 1 | NS waiters re-check and park |
| 6 | B re-checks, proceeds | 1 | C still queued on lock or cond |
| 7 | C proceeds after B (model A) or with B (model B) | 1 | same-dir policy choice |
| 8 | switch back to NS; NS car proceeds | 0 | cycle continues |
The mid-cross hazard, and the drain protocol that closes it
Model B trades safety for throughput, and the caveat in the comment is a real accident, not a footnote. Because the
green check and the drive are no longer in the same critical section, a switch can land between
them. Trace it:
| # | Event | green | Who is on the road |
|---|---|---|---|
| 1 | NS car A: takes lock, sees green==NS, releases lock | 0 (NS) | — |
| 2 | A begins slow drive() (lock not held) | 0 | A crossing NS |
| 3 | Controller switchLight() → green=EW, signalAll | 1 (EW) | A still crossing NS |
| 4 | EW car B: takes lock, sees green==EW, releases, drives | 1 | A (NS) and B (EW) crossing together → collision |
The hazard is a check-then-act race across a state change: A's admission decision was valid when made and stale by the time it acted. Widening the lock (Model A) removes it by making admission and drive one atomic step — at the cost of serializing everyone. To keep same-direction parallelism and stay safe you need a drain protocol: the switch must wait for every car admitted under the old green to clear before it greens the other direction. That is exactly what a real intersection's all-red clearance interval is — a fixed gap long enough for the longest legal crossing. In software you make it precise with an in-flight counter instead of a guessed time:
private int green = 0;
private final int[] inFlight = new int[2]; // cars currently crossing, per direction
public void cross(int direction, Runnable drive) throws InterruptedException {
lock.lock();
try {
while (green != direction) changed.await();
inFlight[direction]++; // registered while still green, under the lock
} finally { lock.unlock(); }
drive.run(); // many same-dir cars overlap here
lock.lock();
try { inFlight[direction]--; changed.signalAll(); }
finally { lock.unlock(); }
}
public void switchLight() throws InterruptedException {
lock.lock();
try {
int old = green;
while (inFlight[old] > 0) changed.await(); // DRAIN: wait out everyone still crossing
green = 1 - green; // only now is the flip safe
changed.signalAll();
} finally { lock.unlock(); }
}
Why this is correct: a car increments inFlight[dir] under the lock while its direction is still
green, and the switch cannot flip until inFlight[old] == 0. So the set of cars crossing under green
g is fully drained before any car is ever admitted under the perpendicular green — the two directions can
never overlap on the road, yet same-direction cars still drive concurrently. The generation-counter variant is the same
idea keyed differently: stamp each admitted car with the current green generation and refuse to flip while
any car of the outgoing generation is unfinished. The cost is a throughput ceiling: while draining, the intersection is
effectively all-red, so a stream of long crossings in one direction delays the switch — the same starvation-vs-safety
tension real signals resolve with a maximum green time.
When each model is correct
- Model A (lock through drive) — when a "crossing" is short and cheap (updating a shared cell, touching a small critical region) so full serialization costs nothing, or when you want the simplest provably-safe code. This is the right default for the interview puzzle.
- Model B + drain — when a crossing is long relative to switch frequency and same-direction throughput matters (real traffic, or a resource where many same-class users can proceed together). You accept the extra in-flight bookkeeping and the drain latency to unlock concurrency.
- Model B without a drain — never in anything that must be safe. It is only acceptable when a stale admission is harmless (e.g. best-effort metrics), which the "two cars must not collide" invariant is precisely not.
Correct Go (mutex + cond)
type Intersection struct { mu sync.Mutex; cv *sync.Cond; green int }
func New() *Intersection { x := &Intersection{}; x.cv = sync.NewCond(&x.mu); return x }
func (x *Intersection) Switch() { x.mu.Lock(); x.green = 1 - x.green; x.cv.Broadcast(); x.mu.Unlock() }
func (x *Intersection) Cross(dir int, drive func()) {
x.mu.Lock()
for x.green != dir { x.cv.Wait() }
drive() // model A: serialize under lock
x.mu.Unlock()
}
Alternative: controller owns the light (CSP)
// One goroutine owns green; cars request permission via channels
// req := make(chan int); grant := make(chan struct{})
// controller: for { select { case d := <-req: if d==green { grant<-struct{}{} }; case <-tick: green=1-green }}
// car: req <- dir; <-grant; drive()
// No shared mutable flag — ownership replaces the condition variable.
When NOT shared mutable green: if you already have an event loop / actor for the intersection, channel grants are clearer. Use the lock+cond form when mapping directly from the Java monitor solution.
Pitfalls
- Reading shared state without the lock (the original bug): even a single
int/array read can see a stale value without proper synchronization;volatilefixes visibility but not the check-then-act atomicity you need here — use the lock. - Guard the wait in a
whileloop, not anif: aftersignalAll, only the threads whose direction is now green should proceed; the rest must re-check and wait again. - Lock held for entire drive serializes same-direction traffic — state the model deliberately.
Takeaways
- Read and write shared state under the same lock — a one-sided guard is no guard.
- Vehicles wait on a condition for their green;
signalAllon every switch wakes the right ones. volatile/atomics give visibility but not check-then-act atomicity; here the lock is required.- Choose: serialize under lock (simple) vs release-before-drive (same-dir parallelism + clearance rules).
Re-authored for correctness for this guide (the prior version read unsynchronized state and lacked cross- direction exclusion). See also: Mutex Lock, Condition Variables, Critical Section & Race Condition.
🤖 Don't fully get this? Learn it with Claude
Stuck on Problem 17 Traffic-Light-Controlled Intersection Synchronization? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Progressively stronger hints — you still solve it.
I'm working on the problem **Problem 17 Traffic-Light-Controlled Intersection Synchronization** (Concurrency). Give me a HINT LADDER: start with the tiniest nudge, then wait. Only reveal the next, stronger hint when I ask. Do NOT show the full solution unless I type 'show solution'. Keep me doing the thinking. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
See the technique, not just code.
Explain the optimal approach to **Problem 17 Traffic-Light-Controlled Intersection Synchronization** with a VISUAL walkthrough: trace it on a small concrete example using ASCII art / a step-by-step diagram, narrate what changes each step, then give time & space complexity with a one-line derivation. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Catch bugs, edge cases, sub-optimality.
I'll paste my solution to **Problem 17 Traffic-Light-Controlled Intersection Synchronization**. Review it for correctness, missed edge cases, and time/space complexity, then coach me toward the optimal — don't just rewrite it. Ask me to paste my code now. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Lock in recognition with look-alikes.
Give me 2 problems that use the SAME underlying pattern as **Problem 17 Traffic-Light-Controlled Intersection Synchronization**. For each, let me attempt first, then review my answer and name the trigger signal that reveals the pattern. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.