Thread synchronization is crucial in multi-threaded programming to prevent thread interference and memory consistency errors when multiple threads access shared resources.
The Problem: Race Conditions
public class Counter { private int count = 0; public void increment() { count++; // Not thread-safe! } public int getCount() { return count; } } public class RaceConditionDemo { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); t1.start(); t2.start(); t1.join(); t2.join(); // Expected: 2000, but often gets less due to race condition System.out.println("Final count: " + counter.getCount()); } } Synchronization Solutions
1. Synchronized Methods
public class SynchronizedCounter { private int count = 0; // Synchronized method - locks on 'this' public synchronized void increment() { count++; } public synchronized void decrement() { count--; } public synchronized int getCount() { return count; } } 2. Synchronized Blocks
public class SynchronizedBlockCounter { private int count = 0; private final Object lock = new Object(); // Explicit lock object public void increment() { synchronized (lock) { count++; } } public int getCount() { synchronized (lock) { return count; } } // Synchronized block on different objects public void transfer(SynchronizedBlockCounter from, int amount) { // Avoid deadlock by ordering locks SynchronizedBlockCounter first, second; if (System.identityHashCode(from) < System.identityHashCode(this)) { first = from; second = this; } else { first = this; second = from; } synchronized (first) { synchronized (second) { // Transfer logic } } } } 3. Static Synchronization
public class StaticSynchronizedExample { private static int staticCount = 0; private int instanceCount = 0; // Locks on Class object (StaticSynchronizedExample.class) public static synchronized void incrementStatic() { staticCount++; } // Synchronized block on class object public static void decrementStatic() { synchronized (StaticSynchronizedExample.class) { staticCount--; } } // Instance method - locks on 'this' public synchronized void incrementInstance() { instanceCount++; } } Advanced Synchronization Mechanisms
1. ReentrantLock
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.Condition; public class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; private boolean available = false; private final Condition condition = lock.newCondition(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); // Always unlock in finally block } } // Producer-Consumer with Conditions public void produce(int value) throws InterruptedException { lock.lock(); try { while (available) { condition.await(); // Wait until item is consumed } count = value; available = true; System.out.println("Produced: " + value); condition.signalAll(); // Notify consumers } finally { lock.unlock(); } } public void consume() throws InterruptedException { lock.lock(); try { while (!available) { condition.await(); // Wait until item is produced } System.out.println("Consumed: " + count); available = false; condition.signalAll(); // Notify producers } finally { lock.unlock(); } } public boolean tryIncrement() { if (lock.tryLock()) { try { count++; return true; } finally { lock.unlock(); } } return false; } } 2. ReadWriteLock
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockExample { private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private String data = ""; public void writeData(String newData) { rwLock.writeLock().lock(); try { data = newData; System.out.println("Data written: " + newData); Thread.sleep(1000); // Simulate write time } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { rwLock.writeLock().unlock(); } } public String readData() { rwLock.readLock().lock(); try { System.out.println("Data read: " + data); Thread.sleep(500); // Simulate read time return data; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return ""; } finally { rwLock.readLock().unlock(); } } } Thread-Safe Collections
1. Concurrent Collections
import java.util.concurrent.*; import java.util.*; public class ConcurrentCollectionsExample { public static void main(String[] args) { // Thread-safe collections ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(); CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>(); ConcurrentLinkedQueue<String> concurrentQueue = new ConcurrentLinkedQueue<>(); // Using concurrent map concurrentMap.put("key1", 1); concurrentMap.putIfAbsent("key1", 2); // Won't replace existing // Atomic operations concurrentMap.compute("key1", (k, v) -> v == null ? 1 : v + 1); concurrentMap.merge("key2", 1, Integer::sum); } } 2. Synchronized Collections Wrappers
public class SynchronizedCollectionsExample { public static void main(String[] args) { // Creating synchronized collections List<String> syncList = Collections.synchronizedList(new ArrayList<>()); Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>()); Set<String> syncSet = Collections.synchronizedSet(new HashSet<>()); // When iterating, must synchronize manually synchronized (syncList) { Iterator<String> it = syncList.iterator(); while (it.hasNext()) { System.out.println(it.next()); } } } } Atomic Variables
import java.util.concurrent.atomic.*; public class AtomicExample { private final AtomicInteger atomicCount = new AtomicInteger(0); private final AtomicReference<String> atomicReference = new AtomicReference<>("initial"); private final AtomicLong atomicLong = new AtomicLong(0L); public void increment() { atomicCount.incrementAndGet(); // Atomic operation } public void updateReference() { // Atomic compare-and-set String current, next; do { current = atomicReference.get(); next = current + "-updated"; } while (!atomicReference.compareAndSet(current, next)); } public int accumulate(int value) { return atomicCount.accumulateAndGet(value, Math::max); } } Volatile Keyword
public class VolatileExample { private volatile boolean running = true; private volatile int counter = 0; public void stop() { running = false; // Immediately visible to other threads } public void increment() { counter++; // Not atomic! Use for visibility only } public void example() { Thread worker = new Thread(() -> { while (running) { // Do work System.out.println("Working..."); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } System.out.println("Stopped"); }); worker.start(); // Stop after 5 seconds try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } stop(); // Stops the worker thread } } Deadlock Prevention
public class DeadlockPrevention { private final Object lock1 = new Object(); private final Object lock2 = new Object(); private int resource1 = 0; private int resource2 = 0; // POTENTIAL DEADLOCK public void method1() { synchronized (lock1) { synchronized (lock2) { resource1++; resource2--; } } } public void method2() { synchronized (lock2) { synchronized (lock1) { // Different order - DEADLOCK! resource1--; resource2++; } } } // DEADLOCK-FREE VERSION public void method1Safe() { synchronized (lock1) { synchronized (lock2) { resource1++; resource2--; } } } public void method2Safe() { synchronized (lock1) { // Same order as method1Safe synchronized (lock2) { resource1--; resource2++; } } } // Using tryLock to avoid deadlocks public boolean tryTransfer() { ReentrantLock lock1 = new ReentrantLock(); ReentrantLock lock2 = new ReentrantLock(); while (true) { if (lock1.tryLock()) { try { if (lock2.tryLock()) { try { // Do work return true; } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } // Avoid livelock try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } } } Producer-Consumer Pattern
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class ProducerConsumerExample { private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); public class Producer implements Runnable { public void run() { int value = 0; while (true) { try { queue.put(value); System.out.println("Produced: " + value); value++; Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } } public class Consumer implements Runnable { public void run() { while (true) { try { Integer value = queue.take(); System.out.println("Consumed: " + value); Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } } public void start() { Thread producer = new Thread(new Producer()); Thread consumer = new Thread(new Consumer()); producer.start(); consumer.start(); } } Best Practices
- Minimize synchronization scope - synchronize only critical sections
- Use concurrent collections when possible instead of manual synchronization
- Prefer immutability - immutable objects are inherently thread-safe
- Use atomic variables for simple atomic operations
- Avoid nested locks to prevent deadlocks
- Always unlock in finally blocks
- Document thread safety in your classes
// Thread safety documentation examples @ThreadSafe public class ThreadSafeClass { // Fully synchronized - safe for concurrent access } @Immutable public final class ImmutableClass { // No synchronization needed - immutable } @NotThreadSafe public class NotThreadSafeClass { // Client must synchronize externally } Common Pitfalls
- Synchronizing on different objects for related operations
- Forgotten synchronization in some methods
- Synchronizing on mutable fields
- Excessive synchronization causing performance issues
- Ignoring iteration synchronization
Thread synchronization is essential for writing correct multi-threaded applications in Java. Choose the right synchronization mechanism based on your specific needs and always test thoroughly under concurrent conditions.