AQS简介

AQS(AbstractQueuedSynchronizer)「抽象队列同步器」,简单的说「AQS就是一个抽象类」,抽象类就是AbstractQueuedSynchronizer,没有实现任何的接口,「仅仅定义了同步状态(state)的获取和释放的方法」

它提供了一个「FIFO队列」,多线程竞争资源的时候,没有竞争到的线程就会进入队列中进行等待,并且「定义了一套多线程访问共享资源的同步框架」

在AQS中的锁类型有两种:分别是「Exclusive(独占锁)「和」Share(共享锁)」。

「独占锁」就是「每次都只有一个线程运行」,例如ReentrantLock。关于ReentrantLock之前写过一篇详细的源码文章,喜欢的可以看一看[【源码篇】深入Lock锁底层原理实现,手写一个可重入锁]。

「共享锁」就是「同时可以多个线程运行」,如Semaphore、CountDownLatch、ReentrantReadWriteLock

AQS源码分析

在AQS的源码可以看到对于「state共享变量」,使用「volatile关键字」进行修饰,从而「保证了可见性」,若是对于volatile关键字不熟悉的可以参考这一篇[面试造飞机系列:volatile面试的连环追击,你还好吗?]。从上面的源码中可以看出,对于state的修改操作提供了setStatecompareAndSetState「那么为什么要提供这两个对state的修改呢?」

因为compareAndSetState方法「通常使用在获取到锁之前」,当前线程不是锁持有者,对于state的修改可能存在线程安全问题,所以「需要保证对state修改的原子性操作」

setState方法通常用于当前正持有锁的线程对「state共享变量」进行修改,因为不存在竞争,是线程安全的,所以没必要使用CAS操作。

分析了AQS的源码的实现,接下来我们看看AQS的实现的原理。这里AQS的实现源码和理论都会比较简单,因为还没有涉及到具体的实现类。

AQS实现原理

上面说到AQS中维护了一个「FIFO队列」,并且「该队列是一个双向链表」,链表中的每一个节点为「Node节点」「Node类是AbstractQueuedSynchronizer中的一个内部类」

让我们来看看AQS中Node内部类的源码,这样有助于我们能够对AQS的内部的实现更加的清晰:

 static final class Node {static final Node SHARED = new Node();static final Node EXCLUSIVE = null;static final int CANCELLED =  1;static final int SIGNAL    = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;volatile int waitStatus;volatile Node prev;volatile Node next;volatile Thread thread;Node nextWaiter;final boolean isShared() {return nextWaiter == SHARED;}final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}Node() {    // Used to establish initial head or SHARED marker}Node(Thread thread, Node mode) {     // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) { // Used by Conditionthis.waitStatus = waitStatus;this.thread = thread;}}

可以看到上面的Node类比较简单,只是对于每个Node节点拥有的属性进行维护,在Node内部类中最重要的基本构成就是这几个:

    volatile Node prev;volatile Node next;volatile Thread thread;

「根据源码的分析和线程的竞争共享资源的原理」,关于AQS的实现原理,我这里画了一张图:在FIFO队列中,「头节点占有锁」,也就是头节点才是锁的持有者,尾指针指向队列的最后一个等待线程节点,除了头节点和尾节点,节点之间都有「前驱指针」「后继指针」

在AQS中维护了一个「共享变量state」,标识当前的资源是否被线程持有,多线程竞争的时候,会去判断state是否为0,尝试的去把state修改为1

分析了AQS的源码的实现和原理实现,但是AQS里面具体是没有做同步的具体实现,如果要什么了解AQS的具体的实现原理,要需要看AQS的具体实现类,这边就以ReentrantLock为例。

ReentrantLock实现原理

如果多线程在竞争共享资源时,「竞争失败的线程就会添加入FIFO队列的尾部」

ReentrantLock的的具体实现中,这边以在ReentrantLock的非公平锁的实现为例,因为公平锁的实现,之前已经写过一篇文章分析过了。

我们来看看新添加节点的源码写的实现逻辑:当竞争锁资源的线程失败后直接进入acquire(1)方法,来来看看acquire(1)的具体实现:从源码中可以看出,acquire(1)的实现主要有这三步的逻辑:

  1. tryAcquire(arg):尝试再次获取锁。

  2. addWaiter(Node.EXCLUSIVE):若是获取锁失败,就会将当前线程组装成一个Node节点,进行入队操作。

  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):acquireQueued方法以addWaiter返回的头节点作为参数,内部实现进行锁自旋,以及判断是否应该执行线程挂起。

下面我们再来看看tryAcquire(arg)的源码,从上面的看一看出arg的值为1,具体的实现源码如下:从源码的分析中可以看出,tryAcquire(arg)的实现也就是判断state的值是否已经被释放,「若释放则当前线程就会CAS操作将state设置为1,若是没有释放,就会判断是否可以进行锁的重入」

分析完tryAcquire(arg)的实现,来看看addWaiter,入队操作的实现源码如下:从上面的源码分析,可以看出对于新加入的线程添加到双向链表中使用尾插法,具体的实现原理图如下所示。从上图分析,当线程加入队列中,主要进行这几步操作「新加入的节点prev指针指向原来的tail节点,原来的tail节点的next指针指向新加入的节点」,这个也就是常见的「双向链表尾插法」的操作。

「最后把tail指向新加入的节点」,如此一来就完成了新加入节点的入队操作,接下来我们接着分析源码。

当然这里的前提是「队列中不为空」,若是为空的话,不会走上面的逻辑,而是走enq(node),进行初始化节点,我们来看看enq(node)操作,源码如下:执行完上面的入对操作后,接着执行acquireQueued方法,来看看它的具体实现源码:从上上面的源码中可以看出,涉及到「头节点head的出队」操作,并且将「当前线程的node节点晋升为head节点」

因为只有「头节点才是锁的持有者」,所以对于head节点的出队操作,head的指向会随时改变,我这里画了一张原理图如下所示:具体实现如上图所示,会把「原来的头节点进行出队操作,也就是把原来的头节点next指针指向null,原来第二节点的prev指针指向null」

最后把head指针指向第二节点,当然thread2同时还会修改共享状态变量state的值,如此一来就完成了锁的释放。

当释放完锁之后,就会执行shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()判断当前的线程是否应该被挂起,我们来看看它的源码实现:shouldParkAfterFailedAcquire中的实现,当前驱节点的状态量waitStatusSIGNAL的时候,就会挂起。通过上面的分析对于AQS的实现基本有比较清晰的认识,主要是对实现类ReentrantLock的实现原理进行深入的分析,并且是基于「非公平锁」「独占锁」的实现。

在AQS的底层维护了一个「FIFO队列」,多线程竞争共享资源的时候,「失败的线程会被添加入队列中」「非公平锁」实现中,新加入的线程节点会「自旋」尝试的获取锁。

分析完AQS我们来分析CAS,「那么什么是CAS呢?」

CAS简介

在分析ReentrantLock的具体实现的源码中,可以看出所有涉及设置共享变量的操作,都会指向CAS操作,保证原子性操作。

CAS(compare and swap)原语理解就是比较并交换的意思,「CAS是一种乐观锁的实现」

在CAS的算法实现中有三个值:「更新的变量」「旧的值」「新值」。在修改共享资源时候,会与原值进行比较,若是等于原值,就修改为新值。

于是在这里的算法实现下,即使不加锁,也能保证数据的可见性,即使的发现数据是否被更改,若是数据已经被更新则写操作失败。

但是CAS也会引发ABA的问题,「什么是ABA问题呢?」 不慌请听我详细道来

ABA问题

ABA问题就是假如有两个线程,同一时间读取一个共享变量state=1,此时两个线程都已经将state的副本赋值到自己的工作内存中。

当线程一对state修改state=state+1,并且写入到主存中,然后线程一又对state=state-1写入到主存,此时主存的state是变化了两次,只不过又变回了原来的值。

那么此时线程二修改state的时候就会修改成功,这就是ABA问题。对于「ABA问题的解决方案就是加版本号(version)」,每次进行比较的时候,也会比较版本号。

因为版本版是只增不减,比如以时间作为版本号,每一时刻的时间都不一样,这样就能避免ABA的问题。

CAS性能分析

相对于「synchronized的阻塞算法」的实现,「CAS采用的是乐观锁的非阻塞算法」的实现,一般CPU在进行线程的上下文切换的时间比执行CPU的指令集的时间长,所以CAS操作在性能上也有了很大的提升。

但是所有的算法都是没有最完美的,在执行CAS的操作中,没有更新成功的就会自旋,这样也会消耗CPU的资源,对于CPU来说是不友好的。

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢

深入剖析AQS和CAS,看了都说好相关推荐

  1. 抽象同步器AQS、CAS应用之--ReentrantLock,lock和unlock的流程、源码分析

    文章目录 1. AQS和CAS 1.1 CAS存在的bug:ABA问题 2. ReentrantLock和synchronized的区别 3. ReentrantLock的内部结构 3.1 lock. ...

  2. python语法基础学习-Python基础语法精心总结!看完都知道的可以往下继续学习了...

    原标题:Python基础语法精心总结!看完都知道的可以往下继续学习了 这应该是最详细的Python入门基础语法总结! 定义变量,使用变量 1. input 用户自己输入值 2. print 打印值 可 ...

  3. 《看聊天记录都学不会C#?太菜了吧》(6)多晦涩的专业术语原来都会那么简单

    本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题.此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将 ...

  4. 《看聊天记录都学不会C#?太菜了吧》(5)C# 中可以用中文名变量?

    本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题.此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将 ...

  5. 《看聊天记录都学不会Python到游戏实战?太菜了吧》(8)我们开始做一个数字小游戏吧

    本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题.此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将 ...

  6. 《看聊天记录都学不会Python到游戏实战?太菜了吧》(10)无底洞的循环

    本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题.此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将 ...

  7. 《看聊天记录都学不会Python到游戏实战?太菜了吧》(7)我用函数写了个特洛伊木马

    本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题.此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将 ...

  8. 《看聊天记录都学不会C#?太菜了吧》(3)变量:我大哥呢?$:小弟我罩着你!

    本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题.此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将 ...

  9. 《看聊天记录都学不会C#?太菜了吧》(2)C#那么简单我为何之前还学C语言?

    本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题.此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将 ...

最新文章

  1. IBM携手天健“漫步”区域医疗信息化市场
  2. HEALTHY LIFE OPENCART 自适应主题模板 ABC-0133
  3. objdump and readelf
  4. in the java search_LeetCode第[33]题(Java):Search in Rotated Sorted Array
  5. boost::mp11::mp_fold_q相关用法的测试程序
  6. 【Java】Java复习笔记-第三部分
  7. cenos6.4安装gvim
  8. 怎么装python编译器_python编译器安装
  9. 转转集团二手双11大促:长沙用户“秒杀”99新iPhone12成首单
  10. java xml解析器_Java XML解析器
  11. shell训练营Day30
  12. 凯撒密码 (Python实现)
  13. python爬取雪球网交易数据
  14. RMXP脚本解析(二十):Game_Actors
  15. 利用Get-FileHash工具进行哈希验证
  16. 数据库-数据模型(分类、三要素、概念)
  17. 华为云注册登录之图像标签识别
  18. Kindle刷安卓双系统的方法_我是亲民_新浪博客
  19. 帮谷歌推广Webp图片格式之:Webp的格式转换
  20. 模电学习笔记(八)——差分放大器

热门文章

  1. neo4j安装_SpringBoot使用Neo4j
  2. java中局部变量和成员变量_Java中局部变量和成员变量可以转换么?
  3. 邻接表存储图的广度优先遍历
  4. 在一个数组中实现两个堆栈
  5. 用matlab画曲顶柱体费用数据,数值积分的matlab实现
  6. 方阵旋转180度JAVA代码_方阵顺时针旋转的实现代码
  7. zillow房价预测比赛_Zillow房价预测:2018年美国房价将会上涨
  8. python 动态编译代码_使用PyQt(Python+Qt)+动态编译36行代码实现的计算器
  9. linux 脚本停止自身,Linux系统定时任务时shell脚本停止执行如何解决?
  10. html下拉框由后端,select下拉框通过ajax获取后台的值