問題背景
Config 讀取、Cache 查詢、靜態資料存取——這些場景的特徵是:讀取操作遠多於寫入,且讀取之間不需要互斥。
用 synchronized 或 Mutex 鎖住所有讀取,讓讀取操作排隊等待,是不必要的效能浪費。
Read-Write Lock 的規則:
- Read lock(Shared):多個 thread 可以同時持有,可以並行讀
- Write lock(Exclusive):只能一個 thread 持有,且持有時沒有其他 thread 可以讀或寫
Java:ReadWriteLock
import java.util.concurrent.locks.*;
class CacheWithRWLock<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public V get(K key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(K key, V value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
}Go:sync.RWMutex
type Config struct {
mu sync.RWMutex
values map[string]string
}
func (c *Config) Get(key string) string {
c.mu.RLock() // 多個 goroutine 可同時讀
defer c.mu.RUnlock()
return c.values[key]
}
func (c *Config) Set(key, value string) {
c.mu.Lock() // 獨占寫,阻塞所有讀和寫
defer c.mu.Unlock()
c.values[key] = value
}資料庫層的 Read-Write Lock
PostgreSQL 和 MySQL 的 MVCC 是 Read-Write Lock 思想的實作——讀操作看到一致的快照,不阻塞寫;寫操作建立新版本,不阻塞讀。
SQL 的 SELECT ... FOR SHARE 和 SELECT ... FOR UPDATE 是顯式的 shared / exclusive lock。
適用場景 vs 注意事項
適合:
- 讀操作遠多於寫操作(讀 > 10:1 寫)
- 資料存取模式偏向讀(Config、Cache、靜態資料表)
注意:
- 寫者飢餓(Writer Starvation):如果讀取持續進來,寫入可能一直等不到機會。Java 的
ReentrantReadWriteLock有 fair mode(new ReentrantReadWriteLock(true))可以解決,但會降低讀取性能 - 升級問題:持有 read lock 不能直接升級成 write lock(會 deadlock),要先釋放 read lock 再取 write lock(期間有 race condition)
- 讀寫比接近 1:1 時,普通 Mutex 可能更好(RWLock 本身有 overhead)