問題背景

Config 讀取、Cache 查詢、靜態資料存取——這些場景的特徵是:讀取操作遠多於寫入,且讀取之間不需要互斥

synchronizedMutex 鎖住所有讀取,讓讀取操作排隊等待,是不必要的效能浪費。

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 SHARESELECT ... 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)