Synchronization Primitives
Introduction: Why Synchronization?
Section titled “Introduction: Why Synchronization?”When multiple threads access shared data concurrently, we need synchronization primitives to ensure correctness and prevent race conditions. These tools are the foundation of thread-safe programming.
Visual: The Problem Without Synchronization
Section titled “Visual: The Problem Without Synchronization”Critical Sections and Race Conditions
Section titled “Critical Sections and Race Conditions”What is a Critical Section?
Section titled “What is a Critical Section?”A critical section is a code segment that accesses shared resources and must be executed atomically (as a single, indivisible operation) to prevent race conditions.
Visual: Critical Section
Section titled “Visual: Critical Section”Example: Race Condition
Section titled “Example: Race Condition”1import threading2
3# Shared counter4counter = 05
6def increment():7 global counter8 # Critical section - NOT protected!9 for _ in range(100000):10 counter += 1 # Not atomic: read-modify-write11
12# Create multiple threads13threads = []14for _ in range(5):15 thread = threading.Thread(target=increment)16 threads.append(thread)17 thread.start()18
19for thread in threads:20 thread.join()21
22print(f"Expected: 500000, Got: {counter}")23# Output: Expected: 500000, Got: 342156 (WRONG!)1public class RaceCondition {2 private static int counter = 0;3
4 public static void increment() {5 // Critical section - NOT protected!6 for (int i = 0; i < 100000; i++) {7 counter++; // Not atomic: read-modify-write8 }9 }10
11 public static void main(String[] args) throws InterruptedException {12 Thread[] threads = new Thread[5];13
14 for (int i = 0; i < 5; i++) {15 threads[i] = new Thread(() -> increment());16 threads[i].start();17 }18
19 for (Thread thread : threads) {20 thread.join();21 }22
23 System.out.println("Expected: 500000, Got: " + counter);24 // Output: Expected: 500000, Got: 342156 (WRONG!)25 }26}Mutual Exclusion: Locks (Mutex)
Section titled “Mutual Exclusion: Locks (Mutex)”Locks (also called mutexes) provide mutual exclusion—ensuring only one thread can execute a critical section at a time.
Visual: How Locks Work
Section titled “Visual: How Locks Work”Basic Lock Usage
Section titled “Basic Lock Usage”1import threading2
3counter = 04lock = threading.Lock() # Create a lock5
6def increment():7 global counter8 for _ in range(100000):9 lock.acquire() # Acquire lock10 try:11 counter += 1 # Critical section - protected!12 finally:13 lock.release() # Always release lock14
15# Or use context manager (recommended)16def increment_safe():17 global counter18 for _ in range(100000):19 with lock: # Automatically acquire/release20 counter += 1 # Critical section21
22threads = []23for _ in range(5):24 thread = threading.Thread(target=increment_safe)25 threads.append(thread)26 thread.start()27
28for thread in threads:29 thread.join()30
31print(f"Expected: 500000, Got: {counter}")32# Output: Expected: 500000, Got: 500000 (CORRECT!)1import java.util.concurrent.locks.ReentrantLock;2
3public class LockExample {4 private static int counter = 0;5 private static ReentrantLock lock = new ReentrantLock();6
7 public static void increment() {8 for (int i = 0; i < 100000; i++) {9 lock.lock(); // Acquire lock10 try {11 counter++; // Critical section - protected!12 } finally {13 lock.unlock(); // Always release lock14 }15 }16 }17
18 public static void main(String[] args) throws InterruptedException {19 Thread[] threads = new Thread[5];20
21 for (int i = 0; i < 5; i++) {22 threads[i] = new Thread(() -> increment());23 threads[i].start();24 }25
26 for (Thread thread : threads) {27 thread.join();28 }29
30 System.out.println("Expected: 500000, Got: " + counter);31 // Output: Expected: 500000, Got: 500000 (CORRECT!)32 }33}Reentrant Locks
Section titled “Reentrant Locks”A reentrant lock allows the same thread to acquire the lock multiple times without deadlocking. This is useful for recursive methods or when calling other methods that need the same lock.
Visual: Reentrant Lock
Section titled “Visual: Reentrant Lock”Example: Reentrant Lock
Section titled “Example: Reentrant Lock”1import threading2
3lock = threading.RLock() # Reentrant Lock4
5def outer_function():6 with lock: # First acquisition7 print("Outer: Lock acquired")8 inner_function() # Calls inner function9 print("Outer: Lock released")10
11def inner_function():12 with lock: # Second acquisition (same thread!)13 print("Inner: Lock acquired (reentrant)")14 # Do work15 print("Inner: Lock released")16
17# This works without deadlock!18outer_function()1import java.util.concurrent.locks.ReentrantLock;2
3public class ReentrantLockExample {4 private static ReentrantLock lock = new ReentrantLock();5
6 public static void outerMethod() {7 lock.lock(); // First acquisition8 try {9 System.out.println("Outer: Lock acquired");10 innerMethod(); // Calls inner method11 } finally {12 lock.unlock();13 System.out.println("Outer: Lock released");14 }15 }16
17 public static void innerMethod() {18 lock.lock(); // Second acquisition (same thread!)19 try {20 System.out.println("Inner: Lock acquired (reentrant)");21 // Do work22 } finally {23 lock.unlock();24 System.out.println("Inner: Lock released");25 }26 }27
28 public static void main(String[] args) {29 outerMethod(); // Works without deadlock!30 }31}synchronized vs ReentrantLock (Java)
Section titled “synchronized vs ReentrantLock (Java)”Java provides two ways to achieve mutual exclusion:
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Syntax | Keyword (implicit) | Class (explicit) |
| Fairness | No | Yes (optional) |
| Try Lock | No | Yes (tryLock()) |
| Interruptible | No | Yes (lockInterruptibly()) |
| Condition Support | Limited | Full (newCondition()) |
| Performance | Slightly faster | Slightly slower |
Example: synchronized vs ReentrantLock
Section titled “Example: synchronized vs ReentrantLock”1public class SynchronizedExample {2 private int counter = 0;3
4 // Implicit locking with synchronized5 public synchronized void increment() {6 counter++;7 }8
9 // Synchronized block10 public void incrementBlock() {11 synchronized (this) {12 counter++;13 }14 }15}1import java.util.concurrent.locks.ReentrantLock;2
3public class ReentrantLockExample {4 private int counter = 0;5 private ReentrantLock lock = new ReentrantLock(true); // Fair lock6
7 // Explicit locking8 public void increment() {9 lock.lock();10 try {11 counter++;12 } finally {13 lock.unlock();14 }15 }16
17 // Try lock (non-blocking)18 public boolean tryIncrement() {19 if (lock.tryLock()) {20 try {21 counter++;22 return true;23 } finally {24 lock.unlock();25 }26 }27 return false; // Lock not available28 }29}Resource Limiting: Semaphores
Section titled “Resource Limiting: Semaphores”A semaphore controls access to a resource with a counter. Unlike a lock (which allows only one thread), a semaphore can allow N threads to access a resource simultaneously.
Visual: Semaphore Concept
Section titled “Visual: Semaphore Concept”Binary Semaphore vs Counting Semaphore
Section titled “Binary Semaphore vs Counting Semaphore”- Binary Semaphore: Count = 1 (similar to a lock, but can be released by different thread)
- Counting Semaphore: Count = N (allows N concurrent accesses)
Example: Rate Limiter with Semaphore
Section titled “Example: Rate Limiter with Semaphore”1import threading2import time3
4class RateLimiter:5 def __init__(self, max_requests, time_window):6 self.semaphore = threading.Semaphore(max_requests)7 self.time_window = time_window8 self.last_reset = time.time()9 self.lock = threading.Lock()10
11 def acquire(self):12 """Acquire a permit, blocking if necessary"""13 self.semaphore.acquire()14 with self.lock:15 current_time = time.time()16 if current_time - self.last_reset >= self.time_window:17 # Reset permits18 for _ in range(self.semaphore._value):19 self.semaphore.release()20 self.last_reset = current_time21
22 def release(self):23 """Release a permit"""24 self.semaphore.release()25
26# Usage: Allow max 5 concurrent requests27limiter = RateLimiter(max_requests=5, time_window=1.0)28
29def make_request(request_id):30 limiter.acquire()31 try:32 print(f"Request {request_id} processing...")33 time.sleep(0.5) # Simulate work34 finally:35 limiter.release()36 print(f"Request {request_id} completed")37
38# Create 10 threads39threads = []40for i in range(10):41 thread = threading.Thread(target=make_request, args=(i,))42 threads.append(thread)43 thread.start()44
45for thread in threads:46 thread.join()1import java.util.concurrent.Semaphore;2import java.util.concurrent.TimeUnit;3
4public class RateLimiter {5 private final Semaphore semaphore;6 private final int maxRequests;7 private final long timeWindowMillis;8 private long lastReset;9 private final Object lock = new Object();10
11 public RateLimiter(int maxRequests, long timeWindowMillis) {12 this.maxRequests = maxRequests;13 this.timeWindowMillis = timeWindowMillis;14 this.semaphore = new Semaphore(maxRequests);15 this.lastReset = System.currentTimeMillis();16 }17
18 public void acquire() throws InterruptedException {19 semaphore.acquire();20 synchronized (lock) {21 long currentTime = System.currentTimeMillis();22 if (currentTime - lastReset >= timeWindowMillis) {23 // Reset permits24 int available = maxRequests - semaphore.availablePermits();25 semaphore.release(available);26 lastReset = currentTime;27 }28 }29 }30
31 public void release() {32 semaphore.release();33 }34
35 public static void main(String[] args) throws InterruptedException {36 RateLimiter limiter = new RateLimiter(5, 1000); // 5 requests per second37
38 for (int i = 0; i < 10; i++) {39 final int requestId = i;40 new Thread(() -> {41 try {42 limiter.acquire();43 System.out.println("Request " + requestId + " processing...");44 Thread.sleep(500);45 limiter.release();46 System.out.println("Request " + requestId + " completed");47 } catch (InterruptedException e) {48 Thread.currentThread().interrupt();49 }50 }).start();51 }52 }53}Coordination: Condition Variables
Section titled “Coordination: Condition Variables”Condition variables allow threads to wait for a specific condition to become true, enabling efficient thread coordination.
Visual: Condition Variable Pattern
Section titled “Visual: Condition Variable Pattern”Example: Producer-Consumer with Condition Variables
Section titled “Example: Producer-Consumer with Condition Variables”1import threading2import time3
4class BoundedBuffer:5 def __init__(self, capacity):6 self.capacity = capacity7 self.buffer = []8 self.lock = threading.Lock()9 self.not_full = threading.Condition(self.lock)10 self.not_empty = threading.Condition(self.lock)11
12 def put(self, item):13 with self.lock:14 # Wait while buffer is full15 while len(self.buffer) >= self.capacity:16 self.not_full.wait() # Releases lock, waits17
18 self.buffer.append(item)19 print(f"Produced: {item}")20 self.not_empty.notify() # Wake up consumer21
22 def get(self):23 with self.lock:24 # Wait while buffer is empty25 while len(self.buffer) == 0:26 self.not_empty.wait() # Releases lock, waits27
28 item = self.buffer.pop(0)29 print(f"Consumed: {item}")30 self.not_full.notify() # Wake up producer31 return item32
33# Usage34buffer = BoundedBuffer(capacity=5)35
36def producer():37 for i in range(10):38 buffer.put(i)39 time.sleep(0.1)40
41def consumer():42 for _ in range(10):43 buffer.get()44 time.sleep(0.2)45
46threading.Thread(target=producer).start()47threading.Thread(target=consumer).start()1import java.util.LinkedList;2import java.util.Queue;3import java.util.concurrent.locks.Condition;4import java.util.concurrent.locks.Lock;5import java.util.concurrent.locks.ReentrantLock;6
7public class ConditionExample {8 static class BoundedBuffer {9 private final Queue<Integer> buffer = new LinkedList<>();10 private final int capacity;11 private final Lock lock = new ReentrantLock();12 private final Condition notFull = lock.newCondition();13 private final Condition notEmpty = lock.newCondition();14
15 public BoundedBuffer(int capacity) {16 this.capacity = capacity;17 }18
19 public void put(int item) throws InterruptedException {20 lock.lock();21 try {22 // Wait while buffer is full23 while (buffer.size() >= capacity) {24 notFull.await(); // Releases lock, waits25 }26 buffer.offer(item);27 System.out.println("Produced: " + item);28 notEmpty.signal(); // Wake up consumer29 } finally {30 lock.unlock();31 }32 }33
34 public int get() throws InterruptedException {35 lock.lock();36 try {37 // Wait while buffer is empty38 while (buffer.isEmpty()) {39 notEmpty.await(); // Releases lock, waits40 }41 int item = buffer.poll();42 System.out.println("Consumed: " + item);43 notFull.signal(); // Wake up producer44 return item;45 } finally {46 lock.unlock();47 }48 }49 }50
51 public static void main(String[] args) {52 BoundedBuffer buffer = new BoundedBuffer(5);53
54 new Thread(() -> {55 try {56 for (int i = 0; i < 10; i++) {57 buffer.put(i);58 Thread.sleep(100);59 }60 } catch (InterruptedException e) {61 Thread.currentThread().interrupt();62 }63 }).start();64
65 new Thread(() -> {66 try {67 for (int i = 0; i < 10; i++) {68 buffer.get();69 Thread.sleep(200);70 }71 } catch (InterruptedException e) {72 Thread.currentThread().interrupt();73 }74 }).start();75 }76}notify() vs notifyAll()
Section titled “notify() vs notifyAll()”notify(): Wakes up one waiting thread (unpredictable which one)notifyAll(): Wakes up all waiting threads (they compete for the lock)
Coordination: Barriers and Latches
Section titled “Coordination: Barriers and Latches”Barriers
Section titled “Barriers”A barrier makes threads wait until all threads reach a synchronization point.
Visual: Barrier
Section titled “Visual: Barrier”Example: Barrier
Section titled “Example: Barrier”1import threading2
3barrier = threading.Barrier(3) # 3 threads must wait4
5def worker(worker_id):6 print(f"Worker {worker_id}: Phase 1")7 barrier.wait() # Wait for all threads8 print(f"Worker {worker_id}: Phase 2 (all arrived!)")9
10threads = []11for i in range(3):12 thread = threading.Thread(target=worker, args=(i,))13 threads.append(thread)14 thread.start()15
16for thread in threads:17 thread.join()1import java.util.concurrent.CyclicBarrier;2
3public class BarrierExample {4 public static void main(String[] args) {5 CyclicBarrier barrier = new CyclicBarrier(3); // 3 threads must wait6
7 for (int i = 0; i < 3; i++) {8 final int workerId = i;9 new Thread(() -> {10 try {11 System.out.println("Worker " + workerId + ": Phase 1");12 barrier.await(); // Wait for all threads13 System.out.println("Worker " + workerId + ": Phase 2 (all arrived!)");14 } catch (Exception e) {15 e.printStackTrace();16 }17 }).start();18 }19 }20}CountDownLatch (Java)
Section titled “CountDownLatch (Java)”A CountDownLatch is a one-time synchronization point. Threads wait until a count reaches zero.
1import java.util.concurrent.CountDownLatch;2
3public class CountDownLatchExample {4 public static void main(String[] args) throws InterruptedException {5 CountDownLatch latch = new CountDownLatch(3); // Count starts at 36
7 // Worker threads8 for (int i = 0; i < 3; i++) {9 final int workerId = i;10 new Thread(() -> {11 try {12 System.out.println("Worker " + workerId + " working...");13 Thread.sleep(1000);14 latch.countDown(); // Decrement count15 System.out.println("Worker " + workerId + " finished");16 } catch (InterruptedException e) {17 Thread.currentThread().interrupt();18 }19 }).start();20 }21
22 System.out.println("Main thread waiting...");23 latch.await(); // Wait until count reaches 024 System.out.println("All workers finished! Main thread proceeds.");25 }26}Read Optimization: Read-Write Locks
Section titled “Read Optimization: Read-Write Locks”Read-Write Locks optimize for read-heavy workloads by allowing multiple readers or a single writer.
Visual: Read-Write Lock
Section titled “Visual: Read-Write Lock”Example: Thread-Safe Cache with Read-Write Lock
Section titled “Example: Thread-Safe Cache with Read-Write Lock”1import java.util.HashMap;2import java.util.Map;3import java.util.concurrent.locks.ReadWriteLock;4import java.util.concurrent.locks.ReentrantReadWriteLock;5
6public class ReadWriteLockExample {7 private final Map<String, String> cache = new HashMap<>();8 private final ReadWriteLock lock = new ReentrantReadWriteLock();9
10 public String get(String key) {11 lock.readLock().lock(); // Multiple readers allowed12 try {13 return cache.get(key);14 } finally {15 lock.readLock().unlock();16 }17 }18
19 public void put(String key, String value) {20 lock.writeLock().lock(); // Exclusive access21 try {22 cache.put(key, value);23 } finally {24 lock.writeLock().unlock();25 }26 }27
28 public static void main(String[] args) {29 ReadWriteLockExample cache = new ReadWriteLockExample();30
31 // Multiple readers can read simultaneously32 for (int i = 0; i < 10; i++) {33 final int readerId = i;34 new Thread(() -> {35 String value = cache.get("key");36 System.out.println("Reader " + readerId + " read: " + value);37 }).start();38 }39
40 // Writer has exclusive access41 new Thread(() -> {42 cache.put("key", "value");43 System.out.println("Writer updated cache");44 }).start();45 }46}Thread-Local Storage
Section titled “Thread-Local Storage”Thread-Local Storage provides each thread with its own independent copy of a variable, avoiding the need to pass parameters through method calls.
Visual: Thread-Local Storage
Section titled “Visual: Thread-Local Storage”Example: Request Context with ThreadLocal
Section titled “Example: Request Context with ThreadLocal”1import threading2
3# Thread-local storage4request_context = threading.local()5
6def set_user(user_id):7 request_context.user_id = user_id8 request_context.request_id = threading.current_thread().name9
10def get_user():11 return getattr(request_context, 'user_id', None)12
13def process_request(user_id):14 set_user(user_id)15 print(f"Thread {threading.current_thread().name}: "16 f"Processing request for user {get_user()}")17
18# Each thread has its own context19threads = []20for i in range(3):21 thread = threading.Thread(22 target=process_request,23 args=(f"user-{i}",),24 name=f"Thread-{i}"25 )26 threads.append(thread)27 thread.start()28
29for thread in threads:30 thread.join()1public class ThreadLocalExample {2 private static ThreadLocal<String> userContext = new ThreadLocal<>();3 private static ThreadLocal<Integer> requestId = new ThreadLocal<>();4
5 public static void setUser(String userId) {6 userContext.set(userId);7 requestId.set((int) Thread.currentThread().getId());8 }9
10 public static String getUser() {11 return userContext.get();12 }13
14 public static void processRequest(String userId) {15 setUser(userId);16 System.out.println("Thread " + Thread.currentThread().getName() +17 ": Processing request for user " + getUser());18 }19
20 public static void main(String[] args) throws InterruptedException {21 Thread[] threads = new Thread[3];22
23 for (int i = 0; i < 3; i++) {24 final int userId = i;25 threads[i] = new Thread(() -> {26 processRequest("user-" + userId);27 }, "Thread-" + i);28 threads[i].start();29 }30
31 for (Thread thread : threads) {32 thread.join();33 }34 }35}Memory Visibility: Volatile (Java)
Section titled “Memory Visibility: Volatile (Java)”The volatile keyword in Java ensures visibility of changes across threads and prevents certain compiler optimizations.
Visual: Volatile Visibility
Section titled “Visual: Volatile Visibility”Example: Volatile Flag
Section titled “Example: Volatile Flag”1public class VolatileExample {2 private volatile boolean flag = false; // Volatile ensures visibility3
4 public void setFlag() {5 flag = true; // Write is immediately visible to all threads6 }7
8 public boolean getFlag() {9 return flag; // Read sees the latest value10 }11
12 public static void main(String[] args) throws InterruptedException {13 VolatileExample example = new VolatileExample();14
15 // Reader thread16 Thread reader = new Thread(() -> {17 while (!example.getFlag()) {18 // Busy wait - will see flag change immediately19 }20 System.out.println("Flag is now true!");21 });22
23 reader.start();24 Thread.sleep(1000);25
26 // Writer thread27 example.setFlag(); // This change is immediately visible28
29 reader.join();30 }31}Comparison Table
Section titled “Comparison Table”| Primitive | Use Case | Java | Python | When to Use |
|---|---|---|---|---|
| Lock/Mutex | Mutual exclusion | ReentrantLock | threading.Lock | Protect critical sections |
| Reentrant Lock | Recursive locking | ReentrantLock | threading.RLock | Same thread needs lock multiple times |
| Read-Write Lock | Read-heavy workloads | ReentrantReadWriteLock | Limited support | Many readers, few writers |
| Semaphore | Limit concurrent access | Semaphore | threading.Semaphore | Rate limiting, resource pools |
| Condition | Thread coordination | Condition | threading.Condition | Wait for conditions (Producer-Consumer) |
| Barrier | Synchronization point | CyclicBarrier | threading.Barrier | All threads wait, then proceed together |
| Latch | One-time sync | CountDownLatch | N/A | Wait for N events to complete |
| Thread-Local | Per-thread variables | ThreadLocal | threading.local() | Avoid parameter passing |
| Volatile | Memory visibility | volatile | N/A | Simple flags, single variable updates |
Practice Problems
Section titled “Practice Problems”Easy: Thread-Safe Counter
Section titled “Easy: Thread-Safe Counter”Design a thread-safe counter that supports increment(), decrement(), and get() operations.
Solution
1import threading2
3class ThreadSafeCounter:4 def __init__(self):5 self._value = 06 self._lock = threading.Lock()7
8 def increment(self):9 with self._lock:10 self._value += 111
12 def decrement(self):13 with self._lock:14 self._value -= 115
16 def get(self):17 with self._lock:18 return self._value1import java.util.concurrent.locks.ReentrantLock;2
3public class ThreadSafeCounter {4 private int value = 0;5 private final ReentrantLock lock = new ReentrantLock();6
7 public void increment() {8 lock.lock();9 try {10 value++;11 } finally {12 lock.unlock();13 }14 }15
16 public void decrement() {17 lock.lock();18 try {19 value--;20 } finally {21 lock.unlock();22 }23 }24
25 public int get() {26 lock.lock();27 try {28 return value;29 } finally {30 lock.unlock();31 }32 }33}Medium: Rate Limiter with Semaphore
Section titled “Medium: Rate Limiter with Semaphore”Implement a rate limiter that allows N requests per second using semaphores.
Solution
See the Rate Limiter example in the Semaphores section above.
Interview Questions
Section titled “Interview Questions”Q1: “What’s the difference between synchronized and ReentrantLock?”
Section titled “Q1: “What’s the difference between synchronized and ReentrantLock?””Answer:
synchronized: Implicit locking keyword, simpler syntax, slightly faster, but less flexibleReentrantLock: Explicit lock class, more features (fairness, tryLock, interruptible), more control, slightly slower- When to use: Use
synchronizedfor simple cases,ReentrantLockwhen you need advanced features
Q2: “When would you use a semaphore vs a lock?”
Section titled “Q2: “When would you use a semaphore vs a lock?””Answer:
- Lock (Mutex): Allows only ONE thread at a time (mutual exclusion)
- Semaphore: Allows N threads at a time (resource limiting)
- Use semaphore: When you need to limit concurrent access to a resource (e.g., database connections, API rate limiting)
- Use lock: When you need exclusive access to shared data
Q3: “Explain the difference between notify() and notifyAll().”
Section titled “Q3: “Explain the difference between notify() and notifyAll().””Answer:
notify(): Wakes up ONE waiting thread (unpredictable which one)notifyAll(): Wakes up ALL waiting threads (they compete for the lock)- Use
notify(): When only one thread can proceed (e.g., single consumer) - Use
notifyAll(): When multiple threads might proceed (e.g., multiple consumers, complex conditions)
Q4: “What is ThreadLocal and when would you use it?”
Section titled “Q4: “What is ThreadLocal and when would you use it?””Answer:
- ThreadLocal: Provides each thread with its own independent copy of a variable
- Use cases: Request context in web servers, avoiding parameter passing, per-thread configuration
- Benefits: No synchronization needed, thread-safe by design
- Caution: Can cause memory leaks in thread pools if not cleaned up
Key Takeaways
Section titled “Key Takeaways”Next Steps
Section titled “Next Steps”Continue learning concurrency patterns:
- Producer-Consumer Pattern - Apply condition variables in a real pattern
- Thread Pools & Executors - Efficient resource management
Mastering synchronization primitives is essential for building thread-safe systems! 🔒