搞定ReentrantReadWriteLock 几道小小数学题就够了 (3)

简单来说,如果请求读锁的当前线程发现同步队列的 head 节点的下一个节点为排他式节点,那么就说明有一个线程在等待获取写锁(争抢写锁失败,被放入到同步队列中),那么请求读锁的线程就要阻塞,毕竟读多写少,如果还没有这点判断机制,写锁可能会发生【饥饿】

上述条件都满足了,也就会进入 tryAcquireShared 代码的第 14 行到第 25 行,这段代码主要是为了记录线程持有锁的次数。读锁是共享式的,还想记录每个线程持有读锁的次数,就要用到 ThreadLocal 了,因为这不影响同步状态 state 的值,所以就不分析了, 只把关系放在这吧

搞定ReentrantReadWriteLock 几道小小数学题就够了

到这里读锁的获取也就结束了,比写锁稍稍复杂那么一丢丢,接下来就说明一下那个可能让你迷惑的锁升级/降级问题吧

读写锁的升级与降级

个人理解:读锁是可以被多线程共享的,写锁是单线程独占的,也就是说写锁的并发限制比读锁高,所以

搞定ReentrantReadWriteLock 几道小小数学题就够了

在真正了解读写锁的升级与降级之前,我们需要完善一下本文开头 ReentrantReadWriteLock 的例子

public static final Object get(String key) { Object obj = null; rl.lock(); try{ // 获取缓存中的值 obj = map.get(key); }finally { rl.unlock(); } // 缓存中值不为空,直接返回 if (obj!= null) { return obj; } // 缓存中值为空,则通过写锁查询DB,并将其写入到缓存中 wl.lock(); try{ // 再次尝试获取缓存中的值 obj = map.get(key); // 再次获取缓存中值还是为空 if (obj == null) { // 查询DB obj = getDataFromDB(key); // 伪代码:getDataFromDB // 将其放入到缓存中 map.put(key, obj); } }finally { wl.unlock(); } return obj; }

有童鞋可能会有疑问

在写锁里面,为什么代码第19行还要再次获取缓存中的值呢?不是多此一举吗?

其实这里再次尝试获取缓存中的值是很有必要的,因为可能存在多个线程同时执行 get 方法,并且参数 key 也是相同的,执行到代码第 16 行 wl.lock() ,比如这样:

搞定ReentrantReadWriteLock 几道小小数学题就够了

线程 A,B,C 同时执行到临界区 wl.lock(), 只有线程 A 获取写锁成功,线程B,C只能阻塞,直到线程A 释放写锁。这时,当线程B 或者 C 再次进入临界区时,线程 A 已经将值更新到缓存中了,所以线程B,C没必要再查询一次DB,而是再次尝试查询缓存中的值

既然再次获取缓存很有必要,我能否在读锁里直接判断,如果缓存中没有值,那就再次获取写锁来查询DB不就可以了嘛,就像这样:

public static final Object getLockUpgrade(String key) { Object obj = null; rl.lock(); try{ obj = map.get(key); if (obj == null){ wl.lock(); try{ obj = map.get(key); if (obj == null) { obj = getDataFromDB(key); // 伪代码:getDataFromDB map.put(key, obj); } }finally { wl.unlock(); } } }finally { rl.unlock(); } return obj; }

这还真是不可以的,因为获取一个写入锁需要先释放所有的读取锁,如果有两个读取锁试图获取写入锁,且都不释放读取锁时,就会发生死锁,所以在这里,锁的升级是不被允许的

读写锁的升级是不可以的,那么锁的降级是可以的嘛?这个是 Oracle 官网关于锁降级的示例 ,我将代码粘贴在此处,大家有兴趣可以点进去连接看更多内容

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 = ... cacheValid = true; } //在释放写锁前,降级为读锁 rwl.readLock().lock(); } finally { //释放写锁,此时持有读锁 rwl.writeLock().unlock(); } } try { use(data); } finally { rwl.readLock().unlock(); } } }

代码中声明了一个 volatile 类型的 cacheValid 变量,保证其可见性。

首先获取读锁,如果cache不可用,则释放读锁

然后获取写锁

在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true

然后在释放写锁前获取读锁 此时

cache中数据可用,处理cache中数据,最后释放读锁

这个过程就是一个完整的锁降级的过程,目的是保证数据可见性,听起来很有道理的样子,那么问题来了:

上述代码为什么在释放写锁之前要获取读锁呢?

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppfdf.html