摘要:好了,继续向下执行,尝试获取锁失败后,会调用首先通过方法,将包装成共享结点,插入等待队列,插入完成后队列结构如下然后会进入自旋操作,先尝试获取一次锁,显然此时是获取失败的主线程还未调用,同步状态还是。
本文首发于一世流云的专栏:https://segmentfault.com/blog...一、本章概述
AQS系列的前三个章节,我们通过ReentrantLock的示例,分析了AQS的独占功能。
本章将以CountDownLatch为例,分析AQS的共享功能。CountDownLatch,是J.U.C中的一个同步器类,可作为倒数计数器使用,关于CountDownLatch的使用和说明,读者可以参考:
Java多线程进阶(十八)—— J.U.C之synchronizer框架:CountDownLatch。
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进入阻塞态,最终队列结构如下:
流程和步骤2完全相同,调用后ThreadB也被加入到等待队列中:
ThreadA和ThreadB调用了await()方法后都在等待了,现在主线程main开始调用countDown()方法,该方法调用后,ThreadA和ThreadB都会被唤醒,并继续往下执行,达到类似门栓的作用。
来看下countDown方法的内部:
该方法内部调用了AQS的releaseShared方法,先尝试一次释放锁,tryReleaseShared方法是一个钩子方法,由CountDownLatch实现,当同步State状态值首次变为0时,会返回true:
先调用compareAndSetWaitStatus将头结点的等待状态置为0,表示将唤醒后续结点(ThreadA),成功后的等待队列结构如下:
然后调用unparkSuccessor唤醒后继结点(ThreadA被唤醒后会从原阻塞处继续往下执行,这个在步骤5再讲):
此时,等待队列结构如下:
ThreadA被唤醒后,会从原来的阻塞处继续向下执行:
由于是一个自旋操作,ThreadA会再次尝试获取锁,由于此时State同步状态值为0(无锁状态),所以获取成功。然后调用setHeadAndPropagate方法:
setHeadAndPropagate方法把ThreadA结点变为头结点,并根据传播状态判断是否要唤醒并释放后继结点:
①将ThreadA变成头结点
②调用doReleaseShared方法,释放并唤醒ThreadB结点
ThreadB被唤醒后,从原阻塞处继续向下执行,这个过程和步骤5(ThreadA唤醒后继续执行)完全一样。
setHeadAndPropagate方法把ThreadB结点变为头结点,并根据传播状态判断是否要唤醒并释放后继结点:
①将ThreadB变成头结点
②调用doReleaseShared方法,释放并唤醒后继结点(此时没有后继结点了,则直接break):
最终队列状态如下:
AQS的共享功能,通过钩子方法tryAcquireShared暴露,与独占功能最主要的区别就是:
共享功能的结点,一旦被唤醒,会向队列后部传播(Propagate)状态,以实现共享结点的连续唤醒。这也是共享的含义,当锁被释放时,所有持有该锁的共享线程都会被唤醒,并从等待队列移除。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/76551.html
摘要:开始获取锁终于轮到出场了,的调用过程和完全一样,同样拿不到锁,然后加入到等待队列队尾然后,在阻塞前需要把前驱结点的状态置为,以确保将来可以被唤醒至此,的执行也暂告一段落了安心得在等待队列中睡觉。 showImg(https://segmentfault.com/img/remote/1460000016012467); 本文首发于一世流云的专栏:https://segmentfault...
摘要:线程可以调用的方法进入阻塞,当计数值降到时,所有之前调用阻塞的线程都会释放。注意的初始计数值一旦降到,无法重置。 showImg(https://segmentfault.com/img/remote/1460000016012041); 本文首发于一世流云的专栏:https://segmentfault.com/blog... 一、CountDownLatch简介 CountDow...
摘要:公平策略在多个线程争用锁的情况下,公平策略倾向于将访问权授予等待时间最长的线程。使用方式的典型调用方式如下二类原理的源码非常简单,它通过内部类实现了框架,接口的实现仅仅是对的的简单封装,参见原理多线程进阶七锁框架独占功能剖析 showImg(https://segmentfault.com/img/remote/1460000016012582); 本文首发于一世流云的专栏:https...
摘要:关于,最后有两点规律需要注意当的等待队列队首结点是共享结点,说明当前写锁被占用,当写锁释放时,会以传播的方式唤醒头结点之后紧邻的各个共享结点。当的等待队列队首结点是独占结点,说明当前读锁被使用,当读锁释放归零后,会唤醒队首的独占结点。 showImg(https://segmentfault.com/img/remote/1460000016012293); 本文首发于一世流云的专栏:...
摘要:整个包,按照功能可以大致划分如下锁框架原子类框架同步器框架集合框架执行器框架本系列将按上述顺序分析,分析所基于的源码为。后,根据一系列常见的多线程设计模式,设计了并发包,其中包下提供了一系列基础的锁工具,用以对等进行补充增强。 showImg(https://segmentfault.com/img/remote/1460000016012623); 本文首发于一世流云专栏:https...
阅读 1620·2023-04-26 02:11
阅读 2921·2023-04-25 16:18
阅读 3657·2021-09-06 15:00
阅读 2578·2019-08-30 15:55
阅读 1818·2019-08-30 13:20
阅读 1985·2019-08-26 18:36
阅读 3055·2019-08-26 11:40
阅读 2501·2019-08-26 10:11