Skip to content

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()