Synchronization
Synchronization Mechanisms#
Synchronization ensures threads work together correctly when accessing shared resources. Without it, multiple threads modifying the same data simultaneously cause race conditions, corruption, and unpredictable behavior.
Think of synchronization like a restaurant kitchen with one oven. Multiple cooks (threads) need to use it, but only one can access the oven at a time. A kitchen timer system (synchronization) ensures cooks take turns without conflicting, preventing burnt dishes and chaos while keeping the kitchen running smoothly.
Synchronization prevents data corruption, coordinates thread execution, ensures all threads get fair access to resources, and avoids deadlocks where threads endlessly wait for each other.
Synchronization Methods#
Mutual Exclusion Locks (Mutex)#
A mutex ensures only one thread can access protected code or data at a time. When a thread acquires a lock, other threads attempting to acquire the same lock must wait until it's released—like the checkout register at a grocery store, where only one customer (thread) can check out at a time while others wait in line until it's their turn.
import java.util.concurrent.locks.ReentrantLock;
public class MutexExample {
private final ReentrantLock lock = new ReentrantLock();
private int sharedCounter = 0;
public void increment() {
lock.lock();
try {
sharedCounter++;
System.out.println("Counter: " + sharedCounter);
} finally {
lock.unlock();
}
}
public int getCounter() {
lock.lock();
try {
return sharedCounter;
} finally {
lock.unlock();
}
}
}
// Usage
MutexExample example = new MutexExample();
// Create multiple threads that increment the counter
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final counter: " + example.getCounter()); // Output: 10
import java.util.concurrent.locks.ReentrantLock
class MutexExample {
private val lock = ReentrantLock()
private var sharedCounter = 0
fun increment() {
lock.lock()
try {
sharedCounter++
println("Counter: $sharedCounter")
} finally {
lock.unlock()
}
}
fun getCounter(): Int {
lock.lock()
try {
return sharedCounter
} finally {
lock.unlock()
}
}
}
// Usage
val example = MutexExample()
// Create multiple threads that increment the counter
val thread1 = Thread {
repeat(5) { example.increment() }
}
val thread2 = Thread {
repeat(5) { example.increment() }
}
thread1.start()
thread2.start()
thread1.join()
thread2.join()
println("Final counter: ${example.getCounter()}") // Output: 10
TypeScript/JavaScript doesn't support traditional threading or mutex locks. JavaScript runs in a single-threaded event loop model.
Dart doesn't support traditional threading or mutex locks. Dart uses isolates with separate memory spaces that communicate via message passing.
import Foundation
class MutexExample {
private let lock = NSLock()
private var sharedCounter = 0
func increment() {
lock.lock()
defer { lock.unlock() }
sharedCounter += 1
print("Counter: \(sharedCounter)")
}
func getCounter() -> Int {
lock.lock()
defer { lock.unlock() }
return sharedCounter
}
}
// Usage
let example = MutexExample()
// Create multiple threads that increment the counter
let thread1 = Thread {
for _ in 0..<5 {
example.increment()
}
}
let thread2 = Thread {
for _ in 0..<5 {
example.increment()
}
}
thread1.start()
thread2.start()
// Wait for both threads to complete
while !thread1.isFinished || !thread2.isFinished {
Thread.sleep(forTimeInterval: 0.1)
}
print("Final counter: \(example.getCounter())") // Output: 10
import threading
class MutexExample:
def __init__(self):
self._lock = threading.Lock()
self._shared_counter = 0
def increment(self):
with self._lock:
self._shared_counter += 1
print(f"Counter: {self._shared_counter}")
def get_counter(self):
with self._lock:
return self._shared_counter
# Usage
example = MutexExample()
# Create multiple threads that increment the counter
def worker():
for _ in range(5):
example.increment()
thread1 = threading.Thread(target=worker)
thread2 = threading.Thread(target=worker)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"Final counter: {example.get_counter()}") # Output: 10
Semaphores#
Semaphores extend locks to handle multiple identical resources. Think of a parking lot with 10 spaces—the semaphore starts at 10, decrements when a car parks, and increments when a car leaves. When the count reaches zero, new cars must wait for a space to open up.
import java.util.concurrent.Semaphore;
public class DatabaseConnectionPool {
private final Semaphore availableConnections;
private final int maxConnections;
public DatabaseConnectionPool(int maxConnections) {
this.maxConnections = maxConnections;
this.availableConnections = new Semaphore(maxConnections);
}
public void executeQuery(String query) {
try {
availableConnections.acquire(); // Wait for available connection
System.out.println(Thread.currentThread().getName() +
" acquired connection (" + (maxConnections - availableConnections.availablePermits()) +
"/" + maxConnections + " in use)");
// Simulate database query execution
System.out.println(Thread.currentThread().getName() + " executing: " + query);
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " completed query");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
availableConnections.release(); // Return connection to pool
System.out.println(Thread.currentThread().getName() + " released connection");
}
}
}
// Usage: limit to 3 concurrent database connections
DatabaseConnectionPool pool = new DatabaseConnectionPool(3);
for (int i = 0; i < 10; i++) {
final int queryId = i;
new Thread(() -> pool.executeQuery("SELECT * FROM users WHERE id=" + queryId)).start();
}
import java.util.concurrent.Semaphore
class DatabaseConnectionPool(private val maxConnections: Int) {
private val availableConnections = Semaphore(maxConnections)
fun executeQuery(query: String) {
try {
availableConnections.acquire() // Wait for available connection
println("${Thread.currentThread().name} acquired connection " +
"(${maxConnections - availableConnections.availablePermits()}/$maxConnections in use)")
// Simulate database query execution
println("${Thread.currentThread().name} executing: $query")
Thread.sleep(2000)
println("${Thread.currentThread().name} completed query")
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
} finally {
availableConnections.release() // Return connection to pool
println("${Thread.currentThread().name} released connection")
}
}
}
// Usage: limit to 3 concurrent database connections
val pool = DatabaseConnectionPool(3)
repeat(10) { i ->
Thread { pool.executeQuery("SELECT * FROM users WHERE id=$i") }.start()
}
TypeScript/JavaScript doesn't support traditional threading or semaphores. JavaScript runs in a single-threaded event loop model.
Dart doesn't support traditional threading or semaphores. Dart uses isolates with separate memory spaces that communicate via message passing.
import Foundation
class DatabaseConnectionPool {
private let semaphore: DispatchSemaphore
private let maxConnections: Int
init(maxConnections: Int) {
self.maxConnections = maxConnections
self.semaphore = DispatchSemaphore(value: maxConnections)
}
func executeQuery(_ query: String) {
semaphore.wait() // Wait for available connection
// defer ensures signal() is called when function exits (similar to finally block)
defer { semaphore.signal() } // Return connection to pool
let threadName = Thread.current.description
print("\(threadName) acquired connection")
// Simulate database query execution
print("\(threadName) executing: \(query)")
Thread.sleep(forTimeInterval: 2.0)
print("\(threadName) completed query")
print("\(threadName) released connection")
}
}
// Usage: limit to 3 concurrent database connections
let pool = DatabaseConnectionPool(maxConnections: 3)
var threads: [Thread] = []
for i in 0..<10 {
let thread = Thread {
pool.executeQuery("SELECT * FROM users WHERE id=\(i)")
}
thread.start()
threads.append(thread)
}
// Wait for all threads to complete
for thread in threads {
while !thread.isFinished {
Thread.sleep(forTimeInterval: 0.1)
}
}
import threading
import time
class DatabaseConnectionPool:
def __init__(self, max_connections):
self._semaphore = threading.Semaphore(max_connections)
self._max_connections = max_connections
def execute_query(self, query):
self._semaphore.acquire() # Wait for available connection
try:
thread_name = threading.current_thread().name
print(f"{thread_name} acquired connection")
# Simulate database query execution
print(f"{thread_name} executing: {query}")
time.sleep(2)
print(f"{thread_name} completed query")
finally:
self._semaphore.release() # Return connection to pool
print(f"{thread_name} released connection")
# Usage: limit to 3 concurrent database connections
pool = DatabaseConnectionPool(3)
threads = []
for i in range(10):
thread = threading.Thread(
target=pool.execute_query,
args=(f"SELECT * FROM users WHERE id={i}",)
)
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Condition Variables#
Condition variables let threads wait for specific conditions to become true. A thread checks a condition (like "is the queue empty?"), and if false, waits until another thread signals that the condition might have changed. This is perfect for producer-consumer scenarios where one thread produces data and another consumes it.
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionVariableExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;
private int data = 0;
public void producer() {
lock.lock();
try {
// Produce data
data = 42;
ready = true;
System.out.println("Data produced: " + data);
condition.signal(); // Wake up one waiting thread (the consumer)
} finally {
lock.unlock();
}
}
public void consumer() {
lock.lock();
try {
while (!ready) {
System.out.println("Waiting for data...");
condition.await(); // Release lock and wait until signaled
}
System.out.println("Data consumed: " + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
// Usage
ConditionVariableExample example = new ConditionVariableExample();
// Start consumer thread (will wait for data)
Thread consumerThread = new Thread(() -> example.consumer());
consumerThread.start();
// Give consumer time to start waiting
Thread.sleep(1000);
// Start producer thread (will produce data and signal consumer)
Thread producerThread = new Thread(() -> example.producer());
producerThread.start();
// Wait for both threads to complete
consumerThread.join();
producerThread.join();
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.ReentrantLock
class ConditionVariableExample {
private val lock = ReentrantLock()
private val condition = lock.newCondition()
private var ready = false
private var data = 0
fun producer() {
lock.lock()
try {
// Produce data
data = 42
ready = true
println("Data produced: $data")
condition.signal() // Wake up one waiting thread (the consumer)
} finally {
lock.unlock()
}
}
fun consumer() {
lock.lock()
try {
while (!ready) {
println("Waiting for data...")
condition.await() // Release lock and wait until signaled
}
println("Data consumed: $data")
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
} finally {
lock.unlock()
}
}
}
// Usage
val example = ConditionVariableExample()
// Start consumer thread (will wait for data)
val consumerThread = Thread { example.consumer() }
consumerThread.start()
// Give consumer time to start waiting
Thread.sleep(1000)
// Start producer thread (will produce data and signal consumer)
val producerThread = Thread { example.producer() }
producerThread.start()
// Wait for both threads to complete
consumerThread.join()
producerThread.join()
TypeScript/JavaScript doesn't support traditional threading or condition variables. JavaScript runs in a single-threaded event loop model.
Dart doesn't support traditional threading or condition variables. Dart uses isolates with separate memory spaces that communicate via message passing.
import Foundation
class ConditionVariableExample {
private let lock = NSCondition()
private var data: Int = 0
private var ready = false
func producer() {
lock.lock()
defer { lock.unlock() }
// Produce data
self.data = 42
self.ready = true
print("Data produced: \(data)")
lock.signal() // Wake up one waiting thread (the consumer)
}
func consumer() {
lock.lock()
defer { lock.unlock() }
while !ready {
print("Waiting for data...")
lock.wait() // Release lock and wait until signaled
}
print("Data consumed: \(data)")
}
}
// Usage
let example = ConditionVariableExample()
// Start consumer thread (will wait for data)
let consumerThread = Thread {
example.consumer()
}
consumerThread.start()
// Give consumer time to start waiting
Thread.sleep(forTimeInterval: 1.0)
// Start producer thread (will produce data and signal consumer)
let producerThread = Thread {
example.producer()
}
producerThread.start()
// Wait for both threads to complete
while !consumerThread.isFinished || !producerThread.isFinished {
Thread.sleep(forTimeInterval: 0.1)
}
import threading
import time
class ConditionVariableExample:
def __init__(self):
self._lock = threading.Lock()
self._condition = threading.Condition(self._lock)
self._data = None
self._ready = False
def producer(self, data):
with self._condition:
self._data = data
self._ready = True
print(f"Data produced: {data}")
self._condition.notify() # Wake up one waiting thread (the consumer)
def consumer(self):
with self._condition:
while not self._ready:
print("Waiting for data...")
self._condition.wait() # Release lock and wait until signaled
print(f"Data consumed: {self._data}")
# Usage
example = ConditionVariableExample()
# Start consumer thread (will wait for data)
consumer_thread = threading.Thread(target=example.consumer)
consumer_thread.start()
# Give consumer time to start waiting
time.sleep(1)
# Start producer thread (will produce data and signal consumer)
producer_thread = threading.Thread(target=example.producer, args=(42,))
producer_thread.start()
# Wait for both threads to complete
consumer_thread.join()
producer_thread.join()
Atomic Operations#
Atomic operations complete in a single, uninterruptible step without locks. When you increment a counter atomically, the read-modify-write happens as one indivisible operation, preventing race conditions. This provides better performance than locks for simple operations like counters or flags.
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicOperationsExample {
private final AtomicInteger counter = new AtomicInteger(0);
private final AtomicReference<String> value = new AtomicReference<>("initial");
public void incrementCounter() {
int oldValue = counter.getAndIncrement();
System.out.println("Counter incremented from " + oldValue + " to " + counter.get());
}
public void updateValue(String newValue) {
String oldValue = value.getAndSet(newValue);
System.out.println("Value updated from '" + oldValue + "' to '" + newValue + "'");
}
public boolean compareAndSetValue(String expected, String newValue) {
boolean success = value.compareAndSet(expected, newValue);
System.out.println("CAS operation " + (success ? "succeeded" : "failed"));
return success;
}
public int getCounter() {
return counter.get();
}
public String getValue() {
return value.get();
}
}
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
class AtomicOperationsExample {
private val counter = AtomicInteger(0)
private val value = AtomicReference("initial")
fun incrementCounter() {
val oldValue = counter.getAndIncrement()
println("Counter incremented from $oldValue to ${counter.get()}")
}
fun updateValue(newValue: String) {
val oldValue = value.getAndSet(newValue)
println("Value updated from '$oldValue' to '$newValue'")
}
fun compareAndSetValue(expected: String, newValue: String): Boolean {
val success = value.compareAndSet(expected, newValue)
println("CAS operation ${if (success) "succeeded" else "failed"}")
return success
}
fun getCounter(): Int = counter.get()
fun getValue(): String = value.get()
}
TypeScript/JavaScript doesn't support traditional threading or atomic operations. For shared memory scenarios with Web Workers, use SharedArrayBuffer with the Atomics API.
Dart doesn't support traditional threading or atomic operations. Dart uses isolates with separate memory spaces that communicate via message passing.
Swift doesn't provide built-in atomic types in the standard library. For thread-safe operations, use locks (NSLock) to protect shared data, as shown below.
import Foundation
class AtomicOperationsExample {
private let lock = NSLock()
private var counter = 0
func increment() {
lock.lock()
defer { lock.unlock() }
counter += 1
print("Counter: \(counter)")
}
func getCounter() -> Int {
lock.lock()
defer { lock.unlock() }
return counter
}
}
// Usage
let example = AtomicOperationsExample()
example.increment()
Python doesn't provide built-in atomic types in the standard library. For thread-safe operations, use locks (threading.Lock()) to protect shared data, as shown below.
import threading
class AtomicOperationsExample:
def __init__(self):
self._lock = threading.Lock()
self._counter = 0
def increment(self):
with self._lock:
self._counter += 1
print(f"Counter: {self._counter}")
def get_counter(self):
with self._lock:
return self._counter
# Usage
example = AtomicOperationsExample()
example.increment()