Introduction to StampedLock
StampedLock introduced in Java 8 is a capability-based lock with three modes for controlling read/write access. It supports optimistic reading, which can dramatically improve performance in read-heavy scenarios by avoiding writer starvation.
1. Basic StampedLock Modes
Lock Modes Overview
import java.util.concurrent.locks.StampedLock;
public class StampedLockBasic {
private final StampedLock lock = new StampedLock();
private String data = "Initial Data";
// WRITE LOCK - Exclusive access
public void writeData(String newData) {
long stamp = lock.writeLock(); // Block until write lock acquired
try {
System.out.println(Thread.currentThread().getName() + " acquired write lock");
data = newData;
Thread.sleep(1000); // Simulate write operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlockWrite(stamp);
System.out.println(Thread.currentThread().getName() + " released write lock");
}
}
// READ LOCK - Shared access
public String readData() {
long stamp = lock.readLock(); // Block until read lock acquired
try {
System.out.println(Thread.currentThread().getName() + " acquired read lock");
Thread.sleep(500); // Simulate read operation
return data;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
lock.unlockRead(stamp);
System.out.println(Thread.currentThread().getName() + " released read lock");
}
}
// OPTIMISTIC READ - No locking, but validation required
public String optimisticRead() {
long stamp = lock.tryOptimisticRead(); // Non-blocking
String currentData = data; // Read data optimistically
// Check if read was consistent (no write occurred during read)
if (!lock.validate(stamp)) {
System.out.println(Thread.currentThread().getName() + " optimistic read failed, upgrading to read lock");
// Fallback to read lock
stamp = lock.readLock();
try {
currentData = data;
} finally {
lock.unlockRead(stamp);
}
} else {
System.out.println(Thread.currentThread().getName() + " optimistic read succeeded");
}
return currentData;
}
public static void main(String[] args) throws InterruptedException {
StampedLockBasic example = new StampedLockBasic();
// Writer thread
Thread writer = new Thread(() -> {
example.writeData("Updated Data");
}, "Writer-Thread");
// Reader threads
Thread reader1 = new Thread(() -> {
System.out.println("Read result: " + example.readData());
}, "Reader-1");
Thread reader2 = new Thread(() -> {
System.out.println("Optimistic read result: " + example.optimisticRead());
}, "Reader-2");
writer.start();
Thread.sleep(100); // Let writer start first
reader1.start();
reader2.start();
writer.join();
reader1.join();
reader2.join();
}
}
Lock Mode Comparison
| Mode | Characteristics | Use Case |
|---|---|---|
| Write Lock | Exclusive, blocking | Data modification |
| Read Lock | Shared, blocking | Safe data reading |
| Optimistic Read | Non-blocking, requires validation | Read-heavy, low contention |
2. Optimistic Reading Patterns
Basic Optimistic Read Pattern
import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.ThreadLocalRandom;
public class OptimisticReadingBasic {
private final StampedLock lock = new StampedLock();
private int counter = 0;
private String message = "Hello";
/**
* Standard pattern for optimistic reading:
* 1. tryOptimisticRead() - get stamp without blocking
* 2. Read data fields
* 3. validate(stamp) - check if read was consistent
* 4. If invalid, fallback to read lock
*/
public String readDataOptimistically() {
long stamp = lock.tryOptimisticRead();
// Capture all fields needed for consistent read
int currentCounter = counter;
String currentMessage = message;
// Critical: Validate AFTER reading all fields
if (!lock.validate(stamp)) {
// Optimistic read failed - acquire read lock
stamp = lock.readLock();
try {
currentCounter = counter;
currentMessage = message;
} finally {
lock.unlockRead(stamp);
}
}
return String.format("Counter: %d, Message: %s", currentCounter, currentMessage);
}
public void updateData(int newCounter, String newMessage) {
long stamp = lock.writeLock();
try {
// Simulate some work
Thread.sleep(ThreadLocalRandom.current().nextInt(50, 200));
counter = newCounter;
message = newMessage;
System.out.printf("Writer: Updated to counter=%d, message=%s%n", newCounter, newMessage);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlockWrite(stamp);
}
}
public static void main(String[] args) throws InterruptedException {
OptimisticReadingBasic example = new OptimisticReadingBasic();
// Start multiple readers
for (int i = 0; i < 5; i++) {
final int readerId = i;
new Thread(() -> {
for (int j = 0; j < 3; j++) {
String result = example.readDataOptimistically();
System.out.printf("Reader-%d: %s%n", readerId, result);
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(100, 300));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Reader-" + i).start();
}
// Start a writer
Thread writer = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
example.updateData(i, "Message-" + i);
try {
Thread.sleep(500); // Write less frequently
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Writer");
writer.start();
writer.join();
}
}
Complex Object Optimistic Reading
import java.util.concurrent.locks.StampedLock;
import java.util.*;
public class ComplexObjectOptimisticRead {
private final StampedLock lock = new StampedLock();
private final Map<String, Integer> dataMap = new HashMap<>();
private int version = 0;
private long lastUpdated = System.currentTimeMillis();
public ComplexObjectOptimisticRead() {
// Initialize with some data
dataMap.put("A", 1);
dataMap.put("B", 2);
dataMap.put("C", 3);
}
/**
* Optimistic read for complex object state
* Must capture ALL fields needed for consistent view
*/
public Snapshot getSnapshotOptimistically() {
long stamp = lock.tryOptimisticRead();
// Capture all fields needed for consistent snapshot
int currentVersion = version;
long currentLastUpdated = lastUpdated;
Map<String, Integer> currentDataMap = new HashMap<>(dataMap); // Defensive copy
if (!lock.validate(stamp)) {
// Fallback to read lock
stamp = lock.readLock();
try {
currentVersion = version;
currentLastUpdated = lastUpdated;
currentDataMap = new HashMap<>(dataMap);
} finally {
lock.unlockRead(stamp);
}
}
return new Snapshot(currentVersion, currentLastUpdated, currentDataMap);
}
public void updateData(String key, Integer value) {
long stamp = lock.writeLock();
try {
version++;
lastUpdated = System.currentTimeMillis();
dataMap.put(key, value);
System.out.printf("Writer: Updated %s=%d (version=%d)%n", key, value, version);
Thread.sleep(100); // Simulate write latency
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlockWrite(stamp);
}
}
public void bulkUpdate(Map<String, Integer> updates) {
long stamp = lock.writeLock();
try {
version++;
lastUpdated = System.currentTimeMillis();
dataMap.putAll(updates);
System.out.printf("Writer: Bulk update (version=%d, %d entries)%n",
version, updates.size());
Thread.sleep(200); // Simulate longer write operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlockWrite(stamp);
}
}
// Snapshot class to hold consistent view
public static class Snapshot {
private final int version;
private final long lastUpdated;
private final Map<String, Integer> data;
public Snapshot(int version, long lastUpdated, Map<String, Integer> data) {
this.version = version;
this.lastUpdated = lastUpdated;
this.data = Collections.unmodifiableMap(new HashMap<>(data));
}
public int getVersion() { return version; }
public long getLastUpdated() { return lastUpdated; }
public Map<String, Integer> getData() { return data; }
@Override
public String toString() {
return String.format("Snapshot{version=%d, lastUpdated=%d, data=%s}",
version, lastUpdated, data);
}
}
public static void main(String[] args) throws InterruptedException {
ComplexObjectOptimisticRead example = new ComplexObjectOptimisticRead();
// Start multiple readers
List<Thread> readers = new ArrayList<>();
for (int i = 0; i < 3; i++) {
final int readerId = i;
Thread reader = new Thread(() -> {
for (int j = 0; j < 5; j++) {
Snapshot snapshot = example.getSnapshotOptimistically();
System.out.printf("Reader-%d: %s%n", readerId, snapshot);
try {
Thread.sleep(150);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Reader-" + i);
readers.add(reader);
reader.start();
}
// Start writers
Thread writer1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
example.updateData("Key-" + i, i * 10);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Writer-1");
Thread writer2 = new Thread(() -> {
Map<String, Integer> bulkData = Map.of(
"X", 100,
"Y", 200,
"Z", 300
);
example.bulkUpdate(bulkData);
}, "Writer-2");
writer1.start();
writer2.start();
// Wait for completion
writer1.join();
writer2.join();
for (Thread reader : readers) {
reader.join();
}
}
}
3. Advanced Optimistic Patterns
Try-Convert Pattern
import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.ThreadLocalRandom;
public class TryConvertPattern {
private final StampedLock lock = new StampedLock();
private String data = "Initial";
private int readCount = 0;
private int writeCount = 0;
/**
* Advanced pattern: tryConvertToWriteLock
* Useful when read might need to upgrade to write
*/
public boolean updateIfCondition(String newData, String expectedCurrent) {
long stamp = lock.readLock();
try {
// Check condition under read lock
while (data.equals(expectedCurrent)) {
// Try to upgrade to write lock
long writeStamp = lock.tryConvertToWriteLock(stamp);
if (writeStamp != 0) {
// Successfully upgraded
stamp = writeStamp;
data = newData;
writeCount++;
System.out.printf("Successfully updated: %s -> %s (converted)%n",
expectedCurrent, newData);
return true;
} else {
// Failed to upgrade, release read lock and acquire write lock
lock.unlockRead(stamp);
stamp = lock.writeLock();
// Re-check condition under write lock
if (data.equals(expectedCurrent)) {
data = newData;
writeCount++;
System.out.printf("Successfully updated: %s -> %s (write lock)%n",
expectedCurrent, newData);
return true;
} else {
// Condition changed, no update needed
return false;
}
}
}
return false; // Condition not met
} finally {
lock.unlock(stamp);
}
}
/**
* Optimistic read with potential upgrade
*/
public String readAndConditionallyUpdate() {
long stamp = lock.tryOptimisticRead();
String currentData = data;
int currentReadCount = readCount;
if (!lock.validate(stamp)) {
// Fallback to read lock
stamp = lock.readLock();
try {
currentData = data;
currentReadCount = readCount;
} finally {
lock.unlockRead(stamp);
}
}
readCount++;
// Conditionally update based on read
if (currentReadCount % 5 == 0) {
updateIfCondition("Updated-at-" + currentReadCount, currentData);
}
return currentData;
}
public String getData() {
long stamp = lock.readLock();
try {
return data;
} finally {
lock.unlockRead(stamp);
}
}
public static void main(String[] args) throws InterruptedException {
TryConvertPattern example = new TryConvertPattern();
// Start multiple worker threads
List<Thread> workers = new ArrayList<>();
for (int i = 0; i < 5; i++) {
final int workerId = i;
Thread worker = new Thread(() -> {
for (int j = 0; j < 10; j++) {
String result = example.readAndConditionallyUpdate();
System.out.printf("Worker-%d: %s%n", workerId, result);
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(50, 150));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Worker-" + i);
workers.add(worker);
worker.start();
}
for (Thread worker : workers) {
worker.join();
}
System.out.println("Final data: " + example.getData());
}
}
Time-Bounded Operations
import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadLocalRandom;
public class TimeBoundedOperations {
private final StampedLock lock = new StampedLock();
private String data = "Initial Data";
private final long version = 1L;
/**
* Try optimistic read with timeout fallback
*/
public String readWithTimeout(long timeout, TimeUnit unit) {
long startTime = System.nanoTime();
long timeoutNanos = unit.toNanos(timeout);
// First attempt: optimistic read
long stamp = lock.tryOptimisticRead();
String currentData = data;
if (lock.validate(stamp)) {
System.out.println(Thread.currentThread().getName() + ": Optimistic read succeeded");
return currentData;
}
// Optimistic read failed, try timed read lock
try {
stamp = lock.tryReadLock(timeoutNanos, TimeUnit.NANOSECONDS);
if (stamp != 0) {
try {
long elapsed = System.nanoTime() - startTime;
System.out.printf("%s: Read lock acquired after %d ms%n",
Thread.currentThread().getName(),
TimeUnit.NANOSECONDS.toMillis(elapsed));
currentData = data;
return currentData;
} finally {
lock.unlockRead(stamp);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Timeout or interrupt
System.out.println(Thread.currentThread().getName() + ": Read timeout");
return "Timeout - unable to read";
}
/**
* Try write lock with timeout
*/
public boolean writeWithTimeout(String newData, long timeout, TimeUnit unit) {
try {
long stamp = lock.tryWriteLock(timeout, unit);
if (stamp != 0) {
try {
data = newData;
System.out.println(Thread.currentThread().getName() + ": Write completed");
return true;
} finally {
lock.unlockWrite(stamp);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + ": Write timeout");
return false;
}
/**
* Optimistic read with retry mechanism
*/
public String optimisticReadWithRetry(int maxRetries) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
long stamp = lock.tryOptimisticRead();
String currentData = data;
if (lock.validate(stamp)) {
System.out.printf("%s: Optimistic read succeeded on attempt %d%n",
Thread.currentThread().getName(), attempt);
return currentData;
}
// Brief backoff before retry
if (attempt < maxRetries) {
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
// All optimistic attempts failed, fallback to read lock
long stamp = lock.readLock();
try {
System.out.printf("%s: Fell back to read lock after %d retries%n",
Thread.currentThread().getName(), maxRetries);
return data;
} finally {
lock.unlockRead(stamp);
}
}
public static void main(String[] args) throws InterruptedException {
TimeBoundedOperations example = new TimeBoundedOperations();
// Writer thread that holds lock for varying durations
Thread writer = new Thread(() -> {
for (int i = 0; i < 3; i++) {
boolean success = example.writeWithTimeout("Data-" + i, 2, TimeUnit.SECONDS);
if (success) {
try {
// Hold write lock for varying times
Thread.sleep(ThreadLocalRandom.current().nextInt(500, 1500));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}, "Writer");
// Reader threads with different strategies
Thread timeoutReader = new Thread(() -> {
for (int i = 0; i < 5; i++) {
String result = example.readWithTimeout(500, TimeUnit.MILLISECONDS);
System.out.println("TimeoutReader got: " + result);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "TimeoutReader");
Thread retryReader = new Thread(() -> {
for (int i = 0; i < 5; i++) {
String result = example.optimisticReadWithRetry(3);
System.out.println("RetryReader got: " + result);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "RetryReader");
writer.start();
timeoutReader.start();
retryReader.start();
writer.join();
timeoutReader.join();
retryReader.join();
}
}
4. Performance Comparison
Benchmark: ReadLock vs Optimistic Read
import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class PerformanceComparison {
private final StampedLock lock = new StampedLock();
private final AtomicLong data = new AtomicLong(0);
private final int READER_COUNT = 10;
private final int WRITER_COUNT = 2;
private final int OPERATIONS_PER_THREAD = 10000;
/**
* Traditional read lock approach
*/
public long readWithReadLock() {
long stamp = lock.readLock();
try {
return data.get();
} finally {
lock.unlockRead(stamp);
}
}
/**
* Optimistic read approach
*/
public long readWithOptimisticLock() {
long stamp = lock.tryOptimisticRead();
long value = data.get();
if (!lock.validate(stamp)) {
// Fallback to read lock
stamp = lock.readLock();
try {
value = data.get();
} finally {
lock.unlockRead(stamp);
}
}
return value;
}
public void write(long newValue) {
long stamp = lock.writeLock();
try {
data.set(newValue);
} finally {
lock.unlockWrite(stamp);
}
}
public void runBenchmark() throws InterruptedException {
System.out.println("=== StampedLock Performance Comparison ===");
System.out.printf("Readers: %d, Writers: %d, Operations per thread: %d%n%n",
READER_COUNT, WRITER_COUNT, OPERATIONS_PER_THREAD);
// Test with read locks
testWithReadLocks();
// Test with optimistic reads
testWithOptimisticReads();
}
private void testWithReadLocks() throws InterruptedException {
data.set(0);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(READER_COUNT + WRITER_COUNT);
AtomicLong totalReadTime = new AtomicLong(0);
AtomicLong readCount = new AtomicLong(0);
// Create readers
for (int i = 0; i < READER_COUNT; i++) {
new Thread(() -> {
try {
startLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
long startTime = System.nanoTime();
readWithReadLock();
long endTime = System.nanoTime();
totalReadTime.addAndGet(endTime - startTime);
readCount.incrementAndGet();
}
endLatch.countDown();
}).start();
}
// Create writers
for (int i = 0; i < WRITER_COUNT; i++) {
new Thread(() -> {
try {
startLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int j = 0; j < OPERATIONS_PER_THREAD / 10; j++) { // Fewer writes
write(ThreadLocalRandom.current().nextLong());
try {
Thread.sleep(1); // Simulate write work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
endLatch.countDown();
}).start();
}
// Run test
long startTime = System.nanoTime();
startLatch.countDown();
endLatch.await();
long totalTime = System.nanoTime() - startTime;
System.out.println("=== READ LOCK RESULTS ===");
printResults(totalTime, totalReadTime.get(), readCount.get());
}
private void testWithOptimisticReads() throws InterruptedException {
data.set(0);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(READER_COUNT + WRITER_COUNT);
AtomicLong totalReadTime = new AtomicLong(0);
AtomicLong readCount = new AtomicLong(0);
AtomicLong optimisticSuccess = new AtomicLong(0);
// Create readers
for (int i = 0; i < READER_COUNT; i++) {
new Thread(() -> {
try {
startLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
long startTime = System.nanoTime();
long value = readWithOptimisticLock();
long endTime = System.nanoTime();
totalReadTime.addAndGet(endTime - startTime);
readCount.incrementAndGet();
// Track optimistic success rate (simplified)
if (value >= 0) { // In real scenario, track actual optimistic success
optimisticSuccess.incrementAndGet();
}
}
endLatch.countDown();
}).start();
}
// Create writers (same as before)
for (int i = 0; i < WRITER_COUNT; i++) {
new Thread(() -> {
try {
startLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int j = 0; j < OPERATIONS_PER_THREAD / 10; j++) {
write(ThreadLocalRandom.current().nextLong());
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
endLatch.countDown();
}).start();
}
// Run test
long startTime = System.nanoTime();
startLatch.countDown();
endLatch.await();
long totalTime = System.nanoTime() - startTime;
System.out.println("=== OPTIMISTIC READ RESULTS ===");
printResults(totalTime, totalReadTime.get(), readCount.get());
System.out.printf("Optimistic success rate: %.2f%%%n%n",
(optimisticSuccess.get() * 100.0 / readCount.get()));
}
private void printResults(long totalTime, long totalReadTime, long readCount) {
double totalMs = totalTime / 1_000_000.0;
double avgReadTimeNs = totalReadTime / (double) readCount;
System.out.printf("Total time: %.2f ms%n", totalMs);
System.out.printf("Average read time: %.2f ns%n", avgReadTimeNs);
System.out.printf("Throughput: %.2f operations/ms%n%n",
readCount / totalMs);
}
public static void main(String[] args) throws InterruptedException {
PerformanceComparison benchmark = new PerformanceComparison();
benchmark.runBenchmark();
}
}
5. Real-World Use Cases
Cached Data with StampedLock
import java.util.concurrent.locks.StampedLock;
import java.util.*;
import java.util.concurrent.*;
public class CachedDataWithStampedLock {
private final StampedLock lock = new StampedLock();
private final Map<String, CacheEntry> cache = new HashMap<>();
private final long cacheTimeoutMs;
public CachedDataWithStampedLock(long cacheTimeoutMs) {
this.cacheTimeoutMs = cacheTimeoutMs;
}
/**
* Get data with optimistic read for cache hit
*/
public String get(String key) {
// First attempt: optimistic read
long stamp = lock.tryOptimisticRead();
CacheEntry entry = cache.get(key);
if (entry != null && !isExpired(entry) && lock.validate(stamp)) {
// Fast path: cache hit with optimistic read
recordCacheHit("optimistic");
return entry.data;
}
// Slow path: acquire read lock
stamp = lock.readLock();
try {
entry = cache.get(key);
if (entry != null && !isExpired(entry)) {
recordCacheHit("readLock");
return entry.data;
}
} finally {
lock.unlockRead(stamp);
}
// Cache miss - load data
return loadData(key);
}
/**
* Load data with write lock
*/
private String loadData(String key) {
long stamp = lock.writeLock();
try {
// Double-check under write lock
CacheEntry existing = cache.get(key);
if (existing != null && !isExpired(existing)) {
recordCacheHit("writeLock-doubleCheck");
return existing.data;
}
// Simulate expensive data loading
String data = expensiveLoadOperation(key);
cache.put(key, new CacheEntry(data, System.currentTimeMillis()));
recordCacheMiss();
return data;
} finally {
lock.unlockWrite(stamp);
}
}
/**
* Bulk update cache
*/
public void bulkUpdate(Map<String, String> newData) {
long stamp = lock.writeLock();
try {
long now = System.currentTimeMillis();
for (Map.Entry<String, String> entry : newData.entrySet()) {
cache.put(entry.getKey(), new CacheEntry(entry.getValue(), now));
}
System.out.println("Bulk updated " + newData.size() + " entries");
} finally {
lock.unlockWrite(stamp);
}
}
/**
* Clear expired entries
*/
public void cleanup() {
long stamp = lock.writeLock();
try {
Iterator<Map.Entry<String, CacheEntry>> it = cache.entrySet().iterator();
int removed = 0;
while (it.hasNext()) {
if (isExpired(it.next().getValue())) {
it.remove();
removed++;
}
}
System.out.println("Cleaned up " + removed + " expired entries");
} finally {
lock.unlockWrite(stamp);
}
}
private boolean isExpired(CacheEntry entry) {
return System.currentTimeMillis() - entry.timestamp > cacheTimeoutMs;
}
private String expensiveLoadOperation(String key) {
try {
Thread.sleep(100); // Simulate expensive operation
return "Data-for-" + key + "-" + System.currentTimeMillis();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Load interrupted", e);
}
}
// Statistics
private final Map<String, AtomicLong> stats = new ConcurrentHashMap<>();
private void recordCacheHit(String type) {
stats.computeIfAbsent("hit." + type, k -> new AtomicLong()).incrementAndGet();
}
private void recordCacheMiss() {
stats.computeIfAbsent("miss", k -> new AtomicLong()).incrementAndGet();
}
public void printStats() {
System.out.println("\n=== Cache Statistics ===");
stats.forEach((key, value) -> System.out.printf("%s: %d%n", key, value.get()));
}
private static class CacheEntry {
final String data;
final long timestamp;
CacheEntry(String data, long timestamp) {
this.data = data;
this.timestamp = timestamp;
}
}
public static void main(String[] args) throws InterruptedException {
CachedDataWithStampedLock cache = new CachedDataWithStampedLock(5000); // 5 second timeout
// Start readers
List<Thread> readers = new ArrayList<>();
for (int i = 0; i < 10; i++) {
final int readerId = i;
Thread reader = new Thread(() -> {
for (int j = 0; j < 20; j++) {
String key = "key-" + (j % 5); // Limited key set for cache hits
String data = cache.get(key);
System.out.printf("Reader-%d: %s = %s%n", readerId, key, data);
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(50, 200));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
readers.add(reader);
reader.start();
}
// Start periodic writer
Thread writer = new Thread(() -> {
for (int i = 0; i < 3; i++) {
Map<String, String> updates = Map.of(
"key-0", "updated-" + i,
"key-1", "updated-" + i,
"key-6", "new-key-" + i
);
cache.bulkUpdate(updates);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
writer.start();
// Start cleanup
Thread cleanup = new Thread(() -> {
for (int i = 0; i < 2; i++) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
cache.cleanup();
}
});
cleanup.start();
// Wait for completion
for (Thread reader : readers) {
reader.join();
}
writer.join();
cleanup.join();
cache.printStats();
}
}
6. Best Practices and Pitfalls
StampedLock Best Practices
import java.util.concurrent.locks.StampedLock;
public class StampedLockBestPractices {
private final StampedLock lock = new StampedLock();
private String data1 = "Data1";
private String data2 = "Data2";
private int version = 0;
/**
* GOOD: Proper optimistic read pattern
*/
public String readConsistentDataGood() {
long stamp = lock.tryOptimisticRead();
// Read ALL related fields before validation
String currentData1 = data1;
String currentData2 = data2;
int currentVersion = version;
// Validate AFTER reading all fields
if (!lock.validate(stamp)) {
// Fallback to read lock
stamp = lock.readLock();
try {
currentData1 = data1;
currentData2 = data2;
currentVersion = version;
} finally {
lock.unlockRead(stamp);
}
}
return String.format("%s, %s (v%d)", currentData1, currentData2, currentVersion);
}
/**
* BAD: Incorrect optimistic read - validation between reads
*/
public String readConsistentDataBad() {
long stamp = lock.tryOptimisticRead();
String currentData1 = data1;
// WRONG: Validating between reads breaks consistency
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentData1 = data1;
} finally {
lock.unlockRead(stamp);
}
}
String currentData2 = data2; // This might be inconsistent with data1!
return String.format("%s, %s", currentData1, currentData2);
}
/**
* GOOD: Always use try-finally with locks
*/
public void updateDataGood(String newData1, String newData2) {
long stamp = lock.writeLock();
try {
data1 = newData1;
data2 = newData2;
version++;
} finally {
lock.unlockWrite(stamp); // ALWAYS in finally block
}
}
/**
* BAD: Risk of not releasing lock on exception
*/
public void updateDataBad(String newData1, String newData2) {
long stamp = lock.writeLock();
data1 = newData1;
data2 = newData2;
version++;
lock.unlockWrite(stamp); // Not in finally - risky!
}
/**
* GOOD: Proper tryConvert usage
*/
public boolean conditionalUpdateGood(String expected, String newValue) {
long stamp = lock.readLock();
try {
while (data1.equals(expected)) {
long writeStamp = lock.tryConvertToWriteLock(stamp);
if (writeStamp != 0) {
stamp = writeStamp;
data1 = newValue;
return true;
} else {
lock.unlockRead(stamp);
stamp = lock.writeLock();
// Re-check condition under write lock
if (data1.equals(expected)) {
data1 = newValue;
return true;
}
return false;
}
}
return false;
} finally {
lock.unlock(stamp);
}
}
/**
* AVOID: Using synchronized with StampedLock
*/
public void methodWithSynchronized() {
synchronized (this) {
// This can cause deadlocks with StampedLock
long stamp = lock.writeLock(); // DANGEROUS!
try {
// ...
} finally {
lock.unlockWrite(stamp);
}
}
}
/**
* GOOD: Use ThreadLocalRandom instead of Random in concurrent code
*/
public void concurrentRandomAccess() {
long stamp = lock.readLock();
try {
// ThreadLocalRandom is thread-safe and faster
int randomValue = java.util.concurrent.ThreadLocalRandom.current().nextInt(100);
// Use randomValue...
} finally {
lock.unlockRead(stamp);
}
}
}
Summary
Key Benefits of StampedLock Optimistic Reading:
- Performance: Avoids lock acquisition for read-heavy, low-contention scenarios
- Non-blocking: Optimistic reads never block
- Writer Fairness: Prevents writer starvation common in ReadWriteLock
- Flexibility: Multiple lock modes and conversion capabilities
When to Use Optimistic Reading:
- Read-heavy workloads with infrequent writes
- Low contention scenarios
- Large read operations where lock acquisition overhead is significant
- When reads can tolerate occasional retries
Best Practices:
- Always validate optimistic reads after reading all fields
- Use try-finally for lock cleanup
- Provide fallback to read lock when optimistic read fails
- Avoid nested locking with other synchronization mechanisms
- Use tryConvertToWriteLock for read-to-write upgrades
Performance Characteristics:
- Optimistic Read: ~10-50ns (when successful)
- Read Lock: ~100-200ns
- Write Lock: ~200-500ns
- Optimistic Fallback: Additional cost of read lock acquisition
StampedLock with optimistic reading provides excellent performance for read-dominated workloads while maintaining data consistency and writer fairness.