Synchronization

Java Synchronization: In-Depth Guide

1. What is Synchronization?

Definition

  • Mechanism to control multiple threads accessing shared resources

  • Prevents thread interference and memory consistency errors

  • Ensures thread safety and data consistency

Why Synchronization?

  1. Race Conditions: When multiple threads access shared data

  2. Data Inconsistency: Without sync, data can become corrupted

  3. Memory Visibility: Changes made by one thread may not be visible to others

2. Types of Synchronization

A. Method Synchronization

class Counter {
    private int count = 0;
    
    synchronized void increment() {
        count++;
    }
}

B. Block Synchronization

class Counter {
    private int count = 0;
    
    void increment() {
        synchronized(this) {
            count++;
        }
    }
}

C. Static Synchronization

class Counter {
    private static int count = 0;
    
    synchronized static void increment() {
        count++;
    }
}

3. Key Synchronization Concepts

A. Monitor/Lock

  • Every object in Java has a monitor

  • Only one thread can own monitor at a time

  • synchronized keyword uses this intrinsic lock

B. Mutual Exclusion

class BankAccount {
    private double balance;
    
    synchronized void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }
}

C. Memory Visibility

class SharedData {
    private volatile boolean flag = false;
    private int data = 0;
    
    void updateData() {
        data++;
        flag = true;  // Visible to other threads
    }
}

4. Advanced Synchronization Techniques

A. ReentrantLock

class BankAccount {
    private final Lock lock = new ReentrantLock();
    private double balance;
    
    void withdraw(double amount) {
        lock.lock();
        try {
            if (balance >= amount) {
                balance -= amount;
            }
        } finally {
            lock.unlock();
        }
    }
}

B. ReadWriteLock

class CacheData {
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private Map<String, String> cache = new HashMap<>();
    
    String read(String key) {
        rwLock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    void write(String key, String value) {
        rwLock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

5. Common Interview Questions & Answers

Q1: What's the difference between synchronized method and synchronized block?

Answer:

  • Synchronized method locks entire method

  • Synchronized block locks specific section

  • Block synchronization is more efficient

  • Block allows finer control over lock duration

Q2: What is the 'volatile' keyword?

Answer:

  • Ensures variable is read from main memory

  • Prevents thread caching of variable

  • Provides visibility guarantee

  • Doesn't provide atomicity

  • Use cases: flags, status indicators

Q3: Explain object-level vs class-level locking

Answer:

  • Object-level: synchronized non-static methods/blocks

  • Class-level: synchronized static methods/blocks

  • Different locks, can run concurrently

  • Class lock is on Class object itself

Q4: What is thread starvation?

Answer:

  • Thread unable to gain regular access to shared resources

  • Causes: Priority issues, long-holding locks

  • Solution: Fair locks, proper timeout mechanisms

Q5: How to prevent deadlock?

Answer:

  1. Lock ordering

  2. Lock timeouts

  3. Try-lock mechanism

  4. Avoid nested locks

  5. Use Lock interface instead of synchronized

6. Common Synchronization Problems

A. Deadlock

// Deadlock Example
class DeadlockRisk {
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    
    void method1() {
        synchronized(lock1) {
            synchronized(lock2) {
                // code
            }
        }
    }
    
    void method2() {
        synchronized(lock2) {
            synchronized(lock1) {
                // code
            }
        }
    }
}

B. Producer-Consumer Problem

class Buffer {
    private Queue<Integer> queue = new LinkedList<>();
    private int size;
    
    synchronized void produce(int item) throws InterruptedException {
        while(queue.size() == size) {
            wait();
        }
        queue.add(item);
        notify();
    }
    
    synchronized int consume() throws InterruptedException {
        while(queue.isEmpty()) {
            wait();
        }
        int item = queue.remove();
        notify();
        return item;
    }
}

7. Best Practices

Synchronization Guidelines

  1. Keep synchronized blocks small

  2. Don't synchronize immutable data

  3. Avoid synchronizing on String literals

  4. Use private final lock objects

  5. Consider using concurrent collections

  6. Prefer ReentrantLock for complex scenarios

  7. Document thread safety

Performance Considerations

  1. Minimize lock contention

  2. Use lock stripping when possible

  3. Consider using atomic classes

  4. Balance synchronization granularity

8. Modern Alternatives

A. Atomic Classes

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // Thread-safe increment

B. Concurrent Collections

  • ConcurrentHashMap

  • CopyOnWriteArrayList

  • BlockingQueue

  • ConcurrentLinkedQueue

C. CompletableFuture (Java 8+)

CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "Result")
    .thenApply(s -> s + " processed");

Last updated