欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ReentrantReadWriteLock 源码分析 结合场景

程序员文章站 2022-04-23 14:22:25
...
网上很多大神写了关于AQS和读写锁的源码分析,看了收益良多。但是发现很少有基于读锁和写锁被持有的场景结合源码进行分析的,
所以尝试记录自己的分析结果,如果有人有暇发现了错误,请不吝赐教。
 
1: 读锁请求和释放
readLock.lock()
多个读请求锁,主要是调用ReentrantReadWriteLock的tryAcquireShared()方法
下面仔细分析下这个方法:
 
tryAcquireShared()方法:
  ①:判断是当前否存在独占锁,如果存在独占锁,那么返回-1,进入doAcquireShared方法()
  ②:判断readshouldBlock方法,这个方法判断是否需要当前读阻塞。在公平锁的前提下是看SYN队列是否存在先驱节点;如果是非公平模式则看队列中
        第一个是否为写。(多个读场景不可能阻塞因为 没有写 读是不会进入阻塞队列)
  ③:如果读锁持有的数量小于MAX_COUNT(2^32-1=65535)则,通过CAS操作来比较。如果比较成功则返回成功;如果失败则进入步骤④FullAcqurireShared方法
  ④:FullAcqurireShared() 其实就是重复1,2,3步骤,知道获取到state或者返回-1.这里需要注意的是这里用例ThreadLocal对象readHolds来存储当前线程锁重入的次数。
  如果上述方法返回-1,基本上归结为公平模式下SYN的队列中有先驱,或者非公平模式下有阻塞的写在队列头中(这种情况一般是被之前读锁阻塞的写),则进入doAcquired方法。
 
doAcquireShared()方法:
  ①:把当前线程包装成共享类型的Node放入SYN队列中
  ②:如果当前节点前驱为头结点(既当前节点是队列中的第一个阻塞节点),则再一次调用tryAcquireShared()方法
     (需要说明的,这边这样做的原因可能是在这个读锁请求过程中,已阻塞在队列中的的读或者写已经被释放。)
     如果tryAcquiredShared()成功了,则尝试释放队列中的存在后继节点且前继waitStatus小于0 则调用doReleaseShared()方法
  ③:如果第二步失败,则用LockSupport.park()挂起当前节点,进入阻塞状态
 
doReleaseShared()方法:
   这里需要注意的是前一个节点的waitStatus代表后一个节点的状态,如果是singnal状态 则代表后一个节点所在线程已经被挂起。
所以doReleaseShared()方法中 unpark的是singanal状态的节点。队列中下一个节点的unpark()是由前一个节点先前阻塞的doAcquireShared()
方法锁唤醒。
 
readLock.unlock()
tryReleaseShared()方法:
  ①:更具ThreadLocalmap中的 readHolds对象,并把数量-1
 ②:CAS把高位读state-1.如果修改后的state为0 则返回true;否则返回false
  ③:步骤②返回true 代表需要进行doRelaeaseShared方法。这里这样做的原因是 当前锁不被        任何读锁或者写锁持有,则doReleaseShared()。因为如果队列中有写请求,写锁需要等刀所有读释放掉才能获得,后于这个写锁来的读锁,肯定被加入到了队列中;如果队列中没有写请求,那么队列中肯定也不会有阻塞(之前和这个读一起阻塞的读线程已经被unpark掉) 因为读是共享的
 
2:写锁请求和释放
writeLock .lock()
tryAcquire()方法:
      tryAcquire()方法  RRWL重写了这个方法 
①: 如果state不为0 且写锁数为0,则代表当前有读锁被持有,当前写阻塞;如果state不为0,写锁数w不为0且独占线程=本身,则当前lock请求为锁的重入
②:如果state为0,则表示当前没有写锁或者读锁被持有。判断writerShouldBlock()当前线程是否要阻塞:如果公平模式下,判断是否有前驱节点,非公平模式
下,直接返回false
③:步骤②返回true 则当前线程获得锁;返回false,则用当前线程封装一个独占节点Node. EXCLUSIVE,并加入AQS的队列,进入步骤acquireQueued()
 
acquireQueued()方法:
①: 以当前线程构造独占节点并加入到AQS的队列中
②:如果当前的前驱节点是head节点,则再次调用tryAcquired方法,这边再次判断的原因是可能的读锁或者写锁已经释放,如果tryAcquired失败
      则挂起当前线程
 
writeLock .unlock()
①:tryRelease()方法:
    unLock必须是取得锁的线程,因为是独占的 写锁只有一个 所以判断独占线程是否一致 不一致则抛出异常;一致就把state-1,如果写锁被持有数为0
则把独占线程置成null。
②:unparkSuccessor后继节点
    1)判断head是否为空,既判断AQS队列中是否有元素,如果有head节点不为null且waitStatus不为0,则进入步骤2)
    2)进入unparkSuccessor()方法,这个方法意义和名字一样 释放后续的阻塞线程
         2.1)如果当前节点状态小于0则给他置0,这里当前节点状态代表后继节点线程的状态
         2.2)如果后继节点是null或者waitStatus>0(既为CANCELLED取消状态,则跳过),否则unpark()线程
 这种情况,可能是当前写锁被持有,后续来了N个读锁的请求或者写锁请求,这几个锁请求显然被park()变成阻塞状态,放在了队列中。
unLock的unparkSuccessor,唤醒了队列中的head,依次唤醒队列的后续节点,这里需要注意的是,唤醒的顺序 不一定是获得锁的顺序!
 
 
 
 
 
相关标签: AQS JAVA并发