Java Concurrency: Reentrant Locks and ReadWriteLocks
🔰 Introduction to ReentrantLock and ReadWriteLock in Java
Welcome to this comprehensive guide on advanced Java concurrency tools! After exploring thread synchronization with synchronized blocks and methods, and understanding the volatile keyword and atomicity, it's time to dive into more powerful and flexible concurrency control mechanisms: ReentrantLock
and ReadWriteLock
.
These explicit locking mechanisms, introduced in Java 5 as part of the java.util.concurrent.locks
package, provide significant advantages over intrinsic locks (synchronized blocks/methods). They offer finer-grained control over lock acquisition and release, support for lock polling and timeouts, interruptible lock acquisition, and specialized lock types for read-heavy scenarios.
Whether you're building high-performance applications, complex concurrent systems, or simply want more control over your threading behavior, understanding these advanced locking mechanisms will significantly enhance your ability to write robust, efficient concurrent code in Java.
🧠 Understanding Java ReentrantLock and ReadWriteLock
🔒 ReentrantLock: Beyond Synchronized
A ReentrantLock
is an explicit lock implementation that provides the same basic behavior and memory semantics as the implicit locks used by synchronized code, but with extended capabilities. The term "reentrant" means that a thread that holds the lock can acquire it again without blocking itself.
🔄 Reentrant Nature
Both intrinsic locks (synchronized) and ReentrantLock
are reentrant, meaning:
- A thread can acquire the same lock multiple times
- The lock keeps track of how many times it's been acquired
- The lock is only released when the thread calls
unlock()
the same number of times it calledlock()
📝 Analogy: Think of a reentrant lock like a meeting room with a sign-in sheet. If you're already in the room, you can "re-enter" without waiting outside. You just add another entry to the sign-in sheet. Only when you've signed out as many times as you signed in is the room available for others.
lock.lock(); // First acquisition
try {
// Do something
lock.lock(); // Second acquisition (reentrant)
try {
// Do something else
} finally {
lock.unlock(); // Release second acquisition
}
} finally {
lock.unlock(); // Release first acquisition
}
🛠️ Explicit Lock Control
Unlike synchronized blocks, which automatically acquire and release locks, ReentrantLock
gives you explicit control:
- Manual Lock/Unlock: You explicitly call
lock()
andunlock()
- Try-Finally Pattern: Always use a try-finally block to ensure locks are released
- Multiple Unlock Points: You can release a lock in different places in your code
ReentrantLock lock = new ReentrantLock();
// Acquiring the lock
lock.lock();
try {
// Critical section - protected by the lock
// ...
} finally {
// Always release the lock in a finally block
lock.unlock();
}
🔄 Lock Methods
ReentrantLock
provides several methods for acquiring and releasing locks:
lock()
: Acquires the lock, blocking indefinitely until the lock is availableunlock()
: Releases the locktryLock()
: Attempts to acquire the lock without blocking- Returns immediately with a boolean indicating success or failure
- Can include a timeout parameter
lockInterruptibly()
: Acquires the lock, but allows the thread to be interrupted while waiting
Let's explore each of these methods in more detail:
lock()
The most basic method, similar to entering a synchronized block:
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}
This method will block indefinitely until the lock is acquired. If the thread is interrupted while waiting, it will still continue to wait for the lock.
tryLock()
Attempts to acquire the lock without blocking indefinitely:
if (lock.tryLock()) {
try {
// Critical section (only executed if lock was acquired)
} finally {
lock.unlock();
}
} else {
// Alternative action if lock wasn't acquired
}
You can also specify a timeout:
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// Critical section (only executed if lock was acquired within 1 second)
} finally {
lock.unlock();
}
} else {
// Alternative action if lock wasn't acquired within timeout
}
} catch (InterruptedException e) {
// Handle interruption
Thread.currentThread().interrupt();
}
lockInterruptibly()
Acquires the lock, but allows the thread to be interrupted while waiting:
try {
lock.lockInterruptibly();
try {
// Critical section
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// Handle interruption
Thread.currentThread().interrupt();
}
This is useful for tasks that should be cancellable, like responding to user requests to stop an operation.
🔄 Fair vs. Unfair Locking
ReentrantLock
supports two modes of lock acquisition:
- Unfair (default): Threads compete for the lock without regard to how long they've been waiting
- Fair: Locks are granted to threads in the order they requested them
// Default constructor creates an unfair lock
ReentrantLock unfairLock = new ReentrantLock();
// Specify true for a fair lock
ReentrantLock fairLock = new ReentrantLock(true);
Fair locks can reduce throughput but prevent starvation of threads.
📚 Java ReadWriteLock: Optimizing for Read-Heavy Access
ReadWriteLock
is an interface that defines a pair of locks: one for read-only operations and one for write operations. The most common implementation is ReentrantReadWriteLock
.
🔄 The Read-Write Lock Concept
The key insight behind read-write locks is that multiple threads can safely read shared data simultaneously, but writes require exclusive access:
- Read Lock: Multiple threads can hold the read lock simultaneously
- Write Lock: Only one thread can hold the write lock, and no read locks can be held simultaneously
📝 Analogy: Think of a read-write lock like a collaborative document. Many people can view (read) the document at the same time without issue. But when someone wants to edit (write to) the document, they need exclusive access to prevent confusion and conflicts.
🛠️ Using ReadWriteLock
ReadWriteLock
is an interface with two methods:
readLock()
: Returns aLock
that can be acquired by multiple threads simultaneouslywriteLock()
: Returns aLock
that provides exclusive access
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// Reading (shared access)
readLock.lock();
try {
// Read from shared resource
} finally {
readLock.unlock();
}
// Writing (exclusive access)
writeLock.lock();
try {
// Modify shared resource
} finally {
writeLock.unlock();
}
🔄 Downgrading and Upgrading
ReentrantReadWriteLock
supports lock downgrading (converting a write lock to a read lock) but not lock upgrading:
// Lock downgrading (write -> read)
writeLock.lock();
try {
// Update shared data
// Acquire read lock before releasing write lock
readLock.lock();
} finally {
writeLock.unlock(); // Release write lock but still hold read lock
}
try {
// Continue with read-only operations
} finally {
readLock.unlock();
}
Attempting to upgrade from a read lock to a write lock can lead to deadlock, as the thread would be waiting for all read locks (including its own) to be released.
🔄 Fair vs. Unfair Read-Write Locks
Like ReentrantLock
, ReentrantReadWriteLock
supports both fair and unfair locking modes:
// Default constructor creates an unfair read-write lock
ReadWriteLock unfairRWLock = new ReentrantReadWriteLock();
// Specify true for a fair read-write lock
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);
💻 Example with Code Comments
Let's look at a complete example that demonstrates ReentrantLock
and ReadWriteLock
:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class AdvancedLockingDemo {
// A simple cache implementation using ReentrantReadWriteLock
static class Cache {
private final Map<String, String> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// Multiple threads can read simultaneously
public String get(String key) {
readLock.lock(); // Acquire read lock
try {
System.out.println(Thread.currentThread().getName() + " reading");
// Simulate some work
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return cache.get(key);
} finally {
readLock.unlock(); // Always release the lock
}
}
// Only one thread can write at a time, and no readers allowed during write
public void put(String key, String value) {
writeLock.lock(); // Acquire write lock
try {
System.out.println(Thread.currentThread().getName() + " writing");
// Simulate some work
try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
cache.put(key, value);
} finally {
writeLock.unlock(); // Always release the lock
}
}
}
// A resource that uses ReentrantLock with timeout and interruptible acquisition
static class Resource {
private final ReentrantLock lock = new ReentrantLock();
// Method demonstrating tryLock with timeout
public boolean useWithTimeout() {
try {
// Try to acquire the lock, but only wait for 1 second
if (lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " acquired lock with timeout");
// Simulate some work
Thread.sleep(500);
return true;
} finally {
lock.unlock(); // Always release the lock
}
} else {
System.out.println(Thread.currentThread().getName() + " couldn't acquire lock within timeout");
return false;
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " was interrupted while waiting");
Thread.currentThread().interrupt(); // Restore the interrupt status
return false;
}
}
// Method demonstrating lockInterruptibly
public void useInterruptibly() throws InterruptedException {
// This will throw InterruptedException if the thread is interrupted while waiting
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " acquired lock interruptibly");
// Simulate long-running operation
Thread.sleep(2000);
} finally {
lock.unlock(); // Always release the lock
}
}
}
public static void main(String[] args) {
// Demonstrate ReadWriteLock with Cache
final Cache cache = new Cache();
// Initialize the cache
cache.put("key1", "value1");
// Create multiple reader threads
for (int i = 0; i < 5; i++) {
new Thread(() -> {
// Multiple readers can access simultaneously
String value = cache.get("key1");
System.out.println(Thread.currentThread().getName() + " read: " + value);
}, "Reader-" + i).start();
}
// Create a writer thread
new Thread(() -> {
// Writer gets exclusive access
cache.put("key1", "updated-value");
System.out.println(Thread.currentThread().getName() + " updated value");
}, "Writer").start();
// Demonstrate ReentrantLock features with Resource
final Resource resource = new Resource();
// Thread using tryLock with timeout
new Thread(() -> {
boolean acquired = resource.useWithTimeout();
System.out.println(Thread.currentThread().getName() + " completed with result: " + acquired);
}, "TimeoutThread").start();
// Thread using lockInterruptibly
Thread interruptibleThread = new Thread(() -> {
try {
resource.useInterruptibly();
System.out.println(Thread.currentThread().getName() + " completed normally");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " was interrupted");
}
}, "InterruptibleThread");
interruptibleThread.start();
// Interrupt the thread after a delay
try {
Thread.sleep(500);
interruptibleThread.interrupt();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
This example demonstrates:
- A cache implementation using
ReadWriteLock
to allow concurrent reads - A resource using
ReentrantLock
with timeout and interruptible acquisition - Multiple reader threads accessing the cache simultaneously
- A writer thread getting exclusive access to the cache
- Threads using different lock acquisition strategies
📦 More Code Snippets
1. Implementing a Thread-Safe Bounded Buffer with ReentrantLock
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer<E> {
private final Queue<E> queue = new LinkedList<>();
private final int capacity;
// Lock for the buffer
private final ReentrantLock lock = new ReentrantLock();
// Conditions for waiting when buffer is full or empty
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public void put(E item) throws InterruptedException {
lock.lock();
try {
// Wait until there's space in the buffer
while (queue.size() == capacity) {
notFull.await();
}
// Add the item and signal consumers
queue.add(item);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
lock.lock();
try {
// Wait until there's at least one item
while (queue.isEmpty()) {
notEmpty.await();
}
// Remove an item and signal producers
E item = queue.remove();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
}
This example demonstrates:
- Using
ReentrantLock
for thread safety - Using
Condition
objects for thread coordination - Implementing a classic producer-consumer pattern
2. Implementing a Custom Cache with ReadWriteLock
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ConcurrentCache<K, V> {
private final Map<K, CacheEntry<V>> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final long expirationTimeMs;
public ConcurrentCache(long expirationTimeMs) {
this.expirationTimeMs = expirationTimeMs;
}
public V get(K key) {
rwLock.readLock().lock();
try {
CacheEntry<V> entry = cache.get(key);
if (entry == null) {
return null;
}
// Check if the entry has expired
if (entry.isExpired()) {
// Must upgrade to write lock to remove expired entry
// First release read lock to avoid deadlock
rwLock.readLock().unlock();
rwLock.writeLock().lock();
try {
// Check again in case another thread removed it
entry = cache.get(key);
if (entry != null && entry.isExpired()) {
cache.remove(key);
return null;
}
// Downgrade to read lock
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock();
}
}
return entry != null ? entry.getValue() : null;
} finally {
rwLock.readLock().unlock();
}
}
public void put(K key, V value) {
rwLock.writeLock().lock();
try {
cache.put(key, new CacheEntry<>(value, expirationTimeMs));
} finally {
rwLock.writeLock().unlock();
}
}
public void remove(K key) {
rwLock.writeLock().lock();
try {
cache.remove(key);
} finally {
rwLock.writeLock().unlock();
}
}
public void clear() {
rwLock.writeLock().lock();
try {
cache.clear();
} finally {
rwLock.writeLock().unlock();
}
}
private static class CacheEntry<V> {
private final V value;
private final long expirationTime;
CacheEntry(V value, long expirationTimeMs) {
this.value = value;
this.expirationTime = System.currentTimeMillis() + expirationTimeMs;
}
boolean isExpired() {
return System.currentTimeMillis() > expirationTime;
}
V getValue() {
return value;
}
}
}
This example demonstrates:
- Using
ReadWriteLock
for a cache implementation - Lock downgrading (write → read)
- Handling expiration of cache entries
3. Implementing a Timeout-Based Lock Acquisition Strategy
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ResourceManager {
private final ReentrantLock[] locks;
private final long timeoutMs;
public ResourceManager(int numResources, long timeoutMs) {
this.locks = new ReentrantLock[numResources];
this.timeoutMs = timeoutMs;
for (int i = 0; i < numResources; i++) {
locks[i] = new ReentrantLock();
}
}
public boolean useResources(int[] resourceIds) {
// Sort resource IDs to prevent deadlock
java.util.Arrays.sort(resourceIds);
// Track which locks we've acquired
boolean[] acquired = new boolean[resourceIds.length];
try {
// Try to acquire all locks with timeout
for (int i = 0; i < resourceIds.length; i++) {
int resourceId = resourceIds[i];
if (locks[resourceId].tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
acquired[i] = true;
} else {
// If any lock acquisition fails, release all acquired locks
for (int j = 0; j < i; j++) {
if (acquired[j]) {
locks[resourceIds[j]].unlock();
}
}
return false;
}
}
// Use the resources
useResourcesInternal(resourceIds);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
// Release all acquired locks
for (int i = 0; i < resourceIds.length; i++) {
if (acquired[i]) {
locks[resourceIds[i]].unlock();
}
}
}
}
private void useResourcesInternal(int[] resourceIds) {
// Simulate using the resources
System.out.println(Thread.currentThread().getName() + " using resources: " +
java.util.Arrays.toString(resourceIds));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
This example demonstrates:
- Using
tryLock()
with timeout to prevent deadlock - Acquiring multiple locks in a consistent order
- Releasing locks if acquisition fails
- Handling interruption during lock acquisition
🚀 Why It Matters / Use Cases
Understanding and effectively using ReentrantLock
and ReadWriteLock
is crucial for several reasons:
1. Enhanced Control and Flexibility
Explicit locks provide capabilities that intrinsic locks (synchronized) don't offer:
- Timeout-based lock acquisition: Prevent threads from blocking indefinitely
- Interruptible lock acquisition: Allow threads to be cancelled while waiting
- Non-blocking lock attempts: Try to acquire a lock without blocking
- Fair queueing: Ensure locks are granted in the order requested
Real-world example: A responsive user interface that needs to access shared resources without freezing if those resources are unavailable.
2. Performance Optimization
In certain scenarios, explicit locks can provide better performance:
- Read-write locks: Dramatically improve throughput for read-heavy workloads
- Fine-grained locking: Lock only what needs to be locked, for only as long as needed
- Lock stripping: Divide a resource into multiple independently locked segments
Real-world example: A high-performance database cache that needs to handle thousands of concurrent read operations with occasional updates.
3. Advanced Concurrency Patterns
Explicit locks enable more sophisticated concurrency control:
- Resource pools: Manage limited resources with timeout-based acquisition
- Work stealing: Allow threads to "steal" work from other threads' queues
- Reader-writer patterns: Optimize for different access patterns
- Condition variables: Coordinate between threads based on specific conditions
Real-world example: A job scheduling system that needs to coordinate work across multiple worker threads with different priorities and dependencies.
4. Common Use Cases
High-Concurrency Data Structures
- Concurrent caches: Using read-write locks for efficient caching
- Thread-safe collections: Building custom concurrent collections
- Connection pools: Managing database or network connections
Responsive Applications
- UI responsiveness: Preventing UI freezes with timeout-based locking
- Cancellable operations: Supporting user-initiated cancellation
- Progress reporting: Allowing threads to check progress while waiting
Resource Management
- Limited resource allocation: Managing scarce resources
- Deadlock prevention: Using timeout-based acquisition and ordered locking
- Priority-based access: Implementing custom fairness policies
Distributed Systems
- Distributed locks: Building blocks for distributed synchronization
- Leader election: Implementing consensus algorithms
- Partitioned data access: Coordinating access to sharded data
🧭 Best Practices for Java ReentrantLock and ReadWriteLock
1. Always Release Locks in a Finally Block
✅ DO:
- Always place
unlock()
calls in a finally block - Release locks in the reverse order of acquisition
- Check if you hold the lock before releasing it (if necessary)
❌ DON'T:
- Release locks in normal code paths where exceptions might prevent execution
- Forget to release locks in error handling paths
- Return or break out of a method without releasing locks
// GOOD: Proper lock release in finally block
public void processData() {
lock.lock();
try {
// Critical section
} finally {
lock.unlock(); // Always executed, even if an exception occurs
}
}
// BAD: Lock might not be released if an exception occurs
public void processDataBad() {
lock.lock();
// Critical section
lock.unlock(); // Might not be reached if an exception occurs
}
2. Minimize the Scope of Lock Holding
✅ DO:
- Hold locks for the shortest time possible
- Perform expensive operations outside of locked sections
- Use read locks instead of write locks when possible
❌ DON'T:
- Hold locks during I/O operations or network calls
- Perform lengthy computations while holding locks
- Acquire locks unnecessarily early or release them unnecessarily late
// GOOD: Minimized lock scope
public void processData(List<String> data) {
// Prepare data outside of lock
List<String> preparedData = prepareData(data);
lock.lock();
try {
// Only lock for the critical section
for (String item : preparedData) {
sharedResource.add(item);
}
} finally {
lock.unlock();
}
// Process results outside of lock
processResults();
}
// BAD: Excessive lock holding
public void processDataBad(List<String> data) {
lock.lock();
try {
// Preparing data while holding the lock
List<String> preparedData = prepareData(data);
// Critical section
for (String item : preparedData) {
sharedResource.add(item);
}
// Processing results while still holding the lock
processResults();
} finally {
lock.unlock();
}
}
3. Be Consistent with Lock Ordering
✅ DO:
- Always acquire locks in a consistent order
- Consider using resource IDs or hash codes to determine lock order
- Document your locking order conventions
❌ DON'T:
- Acquire locks in different orders in different parts of your code
- Create circular dependencies between locks
- Ignore the potential for deadlocks
// GOOD: Consistent lock ordering
public void transferMoney(Account from, Account to, double amount) {
// Always lock accounts in a consistent order based on account ID
Account firstLock = from.getId() < to.getId() ? from : to;
Account secondLock = from.getId() < to.getId() ? to : from;
firstLock.getLock().lock();
try {
secondLock.getLock().lock();
try {
// Transfer logic
if (from.getBalance() >= amount) {
from.debit(amount);
to.credit(amount);
}
} finally {
secondLock.getLock().unlock();
}
} finally {
firstLock.getLock().unlock();
}
}
4. Use the Right Lock for the Job
✅ DO:
- Use
ReadWriteLock
for read-heavy workloads - Use
ReentrantLock
when you need advanced features - Use
synchronized
for simple cases where advanced features aren't needed
❌ DON'T:
- Use
ReentrantLock
whensynchronized
would suffice - Use write locks when read locks would be sufficient
- Ignore the performance characteristics of different lock types
// GOOD: Using ReadWriteLock for a read-heavy scenario
public class OptimizedCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public V get(K key) {
rwLock.readLock().lock(); // Multiple readers can access simultaneously
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(K key, V value) {
rwLock.writeLock().lock(); // Exclusive access for writers
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
5. Be Careful with Condition Variables
✅ DO:
- Always check condition predicates in a loop
- Signal conditions after modifying the state they depend on
- Hold the lock when calling
await()
,signal()
, andsignalAll()
❌ DON'T:
- Assume that a thread will resume immediately after being signaled
- Forget that
await()
releases the lock while waiting - Use
if
instead ofwhile
when checking condition predicates
// GOOD: Proper condition variable usage
public void awaitCondition() throws InterruptedException {
lock.lock();
try {
while (!conditionMet()) { // Always use a loop
condition.await();
}
// Process when condition is met
} finally {
lock.unlock();
}
}
public void signalCondition() {
lock.lock();
try {
// Update state
updateState();
// Signal after state change
condition.signal();
} finally {
lock.unlock();
}
}
⚠️ Common Pitfalls When Using Java ReentrantLock
1. Forgetting to Release Locks
One of the most common mistakes is failing to release a lock, especially in error paths.
// INCORRECT: Lock might not be released
public void processData() {
lock.lock();
// If an exception occurs here, the lock will never be released
processItem();
lock.unlock();
}
Why it fails: If processItem()
throws an exception, the unlock()
call will never be executed.
Solution: Always use a try-finally block to ensure locks are released.
// CORRECT: Lock will always be released
public void processData() {
lock.lock();
try {
processItem();
} finally {
lock.unlock();
}
}
2. Deadlocks from Inconsistent Lock Ordering
Deadlocks can occur when different threads acquire the same locks in different orders.
// Thread 1
lock1.lock();
try {
// Do something
lock2.lock(); // Might deadlock if Thread 2 holds lock2 and wants lock1
try {
// Do something with both locks
} finally {
lock2.unlock();
}
} finally {
lock1.unlock();
}
// Thread 2
lock2.lock();
try {
// Do something
lock1.lock(); // Might deadlock if Thread 1 holds lock1 and wants lock2
try {
// Do something with both locks
} finally {
lock1.unlock();
}
} finally {
lock2.unlock();
}
Why it fails: If Thread 1 acquires lock1
and Thread 2 acquires lock2
, then each thread will wait indefinitely for the other lock.
Solution: Always acquire locks in a consistent order.
3. Unlocking a Lock You Don't Hold
Attempting to unlock a lock that you don't hold will throw an IllegalMonitorStateException
.
// INCORRECT: Unlocking a lock you don't hold
public void incorrectUnlock() {
// This thread doesn't hold the lock
lock.unlock(); // Throws IllegalMonitorStateException
}
Why it fails: Unlike synchronized
blocks, which automatically track lock ownership, explicit locks require you to manage lock ownership yourself.
Solution: Only unlock locks that you've acquired, and consider using isHeldByCurrentThread()
to check ownership if necessary.
4. Forgetting That await() Releases the Lock
When a thread calls await()
on a condition, it releases the lock and waits to be signaled.
// INCORRECT: Assuming lock is still held after await()
lock.lock();
try {
condition.await();
// Another thread might have modified shared state here
// because the lock was released during await()
useSharedState(); // Might see inconsistent state
} finally {
lock.unlock();
}
Why it matters: After await()
returns, the thread reacquires the lock, but other threads might have modified the shared state while the lock was released.
Solution: Always recheck your condition predicates after await()
returns, typically using a while loop.
5. Lock Upgrading (Read to Write)
Attempting to upgrade from a read lock to a write lock can lead to deadlock.
// INCORRECT: Attempting to upgrade from read to write lock
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
try {
// Read shared data
if (needToWrite) {
rwLock.writeLock().lock(); // DEADLOCK: Can't acquire write lock while holding read lock
try {
// Update shared data
} finally {
rwLock.writeLock().unlock();
}
}
} finally {
rwLock.readLock().unlock();
}
Why it fails: A thread cannot acquire a write lock while any read locks are held, including its own.
Solution: Release the read lock before acquiring the write lock, or use a write lock from the beginning if updates might be needed.
6. Misusing Fairness Settings
Setting fairness to true
can significantly impact performance.
// Might cause performance issues in high-throughput scenarios
ReentrantLock fairLock = new ReentrantLock(true);
Why it matters: Fair locks ensure threads acquire locks in the order they requested them, but this comes with significant overhead.
Solution: Only use fair locks when necessary to prevent starvation, and be aware of the performance implications.
7. Not Handling InterruptedException Properly
Ignoring or swallowing InterruptedException
can break interruption mechanisms.
// INCORRECT: Swallowing InterruptedException
try {
lock.lockInterruptibly();
try {
// Critical section
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// Ignoring the interruption
}
Why it matters: Interruption is a cooperative mechanism for cancellation. Ignoring it breaks this mechanism.
Solution: Either propagate the exception or restore the interrupt status.
// CORRECT: Properly handling InterruptedException
try {
lock.lockInterruptibly();
try {
// Critical section
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// Restore the interrupt status
Thread.currentThread().interrupt();
// Handle the interruption appropriately
}
📌 Summary / Key Takeaways
-
ReentrantLock provides more flexible locking than synchronized blocks:
- Explicit lock/unlock control
- Timeout-based lock acquisition with
tryLock()
- Interruptible lock acquisition with
lockInterruptibly()
- Fair or unfair lock ordering
- Ability to check lock status and ownership
-
ReadWriteLock optimizes for read-heavy scenarios:
- Multiple threads can hold read locks simultaneously
- Write locks provide exclusive access
- Can significantly improve throughput for read-heavy workloads
- Supports lock downgrading (write → read) but not upgrading
-
Lock Methods:
lock()
: Acquires the lock, blocking indefinitelyunlock()
: Releases the locktryLock()
: Attempts to acquire the lock without blocking indefinitelylockInterruptibly()
: Acquires the lock, but allows thread interruption
-
Condition Variables:
- Allow threads to wait for specific conditions
- More flexible than
wait()
/notify()
- Must be used with the associated lock
-
Best Practices:
- Always release locks in finally blocks
- Minimize the scope of lock holding
- Be consistent with lock ordering
- Use the right lock for the job
- Be careful with condition variables
-
Common Pitfalls:
- Forgetting to release locks
- Deadlocks from inconsistent lock ordering
- Unlocking a lock you don't hold
- Forgetting that
await()
releases the lock - Attempting to upgrade from read to write lock
- Misusing fairness settings
- Not handling
InterruptedException
properly
🧩 Exercises or Mini-Projects
Exercise 1: Implementing a Thread-Safe Resource Pool
Create a resource pool that manages a limited set of resources with timeout-based acquisition.
Requirements:
- Create a generic
ResourcePool<T>
class that manages a fixed number of resources - Implement methods to acquire and release resources
- Support timeout-based acquisition to prevent indefinite blocking
- Allow resources to be marked as invalid and replaced
- Implement proper shutdown behavior
- Ensure thread safety using
ReentrantLock
and conditions - Test with multiple threads concurrently acquiring and releasing resources
Exercise 2: Building a Concurrent Document Editor
Implement a simple document editor that allows multiple readers but exclusive writers.
Requirements:
- Create a
Document
class that represents a text document - Implement methods for reading and modifying the document
- Use
ReadWriteLock
to allow concurrent reads but exclusive writes - Add support for document sections that can be locked independently
- Implement a change history mechanism
- Create a test harness that simulates multiple users accessing the document
- Measure and compare performance with different access patterns
By mastering ReentrantLock
and ReadWriteLock
, you'll have powerful tools for building efficient, responsive concurrent applications. These explicit locking mechanisms provide the flexibility and control needed for advanced concurrency scenarios, while still maintaining the safety guarantees required for correct concurrent code.
Remember that with great power comes great responsibility. Explicit locks give you more control, but they also require more careful management. Always follow best practices, be vigilant about releasing locks, and design your locking strategy thoughtfully to avoid deadlocks and other concurrency hazards.
Happy coding!