读写锁的出现原因
ReentrantLock
实现一种标准的互斥锁,每次最多只有一个线程能持有ReentrantLock
,限制了并发性,互斥是一种保守的加锁策略,虽然避免了“写/写”冲突和“写/读”冲突,但也避免了“读/读”冲突。
而大部分情况下读操作比较多,如果此时能够放宽加锁需求,允许多个读操作的线程同时访问数据结构,可以提升程序的性能(只要每个线程保证读取到最新的数据,并且在读取数据时不会有其他线程修改数据就行)
读锁的作用
任何锁表面上是互斥,但本质是都是为了避免原子性问题(如果程序没有原子性问题,那只用volatile来避免可见性和有序性问题就可以了,效率更高),读锁自然也是为了避免原子性问题。
比如一个long型参数的写操作并不是原子性的,如果允许同时读和写,那读到的数很可能是就是写操作的中间状态,比如刚写完前32位的中间状态。long型数都如此,而实际上一般读的都是复杂的对象,那中间状态的情况就更多了。
所以读锁是防止读到写的中间值。
读写锁的特点
-
如果有一个线程已经占用了
读锁
,则此时其他线程如果要申请读锁
,可以申请成功。 -
如果有一个线程已经占用了
读锁
,则此时其他线程如果要申请写锁
,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。 -
如果有一个线程已经占用了
写锁
,则此时其他线程如果申请写锁
或者读锁
,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。 -
锁降级 不可升级 从写锁降级到读锁不可升级(获取写锁->获取读锁->释放写锁)
总结
-
读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
-
加读锁是防止读取到中间值
示例代码
ReentrantReadWriteLock
源码上提供了两段demo代码
- 下面代码展示了如何在更新缓存后执行锁降级
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 获取写锁前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 双重校验 其他线程可能会获取到读锁并修改值
if (!cacheValid) {
// data = ... // 这里可以修改data值,一般是db层操作
cacheValid = true;
}
// 释放写锁前获取读锁 实现锁降级
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // 释放写锁,仍持有读锁
}
}
try {
// use(data); // 使用数据 最后释放读锁
} finally {
rwl.readLock().unlock();
}
}
}
- 可用于在某些集合的某些用途中提高并发性,特别适合读多写少的情景。以
TreeMap
为例
class RWDictionary {
private final Map<String, Object> m = new TreeMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Object get(String key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
public String[] allKeys() {
r.lock();
try {
return m.keySet().toArray(new String[]{});
} finally {
r.unlock();
}
}
public Object put(String key, Object value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
public void clear() {
w.lock();
try {
m.clear();
} finally {
w.unlock();
}
}
}