1. Solving the Race

To fix the race condition, we need to ensure Mutual Exclusion. We need a “lock” that prevents two threads from entering the Critical Section simultaneously.

But how do we implement a lock? If we just use a boolean flag:

while (locked == true); // Wait
locked = true; // Enter
// Critical Section
locked = false; // Exit

This fails! The check while(locked == true) and the set locked = true are distinct operations. Two threads can both see locked == false and enter together.

2. Hardware Primitives: The Secret Sauce

We cannot build a lock using just software (on modern CPUs). We need help from the hardware. The CPU provides Atomic Instructions that perform a Read-Modify-Write in a single, indivisible step.

1. Test-and-Set (TAS)

Atomically writes 1 (true) to a memory location and returns its old value.

boolean TestAndSet(boolean *lock) {
    boolean old = *lock;
    *lock = true;
    return old;
}

2. Compare-and-Swap (CAS)

The basis of all modern synchronization (including Java’s AtomicInteger). Atomically checks if a value matches expected, and if so, updates it to new.

boolean CAS(int *ptr, int expected, int new) {
    if (*ptr == expected) {
        *ptr = new;
        return true;
    }
    return false;
}

3. Interactive: CAS Visualizer

Understand the atomic nature of Compare-And-Swap. Try to update the value from 0 to 1. Then try to update it again assuming it’s still 0 (which will fail).

Memory Address 0x1A
Current Value
0
CAS(Expected, New)
Ready...

4. Spinlocks vs Mutexes

Once we have atomic instructions, we can build locks.

Analogy: The Fitting Room Imagine a clothing store with a single fitting room. The “Critical Section” is the room itself.

  • Spinlock: You stand right outside the door, repeatedly jiggling the handle to see if the person is done. You waste your energy (CPU cycles), but the moment they step out, you get in immediately (zero latency).
  • Mutex: You give your name to an attendant and go sit on a bench. The OS scheduler has put you to sleep. When the room is free, the attendant walks over and wakes you up. This saves your energy, but there is a slight delay (latency) in getting back up and walking to the room.

1. The Spinlock (Busy Waiting)

The thread runs a loop checking the lock status repeatedly.

while (CAS(&lock, 0, 1) == false); // Spin
  • Pros: Zero latency if the lock is held briefly. No context switch.
  • Cons: Wastes CPU cycles (“spinning”).
  • Use Case: Kernel interrupt handlers, low-level driver code.

2. The Mutex (Sleeping)

If the lock is busy, the thread Yields the CPU and tells the OS Scheduler to put it in the Waiting Queue.

  • Pros: Efficient use of CPU. Other threads can run.
  • Cons: High latency. A Context Switch takes ~1-5 microseconds (thousands of CPU cycles).
  • Use Case: Application level code (Java synchronized, Go Mutex).

5. Code Example: Using Locks

Always use the standard library locks. Do not implement your own spinlock in user-space code.

Java

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SafeCounter {
  private int count = 0;
  private final Lock lock = new ReentrantLock();

  public void increment() {
    lock.lock(); // Acquire lock
    try {
      count++; // Critical Section
    } finally {
      lock.unlock(); // Always unlock in finally!
    }
  }
}

Go

package main

import (
  "sync"
)

type SafeCounter struct {
  mu    sync.Mutex
  count int
}

func (c *SafeCounter) Increment() {
  c.mu.Lock()   // Acquire lock
  defer c.mu.Unlock() // Ensure unlock happens
  c.count++     // Critical Section
}