SwadhyAI Notes

All Low-Level Design notes

Concurrency · LLD

Avoiding shared state

Thread confinement means only one thread ever touches a piece of mutable state — so other threads cannot race it. Example: the lot has two gates, but only one worker holds the whiteboard of free spots. Gate A and Gate B ask for a compact. The worker picks a rectangle, marks it, and returns a number. The board never leaves that worker’s hands.

The gates never both touch the same list. Nothing is shared that can change. That is : the safest shared state is no shared state.

Who writes?

  • Gates — offer a request. Never walk spots.
  • Allocator thread — owns the list. Picks, marks, replies.
  • SMS worker — owns the modem. Reads an immutable note.

The real question

Does this object need to be seen by two threads at all?

Ownership flow

Request in, number out

01

Gate offers

ParkRequest on an inbox. Gate does not hold the list.

02

Allocator takes

One thread. One board. Pick a free compact.

03

Mark and reply

Occupy on this thread only. Complete the future.

04

Gate prints

Ticket from the reply. Printer is local to the gate.

Three honest flavors

You already confine without naming it. The design choice is to keep doing that on purpose when two gates appear, instead of sharing a HashMap “just for convenience.”

Stack / method

A local ArrayList inside park() dies with the call. No other thread has the reference. Do not stash it on a field.

Hand-off

Build the Ticket, put it on a queue, stop writing it. The worker is the new . Prefer an immutable event so the rule is structural.

Owner thread

One or allocator owns the board for the process lifetime. Gates never get a setter on spots.

One thread owns the board

Reason first: if only one thread writes spots, occupy cannot race. Java’s BlockingQueue is how gates talk to that thread, not how they share the list. The inbox is the shared state; the list is not.

java
class Allocator {
  private final BlockingQueue<ParkRequest> inbox = new ArrayBlockingQueue<>(64);
  private final List<ParkingSpot> spots;   // only this thread touches spots

  void run() {
    while (true) {
      ParkRequest req = inbox.take();
      ParkingSpot spot = pickFree(spots, req.vehicle);
      req.reply.complete(spot);
    }
  }
}

// Gates only offer a request. They never walk spots.

pickFree can be an ordinary loop. No CAS. No synchronized. The cost is latency: the gate waits for a reply. The gain is a board that cannot lie. If Saturday needs two allocators, shard by floor — that is , not a shared list with a comment.

ThreadLocal is not a backpack

looks like confinement. Each thread has a slot. That helps for a mutable legacy formatter that is not thread-safe. It does not help for DateTimeFormatter, which is immutable and already safe to share. A mutable Ticket in a slot is worse: if you forget to remove it, a pooled worker later reads another driver’s ticket. Pass the object in the call. Clear the slot in a finally if you must use one.

java
// Legacy mutable formatter: not thread-safe — confine it if you must keep it.
private static final ThreadLocal<SimpleDateFormat> LEGACY =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss"));

// Modern formatter: immutable and thread-safe — share one instance.
private static final DateTimeFormatter FMT = DateTimeFormatter.ISO_LOCAL_TIME;

// Not confined: a live Ticket in a pool thread’s slot.
ThreadLocal<Ticket> current = new ThreadLocal<>();
current.set(ticket);   // another park() on this worker will see it
// missing current.remove() in finally → leak across requests

When confinement is the wrong story

If two gates must allocate in parallel on one shared list, you cannot confine that list. You either shard it, or you make occupy thread-safe. Pretending “we only test with one gate” is a comment, not a design. The public API is park(Vehicle). Two callers may enter.

Confine

One allocator, or one event loop, or a thread-local buffer. Throughput is limited by that thread. Correctness is cheap.

Share on purpose

CAS on each spot, or a lock around pick. Parallel gates. You accepted shared mutable occupancy and gave it a story.

Hand-off rules you can say out loud

  1. 01

    Ask who writes

    If only one thread writes, confine. Do not lock a private list.

  2. 02

    Hand off, then stop

    After you put the ticket on a queue, do not mutate it. That is the hand-off.

  3. 03

    Name the owner

    One allocator thread, or one event loop, is clearer than ThreadLocal.

  4. 04

    Do not share for convenience

    A HashMap on ParkingLot that every gate writes is shared mutable state.

this-escape and inner-class leaks

Stack confinement dies the moment a reference leaves the stack. An inner class that captures this and is passed to a worker is a leak. Publishing this from a constructor — registering a listener before the object is finished — lets another thread see a half-built lot. Confine means the reference never becomes shared mutable state, not “it started as a local.”

java
class ParkingLot {
  ParkingLot(Notifier n) {
    n.listen(this);   // this-escape: worker may call park() mid-construction
  }
}

void park(Vehicle v) {
  List<ParkingSpot> local = spots;     // still a shared list if spots is a field
  worker.execute(() -> local.get(0).occupy());  // escaped
}

Common mistakes

Sharing because it is handy

A HashMap on ParkingLot that every gate reads and writes is shared mutable state. Convenience is how the two-ticket bug arrives.

ThreadLocal as a secret backpack

If you stuff a mutable Ticket into ThreadLocal and another thread later reads it, you did not confine. You hid a global. Pass the object in the call.

Mutating after the put

The is the moment you stop writing. A later setStatus from the producer races the consumer. Freeze the payload, or copy it.

Actors, without the buzzword

An allocator thread with an inbox is an actor: one mailbox, one mutable world, messages in, replies out. You do not need Akka. You need a BlockingQueue and a loop. Sharding two floors onto two allocators is two actors. A car moving floors is a message from actor 1 to actor 2, not a shared HashMap. If the interviewer says “that is just a queue,” agree. Confinement is the reason the queue exists.

Single-thread event loops in UI and Netty are the same idea at a larger scale. LLD does not require that architecture. It requires you to notice when sharing is optional.

Exercises

  1. Two floors, two allocator threads, one vehicle that must move from Floor 1 to Floor 2. What protocol replaces a shared list?
  2. A worker pool of four. You put Ticket in ThreadLocal at the start of park(). Why is that not confinement?
  3. After inbox.put(req), the gate still calls spots.get(0).occupy(). Which rule did you break?

Read next

Takeaway

Ask who writes. If the answer is one thread, confine the board and let gates shout at an inbox. Hand off, then stop mutating. ThreadLocal is a slot, not an owner. Share only when you have a race story for the shared thing.