写点什么

大神一招搞定:ReentrantReadWriteLock- 几道小小数学题就够了。

用户头像
Android架构
关注
发布于: 刚刚

效率提升是一方面,但并发编程更重要的是在保证准确性的前提下提高效率


一个写线程改变了缓存中的值,其他读线程一定是可以 “感知” 到的,否则可能导致查询到的值不准确


所以关于读写锁模型就了下面这 3 条规定:


  1. 允许多个线程同时读共享变量

  2. 只允许一个线程写共享变量

  3. 如果写线程正在执行写操作,此时则禁止其他读线程读共享变量


ReadWriteLock 是一个接口,其内部只有两个方法:


public interface ReadWriteLock {// 返回用于读的锁 Lock readLock();


// 返回用于写的锁 Lock writeLock();}


所以要了解整个读/写锁的整个应用过程,需要从它的实现类 ReentrantReadWriteLock 说起

ReentrantReadWriteLock 类结构

直接对比 ReentrantReadWriteLock 与 ReentrantLock 的类结构



他们又很相似吧,根据类名称以及类结构,按照咱们前序文章的分析,你也就能看出 ReentrantReadWriteLock 的基本特性:



其中黄颜色标记的的 锁降级 是看不出来的, 这里先有个印象,下面会单独说明


另外,不知道你是否还记得,Java AQS 队列同步器以及 ReentrantLock 的应用 说过,Lock 和 AQS 同步器是一种组合形式的存在,既然这里是读/写两种锁,他们的组合模式也就分成了两种:


  1. 读锁与自定义同步器的聚合

  2. 写锁与自定义同步器的聚合


public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();readerLock = new ReadLock(this);writerLock = new WriteLock(this);}复制代码



这里只是提醒大家,模式没有变,不要被读/写两种锁迷惑

基本示例

说了这么多,如果你忘了前序知识,整体理解感觉应该是有断档的,所以先来看个示例(模拟使用缓存)让大家对 ReentrantReadWriteLock 有个直观的使用印象


public class ReentrantReadWriteLockCache {


// 定义一个非线程安全的 HashMap 用于缓存对象 static Map<String, Object> map = new HashMap<String, Object>();// 创建读写锁对象 static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 构建读锁 static Lock rl = readWriteLock.readLock();// 构建写锁 static Lock wl = readWriteLock.writeLock();


public static final Object get(String key) {rl.lock();try{return map.get(key);}finally {rl.unlock()


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


;}}


public static final Object put(String key, Object value){wl.lock();try{return map.put(key, value);}finally {wl.unlock();}}}


你瞧,使用就是这么简单。但是你知道的,AQS 的核心是锁的实现,即控制同步状态 state 的值,ReentrantReadWriteLock 也是应用 AQS 的 state 来控制同步状态的,那么问题来了:


一个 int 类型的 state 怎么既控制读的同步状态,又可以控制写的同步状态呢?


显然需要一点设计了

读写状态设计

如果要在一个 int 类型变量上维护多个状态,那肯定就需要拆分了。我们知道 int 类型数据占 32 位,所以我们就有机会按位切割使用 state 了。我们将其切割成两部分:


  1. 高 16 位表示读

  2. 低 16 位表示写



所以,要想准确的计算读/写各自的状态值,肯定就要应用位运算了,下面代码是 JDK1.8,ReentrantReadWriteLock 自定义同步器 Sync 的位操作


abstract static class Sync extends AbstractQueuedSynchronizer {


static final int SHARED_SHIFT = 16;static final int SHARED_UNIT = (1 << SHARED_SHIFT);static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;


static int sharedCount(int c) {return c >>> SHARED_SHIFT;}


static int exclusiveCount(int c) {return c & EXCLUSIVE_MASK;}}


乍一看真是有些复杂的可怕,别慌,咱们通过几道小小数学题就可以搞定整个位运算过程



整个 ReentrantReadWriteLock 中 读/写状态的计算就是反复应用这几道数学题,所以,在阅读下面内容之前,希望你搞懂这简单的运算


基础铺垫足够了,我们进入源码分析吧

源码分析

写锁分析

由于写锁是排他的,所以肯定是要重写 AQS 中 tryAcquire 方法


protected final boolean tryAcquire(int acquires) {


Thread current = Thread.currentThread();// 获取 state 整体的值 int c = getState();// 获取写状态的值 int w = exclusiveCount(c);if (c != 0) {// w=0: 根据推理二,整体状态不等于零,写状态等于零,所以,读状态大于 0,即存在读锁// 或者当前线程不是已获取写锁的线程// 二者之一条件成真,则获取写状态失败 if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// 根据推理一第 1 条,更新写状态值 setState(c + acquires);return true;}if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;}复制代码


上述代码 第 19 行 writerShouldBlock 也并没有什么神秘的,只不过是公平/非公平获取锁方式的判断(是否有前驱节点来判断)



你瞧,解锁获取方式就是这么简单

读锁分析

由于读锁是共享式的,所以肯定是要重写 AQS 中 tryAcquireShared 方法


protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread();int c = getState();// 写状态不等于 0,并且锁的持有者不是当前线程,根据约定 3,则获取读锁失败 if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;// 获取读状态值 int r = sharedCount(c);// 这个地方有点不一样,我们单独说明 if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {if (r == 0) {firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}// 如果获取读锁失败则进入自旋获取 return fullTryAcquireShared(current);}复制代码


readerShouldBlock 和 writerShouldBlock 在公平锁的实现上都是判断是否有前驱节点,但是在非公平锁的实现上,前者是这样的:


final boolean readerShouldBlock() {return apparentlyFirstQueuedIsExclusive();}


final boolean apparentlyFirstQueuedIsExclusive() {Node h, s;return (h = head) != null &&// 等待队列头节点的下一个节点(s = h.next) != null &&// 如果是排他式的节点!s.isShared() &&s.thread != null;}


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


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



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

读写锁的升级与降级

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



在真正了解读写锁的升级与降级之前,我们需要完善一下本文开头 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) {// 查询 DBobj = getDataFromDB(key); // 伪代码:getDataFromDB// 将其放入到缓存中 map.put(key, obj);}}finally {wl.unlock();}return obj;}复制代码


有童鞋可能会有疑问


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


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

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
大神一招搞定:ReentrantReadWriteLock-几道小小数学题就够了。