Java Concurrency Evolution: Locks, ReadWriteLocks, and StampedLocks


๐Ÿ“– Table of Contents

  1. What Are Java Locks?
  2. The Lock Interface
  3. Key Implementations
  4. What is a StampedLock?
  5. StampedLock Modes of Operation
  6. Performance and Benefits
  7. ReentrantLock vs. StampedLock
  8. Sample Code Examples
  9. Key Considerations & Best Practices
  10. References

What Are Java Locks?

In Java, locks are synchronization tools used to control access to a shared resource among multiple threads. They are critical for preventing race conditions and ensuring data consistency in concurrent applications. By managing access, locks guarantee that only one thread (or a specified number of threads) can modify shared data at any given time.

// A lock is acquired before accessing a shared resource
// and released after the operation is complete.

The Lock Interface

The java.util.concurrent.locks.Lock interface, introduced in Java 5, provides a more flexible and powerful locking mechanism than the traditional synchronized keyword.

  • โœ… Flexible Control โ€“ Can be acquired and released in different methods.
  • โœ… Advanced Features โ€“ Supports timed, polled, and interruptible lock acquisition.
  • โœ… Fairness Policies โ€“ Can be configured to grant access to the longest-waiting thread.
  • โŒ Manual Release Required โ€“ You are responsible for calling unlock(), typically in a finally block to prevent deadlocks.

Key Implementations

1. ReentrantLock

A reentrant, mutual-exclusion lock with the same basic behavior as a synchronized block but with more features. โ€œReentrantโ€ means a thread that already holds the lock can acquire it again without blocking itself.

2. ReadWriteLock

An interface for locks that maintain a separate pair of locks for read and write access.

  • Multiple threads can hold a read lock simultaneously, as long as no thread holds the write lock.
  • Only one thread can hold the write lock at a time. The most common implementation is ReentrantReadWriteLock.

What is a StampedLock?

Introduced in Java 8, StampedLock is a more advanced locking mechanism that provides capability-based locks with three modes. It is designed to offer higher performance and scalability than ReadWriteLock, especially in read-heavy scenarios.

A key difference is that StampedLock is not reentrant. A thread holding a lock cannot acquire it again, which can lead to deadlocks if not handled carefully.


StampedLock Modes of Operation

  1. Write Lock (writeLock())
  • An exclusive lock. No other read or write locks can be held simultaneously.
  • Returns a stamp (a long value) that must be used to unlock it.
  1. Read Lock (readLock())
  • A non-exclusive lock. Multiple threads can hold a read lock.
  • Blocks if a write lock is held.
  1. Optimistic Read (tryOptimisticRead())
  • This is not a true lockโ€”it doesnโ€™t block writers.
  • It returns a stamp immediately. You read the shared data and then call validate(stamp) to check if a write occurred in the meantime. If it did, you must fall back to a full read lock.

Performance and Benefits

FeatureReentrantLock / ReadWriteLockStampedLock
Primary UseGeneral-purpose mutual exclusionHigh-performance, read-heavy scenarios
ReentrancyYesNo
Lock ModesRead, WriteRead, Write, Optimistic Read
PerformanceGood, but can have contentionExcellent, especially with optimistic reads
Lock ConversionNot supportedSupports upgrading a read lock to a write
ComplexityRelatively straightforwardMore complex to use correctly

ReentrantLock vs. StampedLock

FeatureReentrantLockStampedLock
Backed ByAQS (AbstractQueuedSynchronizer)Internal queuing and state logic
Best ForGeneral thread safetyOptimizing read-throughput
Reentrantโœ… YesโŒ No
Optimistic ModeโŒ Noโœ… Yes
Deadlock RiskLower (if used correctly)Higher (due to non-reentrancy)
When to useMost standard concurrent tasks.When reads far outnumber writes.

Sample Code Examples

1. Using ReentrantLock

import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // Acquire the lock
try {
// Critical section
count++;
} finally {
lock.unlock(); // ALWAYS release the lock in a finally block
}
}
}

2. Using StampedLock for Optimistic Reading

import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
public double optimisticReadDistanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // Get a stamp without blocking
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // Check if a write happened
stamp = sl.readLock(); // Fallback to a real read lock
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}

Key Considerations & Best Practices

โœ… Concept Check

  • ๐Ÿ”ธ Always use a try-finally block to ensure unlock() is called, even if an exception occurs.
  • ๐Ÿ”ธ For resources that are read far more often than written, ReadWriteLock or StampedLock can offer significant performance benefits over a simple ReentrantLock.
  • ๐Ÿ”ธ StampedLock is powerful but complex. Its non-reentrant nature is a common source of bugs.

โš ๏ธ Common Pitfalls

  • Q: Can you use a StampedLock for recursive method calls? A: No. Because it is not reentrant, a thread that holds a lock will block itself if it tries to acquire the same lock again, causing a deadlock.

  • Q: Is StampedLock a direct replacement for ReadWriteLock? A: Not always. Itโ€™s an alternative with different trade-offs. Its complexity and non-reentrant nature mean ReadWriteLock is often a safer and simpler choice if you donโ€™t need the extreme performance of optimistic reads.

  • Q: What is the biggest risk with locks? A: Deadlock. This occurs when two or more threads are blocked forever, each waiting for a lock held by the other. Careful lock ordering and design are necessary to prevent this.


References