Comprehensive comparison between ReentrantLock and synchronized for thread synchronization in Java.
1. Basic Usage Comparison
Synchronized Approach
public class SynchronizedExample { private int counter = 0; private final Object lock = new Object(); // Synchronized method public synchronized void increment() { counter++; } // Synchronized block public void incrementWithBlock() { synchronized (lock) { counter++; } } // Synchronized static method public static synchronized void staticMethod() { // Synchronized on class object } public int getCounter() { return counter; } } ReentrantLock Approach
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample { private int counter = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); // Acquire the lock try { counter++; } finally { lock.unlock(); // Always release in finally block } } public int getCounter() { return counter; } } 2. Key Differences and Features
Feature Comparison Table
public class FeatureComparison { public static void main(String[] args) { System.out.println("=== ReentrantLock vs Synchronized ==="); System.out.println("Feature\t\t\tSynchronized\tReentrantLock"); System.out.println("--------\t\t-----------\t------------"); System.out.println("Reentrant\t\tYes\t\tYes"); System.out.println("Fair locking\t\tNo\t\tOptional"); System.out.println("Try lock\t\tNo\t\tYes"); System.out.println("Lock interruption\tNo\t\tYes"); System.out.println("Timeout support\t\tNo\t\tYes"); System.out.println("Condition support\tLimited\t\tYes"); System.out.println("Performance\t\tGood\t\tBetter in contention"); } } 3. Advanced ReentrantLock Features
Try Lock with Timeout
public class TryLockExample { private final ReentrantLock lock = new ReentrantLock(); private final List<String> data = new ArrayList<>(); public boolean addDataWithTimeout(String item, long timeout, TimeUnit unit) { try { // Try to acquire lock with timeout if (lock.tryLock(timeout, unit)) { try { // Critical section Thread.sleep(100); // Simulate work data.add(item); return true; } finally { lock.unlock(); } } else { System.out.println("Could not acquire lock within timeout"); return false; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } public boolean tryAddData(String item) { // Non-blocking try if (lock.tryLock()) { try { data.add(item); return true; } finally { lock.unlock(); } } else { System.out.println("Lock is held by another thread"); return false; } } } Interruptible Locking
public class InterruptibleLockExample { private final ReentrantLock lock = new ReentrantLock(); public void performTask() throws InterruptedException { System.out.println(Thread.currentThread().getName() + " attempting to acquire lock"); lock.lockInterruptibly(); // This can be interrupted try { System.out.println(Thread.currentThread().getName() + " acquired lock"); // Simulate long-running task for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " working... " + i); Thread.sleep(1000); // Check if interrupted during work if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + " was interrupted during work"); break; } } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); System.out.println(Thread.currentThread().getName() + " released lock"); } } } public static void demonstrateInterruption() throws Exception { InterruptibleLockExample example = new InterruptibleLockExample(); Thread worker1 = new Thread(() -> { try { example.performTask(); } catch (InterruptedException e) { System.out.println("Worker 1 was interrupted while waiting for lock"); } }, "Worker-1"); Thread worker2 = new Thread(() -> { try { example.performTask(); } catch (InterruptedException e) { System.out.println("Worker 2 was interrupted while waiting for lock"); } }, "Worker-2"); worker1.start(); Thread.sleep(100); // Ensure worker1 gets lock first worker2.start(); Thread.sleep(100); // Let worker2 start waiting for lock // Interrupt worker2 while it's waiting for the lock worker2.interrupt(); worker1.join(); worker2.join(); } } Fair Locking
public class FairLockExample { private final ReentrantLock fairLock = new ReentrantLock(true); // Fair lock private final ReentrantLock unfairLock = new ReentrantLock(); // Unfair lock (default) public void demonstrateFairness() throws InterruptedException { System.out.println("=== Fair Lock Demonstration ==="); // Test fair lock testLock("Fair Lock", fairLock); Thread.sleep(2000); System.out.println("\n=== Unfair Lock Demonstration ==="); // Test unfair lock testLock("Unfair Lock", unfairLock); } private void testLock(String lockType, ReentrantLock lock) throws InterruptedException { int threadCount = 5; CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch endLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadId = i; new Thread(() -> { try { startLatch.await(); // Wait for start signal lock.lock(); try { System.out.println(lockType + " acquired by Thread-" + threadId + " at " + System.currentTimeMillis()); Thread.sleep(100); // Hold lock briefly } finally { lock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { endLatch.countDown(); } }, "Thread-" + threadId).start(); } // Start all threads at once startLatch.countDown(); endLatch.await(); // Wait for all threads to complete } } 4. Condition Variables with ReentrantLock
Producer-Consumer with Conditions
public class ProducerConsumerWithConditions { private final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); // Condition for consumers private final Condition notFull = lock.newCondition(); // Condition for producers private final Queue<Integer> queue = new LinkedList<>(); private final int capacity; public ProducerConsumerWithConditions(int capacity) { this.capacity = capacity; } public void produce(int value) throws InterruptedException { lock.lock(); try { // Wait until queue is not full while (queue.size() == capacity) { System.out.println("Queue full, producer waiting..."); notFull.await(); // Releases lock and waits } queue.offer(value); System.out.println("Produced: " + value + ", queue size: " + queue.size()); // Signal consumers that queue is not empty notEmpty.signalAll(); } finally { lock.unlock(); } } public int consume() throws InterruptedException { lock.lock(); try { // Wait until queue is not empty while (queue.isEmpty()) { System.out.println("Queue empty, consumer waiting..."); notEmpty.await(); // Releases lock and waits } int value = queue.poll(); System.out.println("Consumed: " + value + ", queue size: " + queue.size()); // Signal producers that queue is not full notFull.signalAll(); return value; } finally { lock.unlock(); } } // Demonstration public static void main(String[] args) throws Exception { ProducerConsumerWithConditions pc = new ProducerConsumerWithConditions(3); // Producer thread Thread producer = new Thread(() -> { try { for (int i = 1; i <= 10; i++) { pc.produce(i); Thread.sleep(200); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // Consumer thread Thread consumer = new Thread(() -> { try { for (int i = 1; i <= 10; i++) { pc.consume(); Thread.sleep(300); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); producer.start(); consumer.start(); producer.join(); consumer.join(); } } Multiple Conditions Example
public class MultipleConditionsExample { private final ReentrantLock lock = new ReentrantLock(); private final Condition dataAvailable = lock.newCondition(); private final Condition processingDone = lock.newCondition(); private boolean dataReady = false; private boolean processingComplete = false; private String data; public void produceData(String data) throws InterruptedException { lock.lock(); try { // Wait if previous data hasn't been processed while (dataReady) { processingDone.await(); } this.data = data; dataReady = true; System.out.println("Produced data: " + data); // Signal that data is available dataAvailable.signalAll(); } finally { lock.unlock(); } } public String consumeData() throws InterruptedException { lock.lock(); try { // Wait for data to be available while (!dataReady) { dataAvailable.await(); } String consumedData = this.data; System.out.println("Consumed data: " + consumedData); return consumedData; } finally { lock.unlock(); } } public void processData() throws InterruptedException { lock.lock(); try { String data = consumeData(); // Simulate processing Thread.sleep(1000); System.out.println("Processed data: " + data.toUpperCase()); // Reset state dataReady = false; this.data = null; processingComplete = true; // Signal that processing is done processingDone.signalAll(); } finally { lock.unlock(); } } } 5. Performance Comparison
Benchmark Example
public class LockPerformanceBenchmark { private static final int ITERATIONS = 10_000; private static final int THREAD_COUNT = 10; // Synchronized implementation static class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } // ReentrantLock implementation static class ReentrantLockCounter { private int count = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } } public static void benchmark() throws InterruptedException { System.out.println("=== Performance Benchmark ==="); System.out.println("Iterations: " + ITERATIONS); System.out.println("Threads: " + THREAD_COUNT); // Test synchronized long syncTime = testSynchronized(); System.out.printf("Synchronized time: %d ms%n", syncTime); // Test ReentrantLock long lockTime = testReentrantLock(); System.out.printf("ReentrantLock time: %d ms%n", lockTime); System.out.printf("ReentrantLock is %.2fx %s%n", (double) syncTime / lockTime, lockTime < syncTime ? "faster" : "slower"); } private static long testSynchronized() throws InterruptedException { SynchronizedCounter counter = new SynchronizedCounter(); CountDownLatch latch = new CountDownLatch(THREAD_COUNT); long startTime = System.currentTimeMillis(); for (int i = 0; i < THREAD_COUNT; i++) { new Thread(() -> { for (int j = 0; j < ITERATIONS; j++) { counter.increment(); } latch.countDown(); }).start(); } latch.await(); long endTime = System.currentTimeMillis(); System.out.println("Synchronized final count: " + counter.getCount()); return endTime - startTime; } private static long testReentrantLock() throws InterruptedException { ReentrantLockCounter counter = new ReentrantLockCounter(); CountDownLatch latch = new CountDownLatch(THREAD_COUNT); long startTime = System.currentTimeMillis(); for (int i = 0; i < THREAD_COUNT; i++) { new Thread(() -> { for (int j = 0; j < ITERATIONS; j++) { counter.increment(); } latch.countDown(); }).start(); } latch.await(); long endTime = System.currentTimeMillis(); System.out.println("ReentrantLock final count: " + counter.getCount()); return endTime - startTime; } } 6. Real-World Use Cases
Resource Pool with ReentrantLock
public class ResourcePool<T> { private final ReentrantLock lock = new ReentrantLock(true); // Fair lock private final Condition resourceAvailable = lock.newCondition(); private final Queue<T> availableResources; private final Set<T> allocatedResources; private final int maxSize; public ResourcePool(int maxSize) { this.maxSize = maxSize; this.availableResources = new LinkedList<>(); this.allocatedResources = new HashSet<>(); } public T acquire() throws InterruptedException { lock.lock(); try { // Wait until resource is available or pool can be expanded while (availableResources.isEmpty() && allocatedResources.size() == maxSize) { resourceAvailable.await(); } T resource; if (availableResources.isEmpty()) { // Create new resource (simulated) resource = createResource(); } else { resource = availableResources.poll(); } allocatedResources.add(resource); return resource; } finally { lock.unlock(); } } public T acquire(long timeout, TimeUnit unit) throws InterruptedException { lock.lock(); try { long nanos = unit.toNanos(timeout); while (availableResources.isEmpty() && allocatedResources.size() == maxSize) { if (nanos <= 0) { return null; // Timeout } nanos = resourceAvailable.awaitNanos(nanos); } T resource; if (availableResources.isEmpty()) { resource = createResource(); } else { resource = availableResources.poll(); } allocatedResources.add(resource); return resource; } finally { lock.unlock(); } } public void release(T resource) { lock.lock(); try { if (allocatedResources.remove(resource)) { availableResources.offer(resource); resourceAvailable.signalAll(); // Notify waiting threads } } finally { lock.unlock(); } } @SuppressWarnings("unchecked") private T createResource() { // Simulate resource creation return (T) new Object(); } public int getAvailableCount() { lock.lock(); try { return availableResources.size(); } finally { lock.unlock(); } } public int getAllocatedCount() { lock.lock(); try { return allocatedResources.size(); } finally { lock.unlock(); } } } Cache with Read-Write Locking
public class ReadWriteLockCache<K, V> { private final ReentrantLock mainLock = new ReentrantLock(); private final Map<K, V> cache = new HashMap<>(); private int readCount = 0; private final Condition canRead = mainLock.newCondition(); private final Condition canWrite = mainLock.newCondition(); private boolean isWriting = false; // Simpler alternative: use ReadWriteLock private final java.util.concurrent.locks.ReadWriteLock rwLock = new java.util.concurrent.locks.ReentrantReadWriteLock(); private final Map<K, V> simpleCache = new HashMap<>(); // Using manual read-write lock implementation public V get(K key) throws InterruptedException { mainLock.lock(); try { // Wait if someone is writing while (isWriting) { canRead.await(); } readCount++; } finally { mainLock.unlock(); } try { return cache.get(key); } finally { mainLock.lock(); try { readCount--; if (readCount == 0) { canWrite.signal(); // Signal writers if no readers } } finally { mainLock.unlock(); } } } public void put(K key, V value) throws InterruptedException { mainLock.lock(); try { // Wait if someone is writing or reading while (isWriting || readCount > 0) { canWrite.await(); } isWriting = true; } finally { mainLock.unlock(); } try { // Perform write operation cache.put(key, value); Thread.sleep(10); // Simulate write time } finally { mainLock.lock(); try { isWriting = false; canRead.signalAll(); // Signal all readers canWrite.signal(); // Signal one writer } finally { mainLock.unlock(); } } } // Simpler approach using built-in ReadWriteLock public V getSimple(K key) { rwLock.readLock().lock(); try { return simpleCache.get(key); } finally { rwLock.readLock().unlock(); } } public void putSimple(K key, V value) { rwLock.writeLock().lock(); try { simpleCache.put(key, value); } finally { rwLock.writeLock().unlock(); } } } 7. Best Practices and Pitfalls
Common Mistakes and Solutions
public class LockBestPractices { // ✅ GOOD: Always unlock in finally block public void goodPractice(ReentrantLock lock) { lock.lock(); try { // Critical section performOperation(); } finally { lock.unlock(); } } // ❌ BAD: Might not unlock if exception occurs public void badPractice(ReentrantLock lock) { lock.lock(); performOperation(); lock.unlock(); // If exception occurs before this, lock won't be released } // ✅ GOOD: Check if current thread holds lock before unlocking public void safeUnlock(ReentrantLock lock) { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } // ✅ GOOD: Using tryLock with resource management public void withTryLock(ReentrantLock lock) { if (lock.tryLock()) { try { // Critical section performOperation(); } finally { lock.unlock(); } } else { // Alternative approach when lock is not available performAlternativeOperation(); } } // ❌ BAD: Nested locking can cause deadlocks public void potentialDeadlock(ReentrantLock lock1, ReentrantLock lock2) { lock1.lock(); try { // Some operation lock2.lock(); // Dangerous if another thread does the reverse try { // Nested critical section } finally { lock2.unlock(); } } finally { lock1.unlock(); } } // ✅ GOOD: Consistent lock ordering to prevent deadlocks public void safeNestedLocking(ReentrantLock lock1, ReentrantLock lock2) { // Always acquire locks in the same order ReentrantLock firstLock, secondLock; if (lock1.hashCode() < lock2.hashCode()) { firstLock = lock1; secondLock = lock2; } else { firstLock = lock2; secondLock = lock1; } firstLock.lock(); try { secondLock.lock(); try { // Nested critical section } finally { secondLock.unlock(); } } finally { firstLock.unlock(); } } private void performOperation() { // Some operation } private void performAlternativeOperation() { // Alternative operation } } When to Use Which
public class LockSelectionGuide { /* * Use Synchronized when: * - Simple synchronization needs * - Basic critical sections * - When you want JVM to handle lock management * - When you don't need advanced features */ public synchronized void useSynchronized() { // Simple critical section } /* * Use ReentrantLock when: * - You need tryLock() functionality * - You need fair locking * - You need lock interruption * - You need multiple condition variables * - You need to check lock status */ public void useReentrantLock() { ReentrantLock lock = new ReentrantLock(); if (lock.tryLock()) { try { // Critical section with advanced features } finally { lock.unlock(); } } } /* * Use ReadWriteLock when: * - Multiple readers, infrequent writers * - Read-heavy workloads * - You want to allow concurrent reads */ public void useReadWriteLock() { java.util.concurrent.locks.ReadWriteLock rwLock = new java.util.concurrent.locks.ReentrantReadWriteLock(); // For reads rwLock.readLock().lock(); try { // Multiple threads can read concurrently } finally { rwLock.readLock().unlock(); } // For writes rwLock.writeLock().lock(); try { // Only one thread can write } finally { rwLock.writeLock().unlock(); } } } Summary
| Aspect | Synchronized | ReentrantLock |
|---|---|---|
| Syntax | Simple, built-in | More verbose, explicit |
| Flexibility | Limited | High (tryLock, fair locking, etc.) |
| Performance | Good for low contention | Better for high contention |
| Condition Support | Limited (wait/notify) | Multiple Condition objects |
| Lock Acquisition | Blocking only | Non-blocking, timed, interruptible |
| Debugging | Harder to debug | Better monitoring capabilities |
Choose synchronized for simple cases and ReentrantLock when you need advanced features like try-lock, fair ordering, or multiple condition variables.