Java多线程进阶(8)—— J.U.C之locks框架:AQS共享功能剖析(4)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

一、本章概述

AQS系列的前三个章节,我们通过ReentrantLock的示例,分析了AQS的独占功能。
本章将以CountDownLatch为例,分析AQS的共享功能。CountDownLatch,是J.U.C中的一个同步器类,可作为倒数计数器使用。

CountDownLatch示例
假设现在有3个线程,ThreadA、ThreadB、mainThread,CountDownLatch初始计数为1:
CountDownLatch switcher = new CountDownLatch(1);

线程的调用时序如下:

    // ThreadA调用await()方法等待
    
    // ThreadB调用await()方法等待
    
    // 主线程main调用countDown()放行

二、AQS共享功能的原理

1. 创建CountDownLatch

CountDownLatch的创建没什么特殊,调用唯一的构造器,传入一个初始计数值,内部实例化一个AQS子类:

CountDownLatch switcher = new CountDownLatch(1);

可以看到,初始计数值count其实就是同步状态值,在CountDownLatch中,同步状态State表示CountDownLatch的计数器的初始大小。

2. ThreadA调用await()方法等待

CountDownLatch的 await 方法是响应中断的,该方法其实是调用了AQS的 acquireSharedInterruptibly 方法:

注意 tryAcquireShared 方法,该方法尝试获取锁,由AQS子类实现,其返回值的含义如下:

State资源的定义
小于0表示获取失败
0表示获取成功
大于0表示获取成功,且后继争用线程可能成功

CountDownLatch中的 tryAcquireShared 实现相当简单,当State值为0时,永远返回成功:

我们之前说了在CountDownLatch中,同步状态State表示CountDownLatch的计数器的初始值,当State==0时,表示无锁状态,且一旦State变为0,就永远处于无锁状态了,此时所有线程在await上等待的线程都可以继续执行。
而在ReentrantLock中,State==0时,虽然也表示无锁状态,但是只有一个线程可以重置State的值。这就是 共享锁 的含义。

好了,继续向下执行,ThreadA尝试获取锁失败后,会调用 doAcquireSharedInterruptibly :

首先通过 addWaiter 方法,将ThreadA包装成共享结点,插入等待队列,插入完成后队列结构如下:

然后会进入自旋操作,先尝试获取一次锁,显然此时是获取失败的(主线程main还未调用countDown,同步状态State还是1)。
然后判断是否要进入阻塞( shouldParkAfterFailedAcquire ):

好了,至此,ThreadA进入阻塞态,最终队列结构如下:

3. ThreadB调用await()方法等待

流程和步骤2完全相同,调用后ThreadB也被加入到等待队列中:

4. 主线程main调用countDown()放行

ThreadA和ThreadB调用了await()方法后都在等待了,现在主线程main开始调用countDown()方法,该方法调用后,ThreadA和ThreadB都会被唤醒,并继续往下执行,达到类似门栓的作用。

来看下 countDown 方法的内部:

该方法内部调用了AQS的 releaseShared 方法,先尝试一次释放锁, tryReleaseShared 方法是一个钩子方法,由 CountDownLatch 实现,当同步State状态值首次变为0时,会返回true:

先调用 compareAndSetWaitStatus 将头结点的等待状态置为0,表示将唤醒后续结点(ThreadA),成功后的等待队列结构如下:

然后调用 unparkSuccessor 唤醒后继结点(ThreadA被唤醒后会从原阻塞处继续往下执行,这个在步骤5再讲):

此时,等待队列结构如下:

5. ThreadA从原阻塞处继续向下执行

ThreadA被唤醒后,会从原来的阻塞处继续向下执行:
由于是一个自旋操作,ThreadA会再次尝试获取锁,由于此时State同步状态值为0(无锁状态),所以获取成功。然后调用 setHeadAndPropagate 方法:

setHeadAndPropagate 方法把ThreadA结点变为头结点,并根据传播状态判断是否要唤醒并释放后继结点:

①将ThreadA变成头结点

②调用 doReleaseShared 方法,释放并唤醒ThreadB结点

6. ThreadB从原阻塞处继续向下执行

ThreadB被唤醒后,从原阻塞处继续向下执行,这个过程和步骤5(ThreadA唤醒后继续执行)完全一样。

setHeadAndPropagate 方法把ThreadB结点变为头结点,并根据传播状态判断是否要唤醒并释放后继结点:

①将ThreadB变成头结点

②调用 doReleaseShared 方法,释放并唤醒后继结点(此时没有后继结点了,则直接break):

最终队列状态如下:

三、总结

AQS的共享功能,通过钩子方法 tryAcquireShared 暴露,与独占功能最主要的区别就是:

共享功能的结点,一旦被唤醒,会向队列后部传播(Propagate)状态,以实现共享结点的连续唤醒。这也是共享的含义,当锁被释放时,所有持有该锁的共享线程都会被唤醒,并从等待队列移除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值