AbstractQueuedSynchronizer(AQS) 超详细原理解析
java.util.concurrent
包中很多类都依赖于这个类AbstractQueuedSynchronizer所提供的队列式的同步器,比如说常用的ReentranLock
,Semaphore
和CountDownLatch
等. 为了方便理解,我们以一段使用ReentranLock
的代码为例,讲解ReentranLock
每个方法中有关AQS
的使用.
ReentranLock示例
我们都知道ReentranLock
的加锁行为和Synchronized
类似,都是可重入的锁,但是二者的实现方式确实完全不同的,我们之后也会讲解Synchronized
的原理.除此之外,Synchronized的阻塞无法被中断,而ReentrantLock则提供了可中断的阻塞下面的代码是ReentranLock
的相关API,我们就以此为顺序,依次讲解.
公平锁和非公平锁
ReentranLock分为公平锁和非公平锁.二者的区别就在获取锁是否和排队顺序相关.我们都知道,如果当前锁被另一个线程持有,那么当前申请锁的线程会被挂起等待,然后加入一个等待队列里.理论上,先调用lock
函数被挂起等待的线程应该排在等待队列的前端,后调用的就排在后边.如果此时,锁被释放,需要通知等待线程再次尝试获取锁,公平锁会让最先进入队列的线程获得锁,而非公平锁则会唤醒所有线程,让它们再次尝试获取锁,所以可能会导致后来的线程先获得了锁,则就是非公平.
- public ReentrantLock(boolean fair) {
- sync = fair ? new FairSync() : new NonfairSync();
我们会发现FairSync
和NonfairSync
都继承了Sync
类,而Sync
的父类就是AbstractQueuedSynchronizer
.但是AQS
的构造函数是空的,并没有任何操作. 之后的源码分析,如果没有特别说明,就是指公平锁.
lock操作
ReentranLock
的lock
函数如下所示,直接调用了sync
的lock
函数.也就是调用了FairSync
的lock
函数.
- //ReentranLock
- public void lock() {
- sync.lock();
- }
- //FairSync
- final void lock() {
- acquire(1);//调用了AQS的acquire函数,这是关键函数之一
好,我们接下来就正式开始AQS
相关的源码分析了,acquire
函数你可以将其理解为获取一个同一时间只能有一个函数获取的量,这个量就是锁概念的抽象化.我们先分析代码,你慢慢就会明白其中的含义.
- public final void acquire(int arg) {
- if (!tryAcquire(arg) && //tryAcquire先尝试获取"锁",//如果成功,直接返回,失败继续执行后续代码
- //addWaiter是给当前线程创建一个节点,并将其加入等待队列
- //acquireQueued是当线程已经加入等待队列之后的行为.
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- selfInterrupt();
}
tryAcquire
,addWaiter
和acquireQueued
都是十分重要的函数,我们接下来依次学习一下这些函数,理解它们的作用.
- //AQS类中的变量.
- private volatile int state;
- //这是FairSync的实现,AQS中未实现,子类按照自己的需要实现该类
- protected final boolean tryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- //获取AQS中的state变量,代表抽象概念的锁.
- int c = getState();
- if (c == 0) { //值为0,那么当前独占性变量还未被线程占有
- if (!hasQueuedPredecessors() && //如果当前阻塞队列上没有先来的线程在等待,UnfairSync这里的实现就不一致
- compareAndSetState(0, acquires)) {
- //成功cas,那么代表当前线程获得该变量的所有权,也就是说成功获得锁
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- else if (current == getExclusiveOwnerThread()) {
- //如果该线程已经获取了独占性变量的所有权,那么根据重入性
- //原理,将state值进行加1,表示多次lock
- //由于已经获得锁,该段代码只会被一个线程同时执行,所以不需要
- //进行任何并行处理
- int nextc = c + acquires;
- if (nextc < 0)
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- //上述情况都不符合,说明获取锁失败
- return false;
- }
由上述代码我们可以发现,tryAcquire
就是尝试获取那个线程独占的变量state
.state的值表示其状态:如果是0,那么当前还没有线程独占此变量;否在就是已经有线程独占了这个变量,也就是代表已经有线程获得了锁.但是这个时候要再进行一次判断,看是否是当前线程自己获得的这个锁,如果是,那么就增加state的值.
ReentranLock获得锁
这里有几点需要说明一下,首先是compareAndSetState
函数,这是使用CAS操作来设置state
的值,而且state值设置了volatile
修饰符,通过这两点来确保修改state的值不会出现多线程问题.然后是公平锁和非公平锁的区别问题,在UnfairSync
的nonfairTryAcquire
函数中不会在相同的位置上调用hasQueuedPredecessors
来判断当前是否已经有线程在排队等待获得锁. 如果tryAcquire
返回true
,那么就是获取锁成功,如果返回false,那么就是未获得锁.需要加入阻塞等待队列.我们下面就来看一下addWaiter
的相关操作
等待锁的阻塞队列
将保存当前线程信息的节点加入到等待队列的相关函数中涉及到了无锁队列的相关算法,由于在AQS
中只是将节点添加到队尾,使用到的无锁算法也相对简单.真正的无锁队列的算法我们等到分析ConcurrentSkippedListMap
时在进行讲解.
- private Node addWaiter(Node mode) {
- Node node = new Node(Thread.currentThread(), mode);
- //先使用快速如列法来尝试一下,如果失败,则进行更加完备的如列算法.
- Node pred = tail;//列尾指针
- if (pred != null) {
- node.prev = pred; //步骤1:该节点的前趋指针指向tail
- if (compareAndSetTail(pred, node)){ //步骤二:cas将尾指针指向该节点
- pred.next = node;//步骤三:如果成果,让旧列尾节点的next指针指向该节点.
- return node;
- }
- }
- //cas失败,或在pred == null时调用enq
- enq(node);
- return node;
- }
- private Node enq(final Node node) {
- for (;;) { //cas无锁算法的标准for循环,不停的尝试
- Node t = tail;
- if (t == null) { //初始化
- if (compareAndSetHead(new Node())) //需要注意的是head是一个哨兵的作用,并不代表某个要获取锁的线程节点
- tail = head;
- } else {
- //和addWaiter中一致,不过有了外侧的无限循环,不停的尝试,自旋锁
- node.prev = t;
- if (compareAndSetTail(t, node)) {
- t.next = node;
- return t;
- }
- }
- }
- }
通过调用addWaiter
函数,AQS
将当前线程加入到了等待队列,但是还没有阻塞当前线程的执行,接下来我们就来分析一下acquireQueued
函数.
等待队列节点的操作
由于进入阻塞状态的操作会降低执行效率,所以,AQS
会尽力避免试图获取独占性变量的线程进入阻塞状态.所以,当线程加入等待队列之后,acquireQueued
会执行一个for循环,每次都判断当前节点是否应该获得这个变量(在队首了),如果不应该获取或在再次尝试获取失败,那么就调用shouldParkAfterFailedAcquire
判断是否应该进入阻塞状态,如果当前节点之前的节点已经进入阻塞状态了,那么就可以判定当前节点不可能获取到锁,为了防止CPU不停的执行for循环,消耗CPU资源,调用parkAndCheckInterrupt
函数来进入阻塞状态.
- final boolean acquireQueued(final Node node, int arg) {
- boolean failed = true;
- try {
- boolean interrupted = false;
- for (;;) { //一直执行,直到获取锁,返回.
- final Node p = node.predecessor();
- //node的前驱是head,就说明,node是将要获取锁的下一个节点.
- if (p == head && tryAcquire(arg)) { //所以再次尝试获取独占性变量
- setHead(node); //如果成果,那么就将自己设置为head
- p.next = null; // help GC
- failed = false;
- return interrupted;//此时,还没有进入阻塞状态,所以直接返回false,表示不需要中断
- }
- //判断是否要进入阻塞状态.如果`shouldParkAfterFailedAcquire`返回true,表示需要进入阻塞
- //调用parkAndCheckInterrupt,否在表示还可以再次尝试获取锁,继续进行for循环
- if (shouldParkAfterFailedAcquire(p, node) &&
- parkAndCheckInterrupt())
- //调用parkAndCheckInterrupt进行阻塞,然后返回是否为中断状态
- interrupted = true;
- }
- } finally {
- if (failed)
- cancelAcquire(node);
- }
- }
- private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
- int ws = pred.waitStatus;
- if (ws == Node.SIGNAL) //前一个节点在等待独占性变量释放的通知,所以,当前节点可以阻塞
- return true;
- if (ws > 0) { //前一个节点处于取消获取独占性变量的状态,所以,可以跳过去
- //返回false
- do {
- node.prev = pred = pred.prev;
- } while (pred.waitStatus > 0);
- pred.next = node;
- } else {
- //将上一个节点的状态设置为signal,返回false,
- compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
- }
- return false;
- }
- private final boolean parkAndCheckInterrupt() {
- LockSupport.park(this); //将AQS对象自己传入
- return Thread.interrupted();
- }
阻塞和中断
由上述分析,我们知道了AQS
通过调用LockSupport
的park
方法来执行阻塞当前进程的操作.其实,这里的阻塞就是线程不再执行的含义.通过调用这个函数,线程进入阻塞状态,上述的lock
操作也就阻塞了.等待中断或在独占性变量被释放.
- public static void park(Object blocker) {
- Thread t = Thread.currentThread();
- setBlocker(t, blocker);//设置阻塞对象,用来记录线程被谁阻塞的,用于线程监控和分析工具来定位
- UNSAFE.park(false, 0L);//让当前线程不再被线程调度,就是当前线程不再执行.
- setBlocker(t, null);
- }
关于中断的相关知识,我们以后再说,就继续沿着AQS
的主线,看一下释放独占性变量的相关操作吧ReentrantLock未获得阻塞,加入队列
unlock操作
与lock
操作类似,unlock
操作调用了AQS
的relase
方法,参数和调用acquire
时一样,都是1.
- public final boolean release(int arg) {
- if (tryRelease(arg)) { //释放独占性变量,起始就是将status的值减1,因为acquire时是加1
- Node h = head;
- if (h != null && h.waitStatus != 0)
- unparkSuccessor(h);//唤醒head的后继节点
- return true;
- }
- return false;
- }
由上述代码可知,release就是先调用tryRelease
来释放独占性变量,如果成功,那么就看一下是否有等待锁的阻塞线程,如果有,就调用unparkSuccessor
来唤醒他们.
- protected final boolean tryRelease(int releases) {
- //由于只有一个线程可以获得独占先变量,所以,所有操作不需要考虑多线程
- int c = getState() - releases;
- if (Thread.currentThread() != getExclusiveOwnerThread())
- throw new IllegalMonitorStateException();
- boolean free = false;
- if (c == 0) { //如果等于0,那么说明锁应该被释放了,否在表示当前线程有多次lock操作.
- free = true;
- setExclusiveOwnerThread(null);
- }
- setState(c);
- return free;
- }
我们可以看到tryRelease
中的逻辑也体现了可重入锁的概念,只有等到state
的值为1时,才代表锁真正被释放了.所以独占性变量state
的值就代表锁的有无.当state=0
时,表示锁未被占有,否在表示当前锁已经被占有.
- private void unparkSuccessor(Node node) {
- .....
- //一般来说,需要唤醒的线程就是head的下一个节点,但是如果它获取锁的操作被取消,或在节点为null时
- //就直接继续往后遍历,找到第一个未取消的后继节点.
- Node s = node.next;
- if (s == null || s.waitStatus > 0) {
- s = null;
- for (Node t = tail; t != null && t != node; t = t.prev)
- if (t.waitStatus <= 0)
- s = t;
- }
- if (s != null)
- LockSupport.unpark(s.thread);
- }
调用了unpark
方法后,进行lock
操作被阻塞的线程就恢复到运行状态,就会再次执行acquireQueued
中的无限for循环中的操作,再次尝试获取锁.
ReentrantLock释放锁并通知阻塞线程恢复执行
原文链接:https://blog.csdn.net/u010325193/article/details/84485908
AbstractQueuedSynchronizer(AQS) 超详细原理解析的更多相关文章
- centos7安装zabbix3.0超详细步骤解析
centos7安装zabbix3.0超详细步骤解析 很详细,感谢作者 以下是我操作的history 622 java -version 623 javac -version 624 grep SELI ...
- 超详细JSON解析步骤
JSON简介 JAVAScript Object Notation是一种轻量级的数据交换格式 具有良好的可读和便于快速编写的特性. 业内主流技术为其提供了完整的解决方案(有点类似于正则表达式 ,获得了 ...
- MySQL索引机制(详细+原理+解析)
MySQL索引机制 永远年轻,永远热泪盈眶 一.索引的类型与常见的操作 前缀索引 MySQL 前缀索引能有效减小索引文件的大小,提高索引的速度.但是前缀索引也有它的坏处:MySQL 不能在 ORDER ...
- 干货 | 10分钟带你掌握branch and price(分支定价)算法超详细原理解析
00 前言 相信大家对branch and price的神秘之处也非常好奇了.今天我们一起来揭秘该算法原理过程.不过,在此之前,请大家确保自己的branch and bound和column gene ...
- 【算法】禁忌搜索算法(Tabu Search,TS)超详细通俗解析附C++代码实例
01 什么是禁忌搜索算法? 1.1 先从爬山算法说起 爬山算法从当前的节点开始,和周围的邻居节点的值进行比较. 如果当前节点是最大的,那么返回当前节点,作为最大值 (既山峰最高点):反之就用最高的邻居 ...
- 转:Centos7安装zabbix3.4超详细步骤解析
安装前准备: 1.1 安装依赖包: yum -y install wget net-snmp-devel OpenIPMI-devel httpd openssl-devel java lrzsz f ...
- JUC 并发编程--11, AQS源码原理解析, ReentrantLock 源码解读
这里引用别人博客,不重复造轮子 https://blog.csdn.net/u012881584/article/details/105886486 https://www.cnblogs.com/w ...
- 超详细的Guava RateLimiter限流原理解析
超详细的Guava RateLimiter限流原理解析 mp.weixin.qq.com 点击上方“方志朋”,选择“置顶或者星标” 你的关注意义重大! 限流是保护高并发系统的三把利器之一,另外两个是 ...
- jdk动态代理和cglib动态代理底层实现原理超详细解析(jdk动态代理篇)
代理模式是一种很常见的模式,本文主要分析jdk动态代理的过程 1.举例 public class ProxyFactory implements InvocationHandler { private ...
随机推荐
- window cmd下常用操作
创建文件夹 mkdir 创建空文件 type nul>文件名 进入目录 cd 进入分区 分区名 引入文件 当前文件: ./文件名 或 直接文件名 上一级目录文件及上一级目录下子文件:../文件名 ...
- 6.mybatis----日志工厂
日志工厂 如果一个数据库操作出现了异常,我们需要排错,所以说日志就是最好的助手 曾经:sout,debug 现在:日志工厂 在Mybatis中具体使用哪一个日志,在设置中设定 咋设定? 在mybati ...
- vue 使用 jsonp 请求数据
vue 使用 jsonp 请求数据 vue请求数据的时候,会遇到跨域问题,服务器为了保证信息的安全,对跨域请求进行拦截,因此,为了解决vue跨域请求问题,需要使用jsonp. 安装jsonp npm ...
- monkey工具使用(未完待续)
monkey命令详解: 转自:http://blog.csdn.net/jlminghui/article/details/38238443 http://www.cnblogs.com/wfh198 ...
- Java学习笔记(九)面向对象---模板方法设计模式
理解 在定义功能时功能的一部分是确定的,但是有一部分是不确定的,而确定的部分在使用不确定的部分,那么就将不确定的部分暴露出去,由该类的子类完成. 举例 需求 获取一段程序的运行时间 代码 abstra ...
- servlet中Request与response使用
服务器根据请求自动创建传入HttpServletRequest对象和HttpServletResponse对象 @Override protected void service(HttpServlet ...
- IntelliJ IDEA 2017.3尚硅谷-----自动导包
- C# byte[]转string, string转byte[] 的四种方法
转载:https://blog.csdn.net/tom_221x/article/details/71643015 第一种 string str = System.Text.Encoding ...
- Codeforces Round #599 (Div. 2) B1. Character Swap (Easy Version)
This problem is different from the hard version. In this version Ujan makes exactly one exchange. Yo ...
- AcWing 840. 模拟散列表
拉链法 #include<cstring> #include<iostream> using namespace std ; ; int h[N],e[N],ne[N],idx ...