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.