Locking Strategies: Managing Shared State
Concurrency is easy until two threads try to modify the same variable at the same time. This is the Critical Section problem. To prevent data corruption, we use locks. But not all locks are created equal.
In Java, we’ve evolved from the simple synchronized keyword to sophisticated ReadWriteLock and the high-performance StampedLock.
1. The Evolution of Locks
- synchronized: Built-in, reentrant, simple. But you can’t interrupt a thread waiting for it, and it’s strictly block-structured.
- ReentrantLock: Explicit lock object. Supports fairness,
tryLock, and interruptibility. - ReadWriteLock: Allows multiple readers OR one writer. Great for read-heavy workloads.
- StampedLock (Java 8): Optimistic locking. Incredibly fast for reads if contention is low.
2. Interactive: Lock Contention Simulator
See how different locking strategies perform under load. Compare Mutex (Exclusive Lock) vs Read-Write Lock (Shared Readers).
3. Code Implementation: StampedLock
StampedLock is complex but powerful. It introduces “optimistic reads” which don’t block writers unless a write actually happens during the read.
Java Implementation
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.StampedLock;
public class OptimisticCache {
private final Map<String, String> map = new HashMap<>();
private final StampedLock lock = new StampedLock();
public void put(String key, String value) {
long stamp = lock.writeLock();
try {
map.put(key, value);
} finally {
lock.unlockWrite(stamp);
}
}
public String get(String key) {
// 1. Try Optimistic Read (Non-blocking)
long stamp = lock.tryOptimisticRead();
String value = map.get(key); // Warning: map state might be inconsistent here!
// 2. Validate the stamp
// If the stamp changed (write occurred), this returns false
if (!lock.validate(stamp)) {
// 3. Fallback to Read Lock (Blocking)
stamp = lock.readLock();
try {
value = map.get(key);
} finally {
lock.unlockRead(stamp);
}
}
return value;
}
}
[!CAUTION] StampedLock is NOT Reentrant! If a thread holds a write lock and tries to acquire it again, it will deadlock itself. Always use it inside
try-finallyblocks.
Go Implementation: RWMutex
Go’s sync.RWMutex is the standard way to handle multiple readers. It does not have an “optimistic” mode like StampedLock, but it is highly optimized for read-heavy workloads.
package main
import (
"fmt"
"sync"
"time"
)
type SafeCache struct {
mu sync.RWMutex
store map[string]string
}
func (c *SafeCache) Put(key, value string) {
c.mu.Lock() // Exclusive Write Lock
defer c.mu.Unlock()
c.store[key] = value
}
func (c *SafeCache) Get(key string) string {
c.mu.RLock() // Shared Read Lock
defer c.mu.RUnlock()
return c.store[key]
}
func main() {
cache := &SafeCache{store: make(map[string]string)}
// Writer
go func() {
for i := 0; i < 5; i++ {
cache.Put("key", fmt.Sprintf("val-%d", i))
time.Sleep(100 * time.Millisecond)
}
}()
// Readers
for i := 0; i < 3; i++ {
go func(id int) {
for {
val := cache.Get("key")
fmt.Printf("Reader %d: %s\n", id, val)
time.Sleep(50 * time.Millisecond)
}
}(id)
}
time.Sleep(1 * time.Second)
}
4. When to Use What?
| Scenario | Recommended Lock | Why? |
|---|---|---|
| General Purpose | ReentrantLock |
Safe, debuggable, interruptible. |
| Read-Heavy (90%+) | ReadWriteLock |
Allows concurrent readers. |
| High Performance | StampedLock |
Optimistic reads avoid cache contention. |
| Simple State | AtomicReference |
No locks needed for single variables. |
5. Summary
Choosing the right lock is a trade-off between safety and performance. While synchronized is sufficient for most apps, high-throughput systems benefit from ReadWriteLock or StampedLock. In Go, sync.RWMutex is your workhorse for shared state.