源码解析-ReentrantReadWriteLock结合AQS读写锁实现原理

研究一下子JUC重要的 ReadWriteLock 原理

 

老规矩 先上Demo:

 readWriteLock 同时能创造读锁 和写锁两个锁。 

读锁为共享锁,意思是多个线程都可以持有读锁

写锁是独占锁,意思是只能由一个线程持有这把锁

遵循的一个原则:  读锁有人持有了 不能再持有写锁, 写锁如果是自己占的还可以再持有读锁。 

 

 分析了一下当一个线程来抢锁时候的几种情况,简单说就是当读锁被占时 只能再抢读锁,当写锁被占时 只能自己再抢锁(读写都行)

其实到这里明白了就都会用了,over,下面分析一下源码.

 

属性解析:

 五个内部类,和ReentrantLock一样,Sync继承自AQS,是公平和非公平Sync的父类。

维护了一个读锁,一个写锁

 

初始化

 看代码,也就是说 构造器创建了读写锁,共用同一个Sync。

也就是说 读写锁公用同一AQS,同一个state

所以光判断state是否为0 不能判断出占用的是读锁还是写锁

将 state 这个 32 位的 int 值分为高 16 位和低 16位,分别用于共享模式和独占模式。 

共享模式(读锁)下 每占用一个读锁 state+1   独占模式(写锁)下,每重入一次写锁 state+1

 

Sync

 这个Sync也没以前那么单纯了,内部维护了两个内部类

 

 HoldCounter就是用来记录读锁数量,ThreadLocalHoldCounter 顾名思义 就是通过ThreadLocal来存储HoldCounter 记录读锁数量

 

接下来看看 Sync几个漂亮的属性和两个重要的方法

 上面说了 Sync中的state是通过 高16位 低16位来区分

上边四个属性就是 用来处理位运算的

sharedCount()  ,将c 右移16位算出 的是高十六位 共享state次数  

exclusiveCount() , 将c 与 11111111 11111111 做与运算,也就是后16位 作为了独占锁重入次数

 一个方法是算出共享锁占有次数,包括重入次数,一个是算出独占锁重入次数

 

下面开始

上锁解锁源码分析  抢占读锁 - 释放读锁 - 抢占写锁 - 释放写锁

 

抢占读锁

1.lock() 不废话  (获取读锁 写锁没有被其他线程占用, 如果写锁被别的线程占了 那么当前线程就去睡觉直到获取了读锁 也就是说有人获取读锁就会被唤醒)

尝试去获取读锁

如果获取失败  就挂起加入到队列

 

 

2. tryAcquireShared()  尝试获取读锁

  

 1. 如果其他线程占了写锁,失败

2. 否则,这个线程是合格的去占有锁,看看是不是需要阻塞 根据等待队列中的规则。如果不需要 尝试去CAS设置state和count次数。

注意,该步骤不检查可重入获取,延迟到完整版本避免检查保持count更典型的不可重入情况。

3. 如果步骤2失败 或者因为 线程不合格,CAS失败,count饱和,保持当前版本循环重试

英语差 翻译的什么玩意我自己都看不懂.. 下面还是整点阳间的吧..

 

 

Thread current = 当前线程;

int c = 当前state状态;

if (如果独占锁的次数不为0 && 独占锁的线程不是当前线程) {     //说明有别的线程占了写锁

   return -1;        //失败了

}

int r = 共享锁占有次数;

if (当前线程抢读锁不需要阻塞 && r次数在合法范围内 && CAS设置state+1成功){

  if(共享次数是0){   //说明自己是第一个来占读锁的

    设置一下firstReader为当前线程;

    firstReaderHoldCount = 1;

  }else if (firstReader已经是当前线程了 说明是重入的读锁){

    直接firstReaderHoldCount++;

  } else {     //说明已经有别人先占读锁了

  HoldCounter rh = cachedHoldCounter 缓存最后一个获取读锁的线程;         //前面说的HoldCounter 就是个Sync的内部类,用来计数读锁次数

  

  if (rh == null  ||  rh中的线程不是当前线程){   

    当前Sync中的HoldCounter = ThreadLocal中的值;          

  }else if (rh.count == 0){  

    初始化ThreadLocal  将rh保存进去

  }

  rh.count++;

  }  

  return 1;         // return > 0 的数 说明成功获取了读锁

}

return fullTryAcquiredShared(current);   获取读锁失败  再次尝试

 

3.  fullTryAcquiredShared(参数传当前线程)     上一步CAS获取读锁失败了,但是没必要进阻塞队列,印此并没有直接放弃,又来挣扎一次。(如果有写锁也在排队,写锁优先,除非当前读锁是来重入的)

 

 HoldCount = null;

for(;;) 死循环去挣扎着抢读锁  直到写锁被人占了 并且不是当前线程占的写锁

if () 和之前一样,先判断写锁是不是被人占了,占的人是不是别的线程, 如果是就直接return -1

else if (读应该被阻塞){    //说明队列里有其他线程排队等待

  if(当前线程就是第一个获取读锁的线程)    //firstReader 线程重入读锁,直接到下面的 CAS

   // cachedHoldCounter 缓存的不是当前线程

                        // 那么到 ThreadLocal 中获取当前线程的 HoldCounter
                        // 如果当前线程从来没有初始化过 ThreadLocal 中的值,get() 会执行初始化
                        rh = readHolds.get();
                        // 如果发现 count == 0,也就是说,纯属上一行代码初始化的,那么执行 remove
                        // 然后往下两三行,乖乖排队去

}

上小段代码我也不是很理解,总之上面就是用来处理读锁重入的

if (sharedCount(c) == MAX_COUNT)    //直接抛异常   一般不会出现

if (CAS成功){

  //说明获取读锁成功了,

  //和上面的获取成功代码差不多  就是设置一下firstReader  firstReaderHoldCount cachedHoldCounter  ThreadLocal这些东西

  //最后返回1

}

如果又没抢到就再次循环;

 

 

释放读锁

1.  unLock()  

 

 尝试释放读锁

如果释放成功,执行唤醒操作

 

2. tryReleaseShared()  

 Thread current = 当前线程;

if (firstReader == current){

  if (count == 1 ){

    firstReader = null;

  }else{

    count--; //说明是重入锁  释放一次

  }

}else{

  HoldCount rh = 最后一个获取读锁的线程;

  if (最后一个线程为null 或者最后一个线程不是当前线程 ){

    rh 从ThreadLocal中获取;

  }

  清理ThreadLocal缓存

  --count

}

for (;;) {

  自旋

  CAS设置一下state;

  return nextc == 0;    // 如果 nextc == 0,那就是 state 全部 32 位都为 0,也就是读锁和写锁都空了

 // 此时这里返回 true 的话,其实是帮助唤醒后继节点中的获取写锁的线程

}

 

抢占写锁

1. lock()

 

 这个这个和普通的ReentrantLock没太大区别

尝试获取锁,

获取失败了加入到阻塞队列

如果等待中途被人中断,自己重新标记一下中断

 

2. tryAcquire()  尝试去获取写锁

 没什么大的不同  需要判断一下写锁重入,记录一下重入次数

值得注意的 writerShouldBlock()这个方法 在公平,非公平的不同方式

 

3.

公平锁下  读写的判定都是按规矩来  队列里有人等着呢就return true

 

 

非公平锁下就有点说法了 

写锁直接返回false,果断不阻塞直接上手抢,这里暗暗隐含了其实写锁优先级高一些。非公平情况下不管那么多先抢,抢不到了再排队

读锁来判断就得看看第一个等待的节点是不是写锁,如果是写锁主动避让 不和她抢👍

 

 

释放写锁

 1. unlock()  和ReentrantLock没啥区别

 

 尝试释放,

然后唤醒后继节点
over;

 

 

最后回顾一下 中心思想, 自己获取了写锁,还可以继续获取读锁。 自己获取了读锁,不能继续获取写锁.因为会出现死锁 !

 说明了说明了 就是 一个神奇的现象叫 《我睡了我自己》

当一个线程获取完读锁,又想继续获取写锁,经过上面判断后会获取失败 然后 将线程自己加入到阻塞队列睡眠,

就造成了既持有读锁无法释放之后可能又没人唤醒他。

 

 

完结撒花

 

posted @ 2020-11-18 18:21  六小扛把子  阅读(401)  评论(0编辑  收藏  举报