读写锁的基本概念
在多线程程序中,多个线程同时访问共享资源是常态。比如一个配置文件,可能被上百个服务实例频繁读取,但很少修改。如果每次读操作都用互斥锁保护,那性能会大打折扣——就像超市只有一个入口,不管你是进去拿东西还是上厕所,都得排队等前面的人出来。
读写锁(Read-Write Lock)就是为了解决这种场景而生的。它允许多个读操作并发进行,但写操作必须独占资源。也就是说,读和读不互斥,读和写互斥,写和写也互斥。
读写锁的工作模式
读写锁内部通常维护两个状态:读锁计数和写锁持有者。当一个线程请求读锁时,只要没有线程持有写锁,就可以获取成功,并将读锁计数加一。多个线程可以同时持有读锁。
而写锁则完全不同。只有当没有任何线程持有读锁或写锁时,写锁请求才能被满足。一旦某个线程获得了写锁,其他所有读和写请求都会被阻塞,直到写操作完成并释放锁。
举个生活化的例子
想象图书馆里的一本参考书。很多学生可以同时查看这本书的内容(读操作),大家互不影响。但如果有人想修改书里的内容(写操作),就必须等到所有人都看完归还后,才能动手修改。改完之前,其他人既不能看也不能改。这就是读写锁的现实映射。
代码示例:Java 中的读写锁使用
Java 提供了 ReentrantReadWriteLock 类来实现读写锁机制。下面是一个简单的缓存类,使用读写锁保护数据:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Cache {
private final Map<String, Object> map = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public Object get(String key) {
lock.readLock().lock();
try {
return map.get(key);
} finally {
lock.readLock().unlock();
}
}
public void put(String key, Object value) {
lock.writeLock().lock();
try {
map.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}
在这个例子中,get 方法获取读锁,多个线程可以并发调用;而 put 方法获取写锁,确保更新操作的原子性和可见性。
读写锁的潜在问题
虽然读写锁提升了读多写少场景下的并发性能,但也带来了一些隐患。最典型的是“写饥饿”问题:如果读请求持续不断,写线程可能一直得不到执行机会。就像图书馆那本书,总有人在翻看,修改者永远排不上队。
一些实现提供了公平模式,按照请求顺序分配锁,避免长时间等待。但公平性会牺牲一定的吞吐量,需要根据实际业务权衡。
适用场景与替代方案
读写锁最适合“频繁读取、极少修改”的场景,比如配置管理、元数据缓存、状态查询系统等。如果写操作也比较频繁,或者临界区代码很短,直接使用互斥锁反而更高效,因为读写锁本身的开销比普通锁要大。
现代 Java 还提供了 StampedLock,它在读写锁基础上引入了乐观读模式,进一步提升性能。对于超高并发场景,也可以考虑无锁数据结构或 Copy-on-Write 策略,比如 CopyOnWriteArrayList。