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.