AQS的原理及源码分析
AQS是什么
AQS= volatile修饰的state变量(同步状态) +FIFO队列(CLH改善版的虚拟双向队列,用于阻塞等待唤醒机制)
队列里维护的Node节点主要包含:等待状态waitStatus,前后指针,等待的线程。
AQS是个抽象队列同步器,是JUC体系中用来构建锁和其他同步器如 ReentrantLock/CountDownLatch/Semphore的基石。AQS内部通过内置的FIFO先进先出的LCH(虚拟双向链表)队列来完成线程排队,并通过volatile 修饰的int类型状态变量来表示持有锁的状态。
简单的说,AQS通过volatile 修饰的int类型状态变量来表示同步状态,加volatial的目的是保证可见性。然后如果状态变量大于等于1是表示资源被占用,这时候抢不到资源的线程就要进入排队等候队列,等待资源的释放,这里面就需要阻塞等待唤醒机制来实现,AQS通过把等待获取资源的线程封装为Node<Thread>
节点入队,在资源释放后通过LockSupport.park().unPark()
来唤醒线程,通过CAS自旋来进行资源的抢占。
AQS源码解析——以ReentrantLock为例
公平锁与非公平锁
ReentrantLock默认是非公平锁,如果要实现公平锁构造函数中传入true表示创建的是公平锁。
公平锁相较于非公平锁体现在公平锁会先判断队列中是否有等待的线程,有的话优先获取到锁资源。
ReentrantLock 类图
AQS的Node属性含义
AQS结构图
非公平锁加锁过程
以3个线程分别为ABC争抢锁为例。
代码示例
public class ReentrantLockTest {
//模拟银行排队
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//第一个获取到锁的客户,执行自己的业务60秒
new Thread(() -> {
lock.lock();
try {
System.out.println("A 获取到锁,执行任务------------");
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
//第二个客户获取不到锁,阻塞
new Thread(() -> {
lock.lock();
try {
System.out.println("B 获取到锁,执行任务------------");
} finally {
lock.unlock();
}
}, "B").start();
//第三个客户获取不到锁,阻塞
new Thread(() -> {
lock.lock();
try {
System.out.println("C 获取到锁,执行任务------------");
} finally {
lock.unlock();
}
}, "C").start();
}
}
- 当A进来加锁时,会进行CAS加锁,加锁成功就会设置exclusiveOwnerThread目前占用锁的线程为自己。
- 当B进来加锁时,也会进行CAS尝试加锁,这时候加锁不成功后,或调用尝试获取锁的方法。这个方法里或再判断下这时候锁状态是否为0,也就是锁是否释放了,如果为0则会再进行CAS尝试加锁;如果锁状态不为0表示被占用了,这时候回判断是否目前加锁的线程是不是自己,如果是的话就进入可重入锁的逻辑,对加锁state变量加1,这就是我们加几次锁就要减几次锁的原因。
- 如果B尝试加锁失败后,B就会进入等待队列中进行等待,在加入队列的操作中,AQS会把线程B先包装成一个独占锁模式的Node节点,并判断尾结点是否为空,为空的话就要先判断队列是否还未初始化,如果还未初始化,会先创建一个空的哨兵节点(也叫虚节点,主要作用是用来占位),再将线程B的节点与哨兵节点进行双向队列关联,跟在哨兵节点后面,这时候就入队成功了。如果判断尾结点不为空,那就设置当前节点为尾结点,并与之前的尾结点设置关联关系。
- 在添加如队列成功后,线程B会调用
acquireQueued
方法继续尝试,线程B会通过自旋判断自己在队列中的位置,如果线程B的前节点是哨兵节点,那么线程B进行自旋处理,首先会继续CAS尝试加锁,这时候如果还是不成功,就会设置线程B的前缀节点的等待状态从0变成-1,表示等待被唤醒状态。继续进入自旋逻辑,还是会再尝试CAS尝试加锁一次,还是失败就会调用LockSupport.park(this);
方法把线程设置为阻塞状态,等待被唤醒。 - 当线程A接收完业务后释放锁,释放锁时当判断释放后state的状态为0时,就会把当前锁的状态设置为0,表示锁已经空闲了,并设置exclusiveOwnerThread目前占用锁的线程为null。然后判断头结点是否不为空且头节点的等待状态为-1等待被唤醒,如果是的话就走唤醒逻辑,先把头节点等待状态设置为初始值0,然后判断头结点的后缀节点如不为空的话,就唤醒它。这样子线程B就会被唤醒了。
- 线程B被唤醒后就会继续进行自旋CAS尝试获取锁,这时候就能成功获取到了。而获取到锁后state状态继续变成1表示锁被占用,设置exclusiveOwnerThread目前占用锁的线程为线程B。然后把原来线程B的节点设置为头节点,并把B处理为null的哨兵节点,把原来的哨兵节点取消前后指针引用让GC回收掉。
注意
- 一个线程会尝试抢4次锁才会进入到等待唤醒的阻塞状态中。
AQS为什么必须有哨兵节点——占位的目的
1.如果没有哨兵节点,那么每次执行入队操作,都需要判断head是否为空,如果为空则head=new Node如果不为空则head.next=new Node,而有哨兵节点则可以大胆的head.next=new Node.
2.如果没有哨兵节点,可能存在之前所说的安全性问题,当只有一个节点的时候执行入队方法,无法保证last和head不为空。哪怕执行enqueue入队之前last和head还指向一个节点,可能由于并发性在具体调用enqueue方法操作last的时候head和last共同指向的头节点已经完成出队,此时last和head都为null,所以enqueue方法中的last.next=new node会抛空指针异常,且由于线程并发性的问题,last始终可能随时为空的问题不使用哨兵节点是无法解决的。
AQS的原理及源码分析的更多相关文章
- 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理
1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...
- (转)ReentrantLock实现原理及源码分析
背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...
- OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波
http://blog.csdn.net/chenyusiyuan/article/details/8710462 OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 201 ...
- ConcurrentHashMap实现原理及源码分析
ConcurrentHashMap实现原理 ConcurrentHashMap源码分析 总结 ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对Ha ...
- HashMap和ConcurrentHashMap实现原理及源码分析
HashMap实现原理及源码分析 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表, ...
- 【转】HashMap实现原理及源码分析
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...
- 【OpenCV】SIFT原理与源码分析:DoG尺度空间构造
原文地址:http://blog.csdn.net/xiaowei_cqu/article/details/8067881 尺度空间理论 自然界中的物体随着观测尺度不同有不同的表现形态.例如我们形 ...
- 《深入探索Netty原理及源码分析》文集小结
<深入探索Netty原理及源码分析>文集小结 https://www.jianshu.com/p/239a196152de
- HashMap实现原理及源码分析之JDK8
继续上回HashMap的学习 HashMap实现原理及源码分析之JDK7 转载 Java8源码-HashMap 基于JDK8的HashMap源码解析 [jdk1.8]HashMap源码分析 一.H ...
随机推荐
- Excel-返回列表或数据库中的分类汇总(汇总可以实现要还是不要统计隐藏行功能) subtotal()
SUBTOTAL函数 函数名称:SUBTOTAL 主要功能:返回列表或数据库中的分类汇总. 使用格式:SUBTOTAL(function_num, ref1, ref2, ...) 参数说明:Func ...
- Identity Server 4 从入门到落地(三)—— 创建Web客户端
书接上回,我们已经搭建好了基于Identity Server 4的认证服务和管理应用(如果还没有搭建,参看本系列前两部分,相关代码可以从github下载:https://github.com/zhen ...
- Spring Security 基于URL的权限判断
1. FilterSecurityInterceptor 源码阅读 org.springframework.security.web.access.intercept.FilterSecurityI ...
- idea安装插件 JClassLib Bytecode viewer
目录 idea安装插件 JClassLib Bytecode viewer 安装过程 使用 idea安装插件 JClassLib Bytecode viewer IDEA 中安装 jClassLib ...
- tomcat拦截特殊字符报400,如 "|" "{" "}" ","等符号的解决方案
最近在做一个项目,需要对外暴露两个接口接收别人给的参数,但是有一个问题就是对方的项目是一个老项目,在传参数的时候是将多个字符放在一个参数里面用"|"进行分割,然而他们传参数的时候又 ...
- java9 模块化 jigsaw
java9并没有在语言层面做出很多改变,而是致力于一些新特性,如模块化,其核心就是解决历史遗留问题,为以后的jar包森林理清道路.模块化是一个很大的命题,就不讲那么细致了,关于java9的特性也有很多 ...
- [PE]结构分析与代码实现
PE结构浅析 知识导向: 程序最开始是存放在磁盘上的,运行程序首先需要申请4GB的内存,将程序从磁盘copy到内存,但不是直接复制,而是进行拉伸处理. 这也就是为什么会有一个文件中地址和一个Virtu ...
- vim使用配置(转)
在终端下使用vim进行编辑时,默认情况下,编辑的界面上是没有行号的.语法高亮度显示.智能缩进等功能的. 为了更好的在vim下进行工作,需要手动配置一个配置文件: .vimrc 在启动vim时,当前用户 ...
- Output of C++ Program | Set 4
Difficulty Level: Rookie Predict the output of below C++ programs. Question 1 1 #include<iostream ...
- mybatis中返回自动生成的id
当有时我们插入一条数据时,由于id很可能是自动生成的,如果我们想要返回这条刚插入的id怎么办呢. 在mysql数据中我们可以在insert下添加一个selectKey用以指定返回的类型和值: ...