data-driven-docs

Living documentation for evolving technologies

View on GitHub

Thread Synchronization

Advanced Java 1.0+ Est. Time: 20min


Table of Contents


Overview

Thread synchronization is a mechanism to control access to shared resources in a multi-threaded environment, ensuring that only one thread can access a resource at a time when necessary.

Key Concepts:

Why Synchronization Matters:

// Without synchronization - UNSAFE!
class Counter {
    private int count = 0;

    public void increment() {
        count++;  // Read-Modify-Write: NOT atomic!
    }
}

// With synchronization - SAFE
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;  // Now thread-safe
    }
}

Back to top


The Need for Synchronization

Race Condition Example

Without synchronization, multiple threads accessing shared data can cause race conditions:

public class BankAccount {
    private int balance = 1000;

    // UNSAFE - Race condition!
    public void withdraw(int amount) {
        if (balance >= amount) {           // Thread 1 checks: 1000 >= 500 ✓
            // Context switch here!
            balance = balance - amount;     // Thread 1: 1000 - 500 = 500
        }                                   // Thread 2: 1000 - 600 = 400 (Lost update!)
    }
}

Problem:

  1. Thread 1 checks balance (1000 >= 500) ✓
  2. Thread 1 gets interrupted before update
  3. Thread 2 checks balance (1000 >= 600) ✓
  4. Thread 2 withdraws 600 → balance = 400
  5. Thread 1 resumes, withdraws 500 → balance = -100 (Overdrawn!)

Solution with Synchronization

public class BankAccount {
    private int balance = 1000;

    // SAFE - synchronized ensures atomicity
    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance = balance - amount;  // Atomic operation
        }
    }

    public synchronized int getBalance() {
        return balance;  // Must also be synchronized for visibility
    }
}

Back to top


Synchronized Methods

Synchronized methods lock the entire object (or class for static methods).

Instance Method Synchronization

public class SynchronizedCounter {
    private int count = 0;

    // Locks on 'this' object
    public synchronized void increment() {
        count++;
    }

    public synchronized void decrement() {
        count--;
    }

    public synchronized int getCount() {
        return count;
    }
}

Equivalent to:

public void increment() {
    synchronized (this) {
        count++;
    }
}

Static Method Synchronization

public class StaticCounter {
    private static int count = 0;

    // Locks on StaticCounter.class object
    public static synchronized void increment() {
        count++;
    }

    public static synchronized int getCount() {
        return count;
    }
}

Equivalent to:

public static void increment() {
    synchronized (StaticCounter.class) {
        count++;
    }
}

Back to top


Synchronized Blocks

Synchronized blocks provide more fine-grained control over locking.

Basic Syntax

synchronized (lockObject) {
    // Critical section
    // Only one thread can execute this at a time
}

Example: Partial Synchronization

public class BankAccount {
    private int balance = 1000;
    private final Object balanceLock = new Object();

    public void withdraw(int amount) {
        // Pre-processing (can be concurrent)
        validateAmount(amount);
        logWithdrawal(amount);

        // Only synchronize the critical section
        synchronized (balanceLock) {
            if (balance >= amount) {
                balance -= amount;
            }
        }

        // Post-processing (can be concurrent)
        notifyCustomer();
    }
}

Benefits:

Multiple Locks for Better Concurrency

public class BankAccount {
    private int balance = 1000;
    private List<Transaction> transactions = new ArrayList<>();

    private final Object balanceLock = new Object();
    private final Object transactionsLock = new Object();

    public void withdraw(int amount) {
        synchronized (balanceLock) {
            balance -= amount;
        }
    }

    public void addTransaction(Transaction tx) {
        synchronized (transactionsLock) {
            transactions.add(tx);
        }
    }

    // Different locks allow concurrent balance operations and transaction logging!
}

Back to top


Intrinsic Locks (Monitors)

Every Java object has an intrinsic lock (also called a monitor).

How Intrinsic Locks Work

sequenceDiagram
    participant T1 as Thread 1
    participant Lock as Object Lock
    participant T2 as Thread 2

    T1->>Lock: Try to acquire
    Lock-->>T1: Acquired ✓
    T1->>T1: Execute synchronized block

    T2->>Lock: Try to acquire
    Lock-->>T2: Blocked (waiting...)

    T1->>T1: Finish execution
    T1->>Lock: Release lock

    Lock-->>T2: Acquired ✓
    T2->>T2: Execute synchronized block
    T2->>Lock: Release lock

Reentrancy

Intrinsic locks are reentrant: a thread can acquire the same lock multiple times.

public class ReentrantExample {
    public synchronized void methodA() {
        System.out.println("Method A");
        methodB();  // Can call another synchronized method
    }

    public synchronized void methodB() {
        System.out.println("Method B");  // Same thread can reacquire lock
    }
}

Without reentrancy, this would deadlock! But Java’s intrinsic locks track ownership and allow the same thread to enter.

Lock Ownership

public class LockOwnership {
    public void demonstrateOwnership() {
        synchronized (this) {
            // Thread owns the lock
            synchronized (this) {  // Same thread can reacquire
                synchronized (this) {  // And again...
                    System.out.println("Nested locks work!");
                }
            }
        }  // Lock released once (reference count = 0)
    }
}

Back to top


Inter-Thread Communication

Threads often need to coordinate their actions. Java provides wait(), notify(), and notifyAll() for this purpose.

The Problem: Busy Waiting

// BAD: Wastes CPU cycles
public class DataProcessor {
    private String data = null;

    public void waitForData() {
        while (data == null) {
            // Busy wait - wastes CPU!
        }
        processData(data);
    }
}

The Solution: wait() and notify()

// GOOD: Efficient waiting
public class DataProcessor {
    private String data = null;

    public synchronized void waitForData() throws InterruptedException {
        while (data == null) {
            wait();  // Releases lock and waits
        }
        processData(data);
    }

    public synchronized void setData(String newData) {
        this.data = newData;
        notify();  // Wake up waiting thread
    }
}

Back to top


wait() and notify()

wait() Method

Purpose: Causes the current thread to wait until another thread invokes notify() or notifyAll().

Key Points:

synchronized (lock) {
    while (!condition) {
        lock.wait();  // Release lock and wait
    }
    // Lock reacquired here
    // Condition is true, proceed
}

notify() vs notifyAll()

notify(): Wakes up one waiting thread (arbitrary choice)

synchronized (lock) {
    condition = true;
    lock.notify();  // Wakes one thread
}

notifyAll(): Wakes up all waiting threads

synchronized (lock) {
    condition = true;
    lock.notifyAll();  // Wakes all threads
}

When to use which:

Complete Example

public class WaitNotifyDemo {
    private final Object lock = new Object();
    private boolean dataReady = false;
    private String data;

    // Consumer thread
    public void consume() throws InterruptedException {
        synchronized (lock) {
            while (!dataReady) {  // Always use 'while', not 'if'!
                System.out.println("Waiting for data...");
                lock.wait();
            }
            System.out.println("Got data: " + data);
            dataReady = false;
        }
    }

    // Producer thread
    public void produce(String newData) {
        synchronized (lock) {
            this.data = newData;
            this.dataReady = true;
            System.out.println("Data produced: " + data);
            lock.notifyAll();  // Wake up waiting consumers
        }
    }
}

Why while instead of if?

Back to top


Producer-Consumer Pattern

Classic concurrency pattern using wait/notify:

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 10;
    private final Object lock = new Object();

    // Producer
    public void produce(int value) throws InterruptedException {
        synchronized (lock) {
            // Wait if queue is full
            while (queue.size() == capacity) {
                System.out.println("Queue full, producer waiting...");
                lock.wait();
            }

            queue.add(value);
            System.out.println("Produced: " + value);

            // Notify consumers
            lock.notifyAll();
        }
    }

    // Consumer
    public int consume() throws InterruptedException {
        synchronized (lock) {
            // Wait if queue is empty
            while (queue.isEmpty()) {
                System.out.println("Queue empty, consumer waiting...");
                lock.wait();
            }

            int value = queue.remove();
            System.out.println("Consumed: " + value);

            // Notify producers
            lock.notifyAll();

            return value;
        }
    }
}

// Usage
public class Demo {
    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        // Producer thread
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    pc.produce(i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Consumer thread
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    pc.consume();
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();
    }
}

Modern Alternative: Use BlockingQueue from java.util.concurrent:

BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

// Producer
queue.put(value);  // Blocks if full

// Consumer
int value = queue.take();  // Blocks if empty

Back to top


Best Practices

1. Keep Synchronized Blocks Small

// BAD: Holding lock too long
public synchronized void processLargeFile() {
    readFile();           // I/O - slow!
    processData();        // CPU intensive
    writeResults();       // I/O - slow!
}

// GOOD: Minimize synchronized region
public void processLargeFile() {
    Data data = readFile();  // No lock needed
    Data processed = processData();  // No lock needed

    synchronized (this) {
        // Only synchronize critical section
        updateSharedState(processed);
    }

    writeResults();  // No lock needed
}

2. Avoid Synchronizing on Public Objects

// BAD: External code can lock on this
public class Counter {
    public synchronized void increment() {  // Locks on 'this'
        count++;
    }
}

// External code can cause problems:
Counter counter = new Counter();
synchronized (counter) {  // Blocks increment()!
    // Long operation
}

// GOOD: Use private lock
public class Counter {
    private final Object lock = new Object();
    private int count = 0;

    public void increment() {
        synchronized (lock) {  // Private lock
            count++;
        }
    }
}

3. Always Use while, Not if

// BAD: Spurious wakeups can cause issues
synchronized (lock) {
    if (!condition) {
        lock.wait();
    }
    // Condition might be false here!
}

// GOOD: Recheck condition in loop
synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
    // Condition guaranteed to be true
}

4. Document Thread Safety

/**
 * Thread-safe counter using synchronized methods.
 * All public methods are synchronized.
 */
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

5. Prefer Higher-Level Utilities

// Instead of manual synchronization:
private final Object lock = new Object();
private int count = 0;

public void increment() {
    synchronized (lock) {
        count++;
    }
}

// Use AtomicInteger:
private final AtomicInteger count = new AtomicInteger(0);

public void increment() {
    count.incrementAndGet();  // Lock-free!
}

See also: Atomic Variables

Back to top


Common Pitfalls

❌ 1. Deadlock with Multiple Locks

// BAD: Can deadlock
public class Transfer {
    public void transfer(Account from, Account to, int amount) {
        synchronized (from) {
            synchronized (to) {  // Lock ordering problem!
                from.debit(amount);
                to.credit(amount);
            }
        }
    }
}

// Thread 1: transfer(A, B, 100)  → locks A, waits for B
// Thread 2: transfer(B, A, 50)   → locks B, waits for A
// DEADLOCK!

// GOOD: Consistent lock ordering
public void transfer(Account from, Account to, int amount) {
    Account first = from.getId() < to.getId() ? from : to;
    Account second = from.getId() < to.getId() ? to : from;

    synchronized (first) {
        synchronized (second) {
            from.debit(amount);
            to.credit(amount);
        }
    }
}

❌ 2. Calling notify() Without Holding Lock

// WRONG: IllegalMonitorStateException!
public void produce(String data) {
    this.data = data;
    lock.notify();  // Must be in synchronized block!
}

// CORRECT:
public void produce(String data) {
    synchronized (lock) {
        this.data = data;
        lock.notify();
    }
}

❌ 3. Not Handling InterruptedException

// BAD: Swallowing interruption
try {
    lock.wait();
} catch (InterruptedException e) {
    // Ignoring!
}

// GOOD: Restore interrupt status
try {
    lock.wait();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();  // Restore interrupt flag
    throw new RuntimeException("Interrupted", e);
}

❌ 4. Synchronizing on null or Changing Reference

// BAD: lock can be null or change
private Object lock = new Object();

public void update() {
    synchronized (lock) {  // What if lock is null?
        // ...
    }
}

public void changeLock() {
    lock = new Object();  // Breaks synchronization!
}

// GOOD: final lock object
private final Object lock = new Object();

Back to top


Ref.

Official Documentation:

Books:

Guides:

Related Topics:


Get Started | Java Concurrency | Java 8