JUC(三)-ReentrantReadWriteLock源码分析

JUC_3_3_ReentrantReadWriteLock 锁

一、为什么要出现读写锁

  1. ReentrantLock和synchronized都是互斥锁,当出现读多写少的场景时 效率比较低下
    1. 这种情况下可以使用读写锁来实现
    2. 读读之间锁资源是共享的 不存在互斥性 , 读和读操作是可以并发执行的
    3. 但如果涉及到写操作 还得是互斥的操作

二、读写锁的原理

ReentrantReadWriteLock 还是基于 AQS 来实现的 , 还是对 state 进行操作 , 拿到锁资源就去干活 , 拿不到就去AQS队列中排队

  • 只不过 在唤醒读锁的时候 会唤醒 下一个写锁之前的所有的读锁 也即读锁是共享的
    • 假设一下 如果共享锁不需要排队 那么写锁就永远拿不到所资源了
  • 写锁只能去排队 , 等待被唤醒
  • 读写锁也是分为 公平锁和非公平锁的

ReentrantReadWriteLock 依然是可重入锁

  • 读锁操作: 基于state的高16位操作
  • 写锁操作: 基于state的低16位操作

写锁重入

  1. 写锁的重入方式 基本和 ReentrantLock 一直 , 没有什么区别,依然是对state+1操作,只要确认当前持有锁的线程是当前写锁线程即可
  2. ReentrantLock的重入次数 就是 state 的取值范围 , 而读写锁写锁的重入次数只能是 低16位 的最大值

读锁重入

  1. 读锁重入时 会对 state的高16位 进行 +1 操作 (不是直接对state+1 而是通过 位运算将1加到高16位上)
  2. 读锁是个共享锁 , 所以同一时间会有多个读线程持有读锁资源 , 这样就无法确认每个线程的重入次数
    1. 因为这种现象 , 所以每一个读操作线程 都会有一个 ThreadLocal 记录锁重入的次数 , 这样就可以知道每个读线程的重入次数了

写锁的饥饿问题

  1. 读锁过多时 会让请求写锁的线程 一直处于等待状态 , 这样写锁的效率就比较低下
  2. 解决问题(源码级别已经解决了): 持有读锁的线程 , 只会唤醒下一个写锁之前的全部读锁等待节点 , 写锁后边的读锁不会唤醒 , 这样就解决了写锁长时间不能获取到资源问题

三、写锁分析

3.1 写锁加锁的大致流程

image.png

  1. 写线程来竞争写锁资源 , 会直接通过 tryAcquire 方法获取写锁资源 (公平和非公平都是)
  2. 获取 state 的值 拿到 低16位的值(写锁操作低16位,读锁操作高16位)
    1. 如果 state 的值为0 , 则说明当前锁没有任何线程持有当前写锁 (写锁是互斥锁 , 此时state的高16位不可能有值,也即不可能有读锁持有锁资源)
      1. 如果当前锁类型是 公平锁
        1. 看当前线程的节点是不是排在第一位,或者没有排队的节点 如果是 那么就利用CAS尝试获取锁资源 (lock 里边调用的还是 acquire 方法 , 里边就是这个逻辑 - 仅仅是 tryAcquire方法不同)
        2. 反之返回则直接返回false
      2. 如果当前锁类型是 非公平锁
        1. 非公平锁 直接尝试获取锁资源
      3. 公平和非公平的区别 是在 writeShouldBlock 方法体现的
    2. 如果 state 的值不是0
      1. 那么判断是否是锁重入操作 -> 看当前持有锁的线程是否是当前线程 是的话就锁重入
  3. 如果没有拿到锁资源,就去需要去排队了 , 排队的逻辑 和 ReentrantLock 一样

3.2 写锁加锁流程源码分析

0. 读写锁一些常量 和 计算方法
  1. 常量
// 共享锁的(读锁) 偏移量 , 即需要此固定的偏移量 获取到 state 中的高16位
static final int SHARED_SHIFT   = 16;
// 可以理解为读锁高16位的单位1 每次加高16state就加这个值 , 每进来一个读线程持有读锁都会 c + SHARED_UNIT 把state的高16位+1
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
// 可用state表示的 最大的互斥锁重入次数
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
// 可以理解为和上边一样
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
  1. 计算方法
  • exclusiveCount
/**
 * 通过位运算 获取当前 state表示的值的低16位的值 --> 互斥锁重入的数量
 *  
 *  00000000 00000000 00000000 00000001 == 1
 *  00000000 00000001 00000000 00000000 == 1 << 16
 *  00000000 00000000 11111111 11111111 == (1 << 16) - 1
 */
static int exclusiveCount(int c) {
    return c & EXCLUSIVE_MASK; 
}
  • shareCount
/**
 * 通过位运算 获取当前 state表示的值的高16位的值 --> 共享锁的重入次数
 *
 *  00000000 00000000 00000000 00000001 == 1
 *  00000000 00000001 00000000 00000000 == 1 << 16
 *  
 *  00000000 00000000 00000000 00000001 == 1 >> 16
 *  
 */
static int sharedCount(int c) { 
    return c >>> SHARED_SHIFT; 
}

1. 写锁加锁入口
  • lock
public void lock() {
    sync.acquire(1);
}
  • acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • tryAcquire , 只有这一个方法不一样 其他的完全一样
protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取当前 state 属性值
    int c = getState();
    // 获取当前互斥锁的重入次数值 
    int w = exclusiveCount(c);
    // c != 0 表示当前 有线程持有写锁 或者 有线程持有读锁 (即肯定有线程持有锁资源)
    if (c != 0) {
        // w == 0 没线程持有写锁(即当前持有锁的是读锁) , 读写锁互斥 直接返回false
        // w!=0(即持有的是写锁但是-->)当前线程不是持有锁的线程(不是锁重入操作) 那么就返回false 
        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() ||
        // CAS 尝试获取锁资源
        !compareAndSetState(c, c + acquires))
        return false;
    // 拿锁成功 , 设置当前占有互斥锁资源的线程 current 
    setExclusiveOwnerThread(current);
    // 返回true , 则 acquire 方法的 tryAcquire后边的都不用走了
    return true;
}
  • writerShouldBlock , 非公平锁 公平锁和非公平锁是不同的
    • 非公平锁 : 直接返回false
final boolean writerShouldBlock() {
    return false; // writers can always barge
}
  • 公平锁 : 则需要查看当前队列有没有线程在排队 , 如果有排队的那么久看自己是不是head的next
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}
// 这个方法 ReentrantLock细讲了 , 如果有排队节点就返回true , 反之 如果没有排队的节点或者当前线程不是head的next节点就返回false
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

3.3 写锁释放锁的大致流程

  1. 大致流程和ReentrantLock相同
    1. 写锁是独占锁 - 其实就可以把 读写锁的写锁看成 ReentrantLock
    2. 先判断当前持有写锁资源的线程是不是当前线程 ,
      1. 不是的话就抛出异常
      2. 是的话就 将写锁的低16位的state值-1
    3. 然后判断计算后的 state 是不是 0
      1. 如果是0 那么证明锁重入 已经释放干净了
      2. 如果不是0 那么证明锁重入还未是放干净
    4. 释放锁资源干净后 , 就需要从头节点开始 , 唤醒后边符合条件的节点线程
      1. 如果头节点的状态是 SIGNAL(-1) , 那么就需要向后找到 不是中断状态、且距离头节点最近的节点唤醒其线程
        1. 唤醒后锁释放流程也就结束了
        2. 要注意 unparkSuccessor 方法是从后往前找到 离当前头节点最近的节点线程去唤醒的
      2. 如果头节点不是-1 则说明后续节点没有挂起的线程 则直接返回true , 锁释放流程也就结束了

3.4 写锁释放锁源码分析

写锁释放锁资源的流程和ReentrantLock的逻辑相同 , 仅仅是 tryRelease方法获取当前写锁重入次数是使用state的低16位判断的

  • 写锁释放锁的 tryRelease
protected final boolean tryRelease(int releases) {
    // 判断当前持有写锁的线程 是否是当前线程
    if (!isHeldExclusively())
        // 不是则直接抛出异常
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    // 判断state的低16位的值是否是0
    // free == 0 说明锁重入全部释放干净
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        // 将当前持有写锁的线程设置为 null
        setExclusiveOwnerThread(null);
    setState(nextc);
    // 释放干净返回true , 如果没有释放干净,那么就不去唤醒还在排队的Node的线程
    return free;
}

四、读锁分析

4.1 读锁加锁的大致流程

  1. 读锁的加锁 是区分 公平和非公平的
  2. 读操作线程,竞争读锁资源 , 竞争的读锁是个共享锁资源
    1. 拿到state的值
    2. 判断state的低16位的值 是否为0
      1. 如果低16位的值不为0 – 那么表明 当前写锁资源被占用了 , 已知 读锁和写锁是不能同时工作的
        1. 如果有资源占用着写锁资源 , 并且是当前线程持有的写锁 , 那么当前线程想要同时获取读锁 这时是可以拿到读锁资源的 (写锁 --> 读锁 , 锁降级)
        2. 如果不是当前线程持有着写锁资源,那么就去排队吧,结束方法
    3. 获取state的高16位的值
      1. 公平锁:
        1. 如果有人排队 , 那么就直接去排队
      2. 非公平锁:
        1. 之前的非公平锁都是可以直接去尝试抢占锁资源的 , 但是读锁这里不行的 - 会出现 上边的 写锁饥饿问题
        2. 避免上边写锁饥饿问题,非公平锁的做法为: 查看AQS队列中是否已经有写锁线程在排队了
          1. 如果已经有写线程在AQS队列中排队了 , 那么当前 读线程则去排队
          2. 如果没有任何写线程在排队 , 那么则 尝试抢读锁资源
      3. 获取完高16位的值之后 , 就回去执行共享锁的重入操作了(如果此时读锁不被阻塞的前提下-看readShouldBlock方法的实现):
        1. 执行readShouldBlock方法 判断此时 读锁能否执行重入
        2. 如果可以重入 并且重入次数没有达到最大值 那么就执行 CAS 对 state 的高16位+1
        3. 如果可以重入并且 CAS成功了就去判断执行重入 就执行重入操作 return 1 证明拿到锁资源了
          1. 如果有任何一个条件没满足 那么就 使用 fullTryAcquireShared 方法进行后续循环执行 CAS 或者其他操作
  3. 读锁的锁重入操作
    1. 读锁为了记录锁的重入次数 , 需要让每个线程都使用ThreadLocal存储当前读线程持有锁的重入次数
      1. 但是ThreadLocal的使用成本太高 , 所以这里做了一些优化
    2. ReentrantReadWriteLock 锁重入计数 做了优化操作(读锁重入的核心概念)
      1. 读写锁内部 对ThreadLocal做了封装 基于 HoldCounter
        1. 其内部有一个count属性记录次数 。而且每个线程都是操作自己的 ThreadLocalHoldCounter ,
          所以每个线程可以直接对自己的 count 属性进行 count++ 操作 不会出现线程安全问题
      2. (第一个获取读锁的线程)第一个拿到读锁资源的线程是不需要去 使用 ThreadLocal 来管理重入次数的 , 读写锁内部有两个属性 来记录第一个读线程的信息
        1. firstReader 和 firstReaderHoldCount , 这样就可以优化一点代码
          1. firstReader 记录第一个拿到读锁的线程信息
          2. firstReaderHoldCount 记录第一个拿到锁的线程(firstReader) 的重入次数
      3. (最后获取读锁的线程)最后一个拿到读锁资源的线程 cachedHoldCounter 属性会缓存他的重入次数
    3. ReentrantReadWriteLock 读锁的重入流程
      1. (即现在读锁还没有被占用)判断当前线程是否是 第一个拿到读锁资源的线程 , 如果是 则直接 将 firstReader 和 firstReaderHoldCounter 设置为当前线程的信息
      2. (读锁已经被持有了)判断当前是否是 firstReader 线程 , 是的话则执行 firstReaderHoldCount++ 操作
      3. (找中间的或者结尾的读线程)到现在已经和firstReader没关系了
        1. 获取最后一个拿到读锁的线程的hold信息 -> cachedHoldCounter , 判断当前线程是否是 cachedHoldCounter 关联存储的信息
          1. 如果当前线程不是 cachedHoldCounter , 那么就从ThreadLocalHoldCounter中获取当前线程的HoldCounter设置给 cachedHoldCounter
          2. 如果是当前线程 , 则还需要判断当前线程的 重入次数是否为 0
            1. 如果不是0那么就直接操作 当前线程持有的 HoldCounter 的count属性即可
            2. (初始化操作到ThreadLocal中)如果cachedHoldCounter的重入次数是0,那么就需要重新将当前线程的HoldCounter信息设置回 readHolds(ThreadLocal中)
          3. 最后执行对 HoldCounter 的 count 属性 count++

image.png

4.2 读锁加锁的源码分析

4.2.1 acquireShared - 读锁加锁入口
public final void acquireShared(int arg) {
    // 尝试竞争锁资源 , 看到后边可知 tryAcquireShared 返回值 < 0 说明拿锁失败了
    if (tryAcquireShared(arg) < 0)
        // 到这 证明没拿到共享锁资源 那么就去 AQS 中排队
        doAcquireShared(arg);
}
4.2.2 tryAcquireShared
  • tryAcquireShared 读锁尝试获取共享锁
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    // 拿到当前的 state
    int c = getState();
    // 这里就是 写锁的降级操作 - 写锁正在工作 如果当前尝试获取读锁的线程就是持有写锁的线程 那么当前线程就可以获取读锁资源
    // exclusiveCount 先拿到 state 的低16位判断其!=0 , == 0 说明写锁没有工作不进if即可 , 低16位!=0 说明有写线程占用着写锁资源
    if (exclusiveCount(c) != 0 &&
        // 当前写锁正在使用的前提下-判断拿到写锁的线程是不是我自己 , 如果不是 那么返回 -1 拿锁失败 , 该尝试加入AQS中等待了
        getExclusiveOwnerThread() != current)
        // 写锁被其他线程使用中 当前线程无法获取读锁资源 那么就返回-1 去排队
        return -1;
    // 到这 说明 1.写锁没有工作 2.当前线程持有着写锁资源
    // 获取 state 高16位的值
    int r = sharedCount(c);

    /** 
     *  readerShouldBlock (读应该阻塞判断逻辑)
     *      公平锁(没排队的继续执行逻辑):  其实就是ReentrantLock的逻辑 , 看当前AQS队列中有没有线程在排队
     *              如果没有线程排队 或者 当前排队的线程是 head的next节点那么就   返回 false , 在这里是可以进if的
     *              如果有排队的 那么就直接返回 true , 在这里进不去 if 的 
     *      非公平: 
     *              没有排队的直接抢锁资源,
     *              有排队的 那么就看一下 , 当前AQS队列的 head的next节点是不是 互斥锁 (因为非公平锁 - 读锁在工作时 读锁一定会直接拿锁的)
     *                  如果head的next节点是 写锁(互斥锁) 那么就不能再尝试获取锁资源了(避免出现写锁饥饿问题)
     *                  如果head的next节点也不是互斥锁(这种情况一般就是 写锁刚刚释放 后边的读线程还未被全部唤醒) , 那么就尝试拿读锁资源
     */
    // 这里就是判断是否可以执行 读锁的重入操作 , 或者是否拿到锁资源(compareAndSetState CAS 修改state高16位)
    // !readerShouldBlock 需要看上边的 公平和非公平的逻辑处理
    if (!readerShouldBlock() && 
            // 判断当前读锁 state 的值是否达到了临界资源 , 达到的话就不能继续获取读锁了
            r < MAX_COUNT && 
            // 一切正常的话 就对 state 高16位 + 1 操作
            compareAndSetState(c, c + SHARED_UNIT)) {
        // 说明读锁还没有被持有 , 说明当前线程是第一个拿到读锁的线程
        if (r == 0) {
            // 设置 firstReader 为当前线程
            firstReader = current;
            // 重入次数为1
            firstReaderHoldCount = 1;
        // 判断当前线程是否是 firstReader 线程
        } else if (firstReader == current) {
            // 是的话 直接增加其 firstReaderHoldCount
            firstReaderHoldCount++;
        } 
        // 说明不是第一个获取读锁资源的线程 , 跟第一个获取读锁资源的线程没有关系了
        else {
            // 获取最后一个拿到读锁资源的 holdCounter
            HoldCounter rh = cachedHoldCounter;
            /**
             *  rh == null : 说明只有firstReader一个线程拿到了读锁(也即当前是第二个获取读锁的线程) , 所以可以设置 cachedHoldCounter 为当前线程
             *  反之 rh != null --> rh.tid != getThreadId(current) , 说明不是第二个获取读锁的线程 , 那么就把当前线程设置为 最后一个获取到锁资源的线程
             */
            if (rh == null || rh.tid != getThreadId(current))
                // 不是最后一个拿到锁资源的线程 , 那么就设置当前线程为 cachedHoldCounter
                // readHolds.get() 这个ThreadLocal的操作 , ThreadLocalHoldCounter 有默认的 initialValue 方法实现 所以为空时 自动set进去一个新的 HoldCounter对象
                cachedHoldCounter = rh = readHolds.get();
            /**  
             * 到这里 说明当前线程是之前的 cachedHoldCounter 信息存储的线程 (即最后一个获取锁资源的线程)
             * 出现的场景为 : 这个线程unlock释放锁了 (释放锁时 cachedHoldCounter 内的count数值会变为0 , 并且readHolds中存储的当前线程的HoldCounter会被清空)
             * 此时这个线程又回来拿到了读锁资源然 , 上边的 rh==null 通过 , 但是 rh.tid != getThreadId(current) 是当前线程的tid , 这个tid是当前线程的tid 所以这个if进不去了 
             * 经过上边的场景后 线程运行-只能走这个逻辑 , 因为此时的 重入次数已经重置为0了 , 所以设置当前线程的重入信息重新设置到readHolds中(避免下次readHolds 获取当前线程的 HoldCounter对象是空的)
             **/
            else if (rh.count == 0)
                // 将当前的重入信息 重新设置到 ThreadLocal中 , 防止再次拿HoldCounter信息为空
                readHolds.set(rh);
            
            // 重入数量+1
            rh.count++;
        }   
        // 拿锁成功 , 不需要去排队了
        return 1;
    }
    // 无论那种情况的 失败 都会走这个方法 , 和 addWaiter 和 enq 方法的关系, 这个方法是循环做保证操作的
    return fullTryAcquireShared(current);
}
  • 非公平锁的 readerShouldBlock 方法逻辑
/**
 * 这个判断主要是为了 避免出现 写锁饥饿问题
 * 只要有一个条件是 false 那么就返回 false , 上边的 if逻辑就以可以继续走  
 *  
 * 
 * 个人理解 从调用名字看 readShouldBlock --> 读操作应该被阻塞 返回true说明应该被阻塞 返回false说明不应该被阻塞
 *         所以只有当 读操作不被阻塞时 读锁资源 才可以去执行重入操作 , 也即 readShouldBlock 需要返回false 
 *                         也即
 *         readShouldBlock ---> apparentlyFirstQueuedIsExclusive 返回false
 *         公平锁(没排队的继续执行逻辑):  其实就是ReentrantLock的逻辑 , 看当前AQS队列中有没有线程在排队
                如果没有线程排队 或者 当前排队的线程是 head的next节点那么就   返回 false , 在这里是可以进if的
                如果有排队的 那么就直接返回 true , 在这里进不去 if 的 
 *         非公平: 
                没有排队的直接抢锁资源,
                有排队的 那么就看一下 , 当前AQS队列的 head的next节点是不是 互斥锁 (因为非公平锁 - 读锁在工作时 读锁一定会直接拿锁的)
                如果head的next节点是 写锁 那么就不能再尝试获取锁资源了(避免出现写锁饥饿问题)
                如果head的next节点也不是互斥锁(这种情况一般就是 写锁刚刚释放 后边的读线程还未被全部唤醒) , 那么就尝试拿读锁资源
 *         那么满足: 
 *               1. (h = head) != null ==> (h = head)==null 即AQS队列指针未初始化 , 没有线程在排队 , 可以执行重入
 *               2. (s = h.next) != null ==> (s = h.next)==null 即当前AQS队列中只有一个哨兵节点(伪节点) , 依然可以重入
 *               3. !s.isShared() ==> s.isShared() , 即head的next节点如果是共享节点 那么也可以去执行重入操作 , 即如果时写锁那么就不行了
 */
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    
    return (h = head) != null &&    // 如果head为null , 说明AQS中就没有等待的线程 , 就不存在写锁饥饿问题 直接抢锁资源即可 (AQS未初始化过) 
        (s = h.next)  != null &&    // 如果head的next为null , 和上边一样可以抢锁资源 (AQS中没有等待的线程了)
        !s.isShared()         &&    // head的next节点的节点类型 是共享节点 这个方法返回false上边的方法就可以进入if逻辑了 , 那么当前线程可以抢锁资源
        s.thread != null;
}
  • fullTryAcquireShared
/**
 * 在 tryAcquireShared 方法中没有拿到锁资源 , 那么就在这个方法中再次尝试获取 , 和 tryAcquireShared 没太大的区别
 */
final int fullTryAcquireShared(Thread current) {
    // 声明当前线程的 所冲入次数 HoldCounter对象
    HoldCounter rh = null;
    // 死循环 - 所以会一直尝试 , 要么拿不到锁资源 出去阻塞 , 要么就拿到锁资源
    for (;;) {
        // 再次拿到 state
        int c = getState();
        // 判断当前 如果写锁在工作中 , 并且不是当前线程-则返回-1,出去排队 (如果是当前线程拿到了写锁 那么就可以执行写锁降级操作)
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        } 
        // 查看当前是否可以尝试竞争锁资源 (公平锁和非公平锁逻辑不同)
        // 只要 readerShouldBlock 返回了true , 那么这里边执行的逻辑就是要准备让当前线程挂起了
        else if (readerShouldBlock()) {
            // 无论公平还是非公平 , 只要进来 , 就代表要放到AQS队列中了 , 先做一波准备
            // **进入这里之后 - 就是在处理 ThreadLocal的内存泄漏**
            if (firstReader == current) {
                // 如果当前线程是 fistReader 不需要处理 - 因为firstReader没有使用ThreadLocal
            } else {
                // 第一此进来 ch 是 null
                if (rh == null) {
                    // 拿到最后一个获取读锁资源的 线程的 HoldCounter 信息
                    rh = cachedHoldCounter;
                    // 如果没有最后一个线程 或者当前线程不是 cachedHoldCounter 
                    if (rh == null || rh.tid != getThreadId(current)) {
                        // 从ThreadLocal中拿到自己的 HoldCounter 计数器
                        rh = readHolds.get();
                        // 如果我的计数器 count==0 , 那么说明当前线程之前就没有拿过锁资源 , 那么就清空其TheadLocal存储的数据
                        if (rh.count == 0)
                            // 避免内存泄漏
                            readHolds.remove();
                    }
                }
                // 前边处理完之后 直接返回 -1 准备加入到AQS队列中
                if (rh.count == 0)
                    return -1;
            }
        }
        
        // 判断重入次数是否超出阈值
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // CAS 尝试获取锁资源 
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 还是之前 tryAcquireShared 的锁重入操作
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            // 拿到锁资源 返回 1 , 不需要进入AQS排队了
            return 1;
        }
    }
}
4.2.2.5 读锁在AQS队列获取到锁资源时的后续操作
  1. 正常如果都是 读线程来获取读锁资源 , 那么是不需要用到队列操作的 , 都是执行CAS拿锁即可(失败也有for循环保证)
  2. 如果有写线程持有着写锁 , 这时读线程 就需要进入AQS中排队了 , 读线程可能会有多个再AQS中排队 ,
    当写锁释放后 , 会唤醒head后边的读线程 , 当head后面的线程拿到所资源之后 , 还需要查看他的next节点
    是否也是正在阻塞的 读线程 , 如果是则需要一并唤醒 (setHeadAndPropagate 方法逻辑) 这样就会形成 一个读线程唤醒了那么后边下个写线程之前的所有读线程都会被唤醒
4.2.3 doAcquireShared
  • doAcquireShared , tryAcquireShared 没有拿到锁资源那么就走这个方法 加入到AQS队列中等待了
private void doAcquireShared(int arg) {
    // 声明共享Node 并且加入到AQS中排队
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 还是拿到上一个节点
            final Node p = node.predecessor();
            // 上一个节点是 head 则可以尝试获取锁资源
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 拿到读锁资源之后 , 需要做的后续处理
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 找到上一个有效节点 将状态设置为-1 , 然后挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
setHeadAndPropagate (读锁唤醒的核心方法)
private void setHeadAndPropagate(Node node, int propagate) {
    // 拿到 head 节点
    Node h = head;
    // 将当前 node节点 设置为新的 (head节点)伪节点
    setHead(node);
    /**
     *  [**拿到锁资源 肯定走下边if里边的逻辑**] 
     *  
     *  1. propagate > 0 这个判断更多的是在信号量处理 JDK1.5出现的BUG的操作 
     *     读写锁这块不需要管这个判断 因为读写锁到这里 propagate只能是1
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        // 拿到当前node节点的 next 节点
        Node s = node.next;
        // 现在是node拿到了锁资源 如果其next节点是共享锁 , 那么直接唤醒后续节点 , 这里就是读锁是共享锁特殊所在 一个读线程唤醒了 下个写线程之前的读线程全部唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

4.3 读锁释放锁的源码分析

  1. 处理重入问题 - 处理 当前线程的 HoldCounter 属性
    1. 如果是 firstReader 则不需要操作 ThreadLocal相关即可
    2. 如果不是firstReader , 那么就需要从 ThreadLocal 或 cachedHoldCounter 中获取计数器
      1. 是 cachedHoldCounter 直接获取其计数器 进行操作接口
      2. 不是上边的 那么就从ThreadLocal中获取到当前线程的计数器
  2. 处理 state 属性的值
    1. 释放干净就走后边 处理AQS队列中的节点操作了
      1. 判断头节点是否为 -1 为-1则需要唤醒后续节点线程
4.3.1 releaseShared
public final boolean releaseShared(int arg) {
    // tryReleaseShared 处理可重入的内容 和 设置 state的值
    if (tryReleaseShared(arg)) {
        // 处理AQS队列
        doReleaseShared();
        return true;
    }
    return false;
}
4.3.2 tryReleaseShared
// 处理重入次数 和 设置state的方法
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 如果当前释放锁的线程是 firstReader , 不需要ThreadLocal相关操作
    if (firstReader == current) {
        // firstReader 只持有锁 没有重入操作
        if (firstReaderHoldCount == 1)
            // 设置firstReder为空
            firstReader = null;
        else
            // 否则本次释放锁就是 减少重入次数
            firstReaderHoldCount--;
    } 
    // 不是 firstReader , 从 cachedHoldCounter 以及 ThreadLocal获取计数器
    else {
        // 如果是 cachedHoldCounter 直接 count--
        HoldCounter rh = cachedHoldCounter;
        // 如果不是 cachedHoldCounter 即不是最后一个获取锁资源的线程
        if (rh == null || rh.tid != getThreadId(current))
            // 那么就从 ThreadLocalHoldCounter中获取当前线程的 HoldCounter计数器
            rh = readHolds.get();
        // 当前线程的 计数器指
        int count = rh.count;
        // 如果count为1或者更小 说明已经释放干净了 直接remove掉计数器即可 避免内存泄漏
        if (count <= 1) {
            readHolds.remove();
            // count已经是0了 说明释放次数多了 , 需要抛出异常了
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    
    // 处理 state 属性
    for (;;) {
        int c = getState();
        // 获取当前state高16位的值
        int nextc = c - SHARED_UNIT;
        // CAS 替换当前state值
        if (compareAndSetState(c, nextc))
            // 如果当前state值已经为0了 说明当前锁重入已经释放干净了 上边方法就可以执行唤醒后续节点了
            // 如果没有释放干净则不能唤醒后续节点线程
            return nextc == 0;
    }
    }
4.3.3 doReleaseShared
// 唤醒 AQS 中排队的线程
private void doReleaseShared() {
    for (;;) {
        // 拿到头节点
        Node h = head;
        // 说明有排队的节点
        if (h != null && h != tail) {
            // 头节点的状态
            int ws = h.waitStatus;
            // 头节点状态为 -1 , 说明后边有挂起的线程
            if (ws == Node.SIGNAL) {
                // 读锁可能会并发unlock , 所以基于CAS将head的状态从 -1 --> 0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue; 
                // 唤醒后续节点 , ReentrantLock 讲过
                unparkSuccessor(h);
            }
            // 说明后续节点没有 挂起的线程
            // 这里不是给读写锁准备的 , PROPAGATE 是给信号量(Semaphore)做的
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                
        }
        // 出口 -- 没有排队的直接结束返回
        if (h == head)                   
            break;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值