Thread Synchronization in Java

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

  1. Minimize synchronization scope - synchronize only critical sections
  2. Use concurrent collections when possible instead of manual synchronization
  3. Prefer immutability - immutable objects are inherently thread-safe
  4. Use atomic variables for simple atomic operations
  5. Avoid nested locks to prevent deadlocks
  6. Always unlock in finally blocks
  7. 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

  1. Synchronizing on different objects for related operations
  2. Forgotten synchronization in some methods
  3. Synchronizing on mutable fields
  4. Excessive synchronization causing performance issues
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper