AQS浅析

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

AQS的原理浅析

本文是《Java特种兵》的样章,本书即将由工业出版社出版

AQS的全称为(AbstractQueuedSynchronizer),这个类也是在java.util.concurrent.locks下面。这个类似乎很不容易看懂,因为它仅仅是提供了一系列公共的方法,让子类来调用。那么要理解意思,就得从子类下手,反过来看才容易看懂。如下图所示:
QQ图片20140110194431

图 5-15 AQS的子类实现

 

这么多类,我们看那一个?刚刚提到过锁(Lock),我们就从锁开始吧。这里就先以ReentrantLock排它锁为例开始展开讲解如何利用AQS的,然后再简单介绍读写锁的要点(读写锁本身的实现十分复杂,要完全说清楚需要大量的篇幅来说明)。
首先来看看ReentrantLock的构造方法,它的构造方法有两个,如下图所示:
QQ图片20140110194534
图 5-16 排它锁的构造方法
很显然,对象中有一个属性叫sync,有两种不同的实现类,默认是“NonfairSync”来实现,而另一个“FairSync”它们都是排它锁的内部类,不论用那一个都能实现排它锁,只是内部可能有点原理上的区别。先以“NonfairSync”类为例,它的lock()方法是如何实现的呢?
QQ图片20140110194615
图 5-17 排它锁的lock方法
lock()方法先通过CAS尝试将状态从0修改为1。若直接修改成功,前提条件自然是锁的状态为0,则直接将线程的OWNER修改为当前线程,这是一种理想情况,如果并发粒度设置适当也是一种乐观情况。
若上一个动作未成功,则会间接调用了acquire(1)来继续操作,这个acquire(int)方法就是在AbstractQueuedSynchronizer当中了。这个方法表面上看起来简单,但真实情况比较难以看懂,因为第一次看这段代码可能不知道它要做什么!不急,一步一步来分解。
首先看tryAcquire(arg)这里的调用(当然传入的参数是1),在默认的“NonfairSync”实现类中,会这样来实现:
QQ图片20140110194650

妈呀,这代码好费劲,胖哥第一回看也是觉得这样,细心看看也不是想象当中那么难:

○ 首先获取这个锁的状态,如果状态为0,则尝试设置状态为传入的参数(这里就是1),若设置成功就代表自己获取到了锁,返回true了。状态为0设置1的动作在外部就有做过一次,内部再一次做只是提升概率,而且这样的操作相对锁来讲不占开销。
○ 如果状态不是0,则判定当前线程是否为排它锁的Owner,如果是Owner则尝试将状态增加acquires(也就是增加1),如果这个状态值越界,则会抛出异常提示,若没有越界,将状态设置进去后返回true(实现了类似于偏向的功能,可重入,但是无需进一步征用)。
○ 如果状态不是0,且自身不是owner,则返回false。

回到图 5-17中对tryAcquire()的调用判定中是通过if(!tryAcquire())作为第1个条件的,如果返回true,则判定就不会成立了,自然后面的acquireQueued动作就不会再执行了,如果发生这样的情况是最理想的。
无论多么乐观,征用是必然存在的,如果征用存在则owner自然不会是自己,tryAcquire()方法会返回false,接着就会再调用方法:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)做相关的操作。
这个方法的调用的代码更不好懂,需要从里往外看,这里的Node.EXCLUSIVE是节点的类型,看名称应该清楚是排它类型的意思。接着调用addWaiter()来增加一个排它锁类型的节点,这个addWaiter()的代码是这样写的:
QQ图片20140110194812
图 5-19 addWaiter的代码
这里创建了一个Node的对象,将当前线程和传入的Node.EXCLUSIVE传入,也就是说Node节点理论上包含了这两项信息。代码中的tail是AQS的一个属性,刚开始的时候肯定是为null,也就是不会进入第一层if判定的区域,而直接会进入enq(node)的代码,那么直接来看看enq(node)的代码。

看到了tail就应该猜到了AQS是链表吧,没错,而且它还应该有一个head引用来指向链表的头节点,AQS在初始化的时候head、tail都是null,在运行时来回移动。此时,我们最少至少知道AQS是一个基于状态(state)的链表管理方式。

QQ图片20140110194853

图 5-20 enq(Node)的源码
这段代码就是链表的操作,某些同学可能很牛,一下就看懂了,某些同学一扫而过觉得知道大概就可以了,某些同学可能会莫不着头脑。胖哥为了给第三类同学来“开开荤”,简单讲解下这个代码。
首先这个是一个死循环,而且本身没有锁,因此可以有多个线程进来,假如某个线程进入方法,此时head、tail都是null,自然会进入if(t == null)所在的代码区域,这部分代码会创建一个Node出来名字叫h,这个Node没有像开始那样给予类型和线程,很明显是一个空的Node对象,而传入的Node对象首先被它的next引用所指向,此时传入的node和某一个线程创建的h对象如下图所示。
QQ图片20140110194922
图 5-21 临时的h对象创建后的与传入的Node指向关系
刚才我们很理想的认为只有一个线程会出现这种情况,如果有多个线程并发进入这个if判定区域,可能就会同时存在多个这样的数据结构,在各自形成数据结构后,多个线程都会去做compareAndSetHead(h)的动作,也就是尝试将这个临时h节点设置为head,显然并发时只有一个线程会成功,因此成功的那个线程会执行tail = node的操作,整个AQS的链表就成为:

QQ图片20140110194957

图 5-22 AQS被第一个请求成功的线程初始化后
有一个线程会成功修改head和tail的值,其它的线程会继续循环,再次循环就不会进入if (t == null)的逻辑了,而会进入else语句的逻辑中。
在else语句所在的逻辑中,第一步是node.prev = t,这个t就是tail的临时值,也就是首先让尝试写入的node节点的prev指针指向原来的结束节点,然后尝试通过CAS替换掉AQS中的tail的内容为当前线程的Node,无论有多少个线程并发到这里,依然只会有一个能成功,成功者执行t.next = node,也就是让原先的tail节点的next引用指向现在的node,现在的node已经成为了最新的结束节点,不成功者则会继续循环。
简单使用图解的方式来说明,3个步骤如下所示,如下图所示:

QQ图片20140110194957

图 5-23 插入一个节点步骤前后动作
插入多个节点的时候,就以此类推了哦,总之节点都是在链表尾部写入的,而且是线程安全的。
知道了AQS大致的写入是一种双向链表的插入操作,但插入链表节点对锁有何用途呢,我们还得退回到前面图 5-19的代码中addWaiter方法最终返回了要写入的node节点, 再回退到图5-17中所在的代码中需要将这个返回的node节点作为acquireQueued方法入口参数,并传入另一个参数(依然是1),看看它里面到底做了些什么?请看下图:
QQ图片20140110195059

图 5-24 acquireQueued的方法内容
这里也是一个死循环,除非进入if(p == head && tryAcquire(arg))这个判定条件,而p为node.predcessor()得到,这个方法返回node节点的前一个节点,也就是说只有当前一个节点是head的时候,进一步尝试通过tryAcquire(arg)来征用才有机会成功。tryAcquire(arg)这个方法我们前面介绍过,成立的条件为:锁的状态为0,且通过CAS尝试设置状态成功或线程的持有者本身是当前线程才会返回true,我们现在来详细拆分这部分代码。
○ 如果这个条件成功后,发生的几个动作包含:
(1) 首先调用setHead(Node)的操作,这个操作内部会将传入的node节点作为AQS的head所指向的节点。线程属性设置为空(因为现在已经获取到锁,不再需要记录下这个节点所对应的线程了),再将这个节点的perv引用赋值为null。
(2) 进一步将的前一个节点的next引用赋值为null。
在进行了这样的修改后,队列的结构就变成了以下这种情况了,通过这样的方式,就可以让执行完的节点释放掉内存区域,而不是无限制增长队列,也就真正形成FIFO了:

QQ图片20140110195124
图 5-25 CAS成功获取锁后,队列的变化
○ 如果这个判定条件失败
会首先判定:“shouldParkAfterFailedAcquire(p , node)”,这个方法内部会判定前一个节点的状态是否为:“Node.SIGNAL”,若是则返回true,若不是都会返回false,不过会再做一些操作:判定节点的状态是否大于0,若大于0则认为被“CANCELLED”掉了(我们没有说明几个状态的值,不过大于0的只可能被CANCELLED的状态),因此会从前一个节点开始逐步循环找到一个没有被“CANCELLED”节点,然后与这个节点的next、prev的引用相互指向;如果前一个节点的状态不是大于0的,则通过CAS尝试将状态修改为“Node.SIGNAL”,自然的如果下一轮循环的时候会返回值应该会返回true。
如果这个方法返回了true,则会执行:“parkAndCheckInterrupt()”方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。
相应的,可以自己看看FairSync实现类的lock方法,其实区别不大,有些细节上的区别可能会决定某些特定场景的需求,你也可以自己按照这样的思路去实现一个自定义的锁。
接下来简单看看unlock()解除锁的方式,如果获取到了锁不释放,那自然就成了死锁,所以必须要释放,来看看它内部是如何释放的。同样从排它锁(ReentrantLock)中的unlock()方法开始,请先看下面的代码截图:

QQ图片20140110195158

图 5-26 unlock方法间接调用AQS的release(1)来完成
通过tryRelease(int)方法进行了某种判定,若它成立则会将head传入到unparkSuccessor(Node)方法中并返回true,否则返回false。首先来看看tryRelease(int)方法,如下图所示:
QQ图片20140110195239

图 5-27 tryRelease(1)方法
这个动作可以认为就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。
在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。
这一点大家写代码要注意了哦,如果是在循环体中lock()或故意使用两次以上的lock(),而最终只有一次unlock(),最终可能无法释放锁。在本书的src/chapter05/locks/目录下有相应的代码,大家可以自行测试的哦。
在方法unparkSuccessor(Node)中,就意味着真正要释放锁了,它传入的是head节点(head节点是已经执行完的节点,在后面阐述这个方法的body的时候都叫head节点),内部首先会发生的动作是获取head节点的next节点,如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程,这样一来将会有一个节点唤醒后继续进入图 5-24中的循环进一步尝试tryAcquire()方法来获取锁,但是也未必能完全获取到哦,因为此时也可能有一些外部的请求正好与之征用,而且还奇迹般的成功了,那这个线程的运气就有点悲剧了,不过通常乐观认为不会每一次都那么悲剧。
再看看共享锁,从前面的排它锁可以看得出来是用一个状态来标志锁的,而共享锁也不例外,但是Java不希望去定义两个状态,所以它与排它锁的第一个区别就是在锁的状态上,它用int来标志锁的状态,int有4个字节,它用高16位标志读锁(共享锁),低16位标志写锁(排它锁),高16位每次增加1相当于增加65536(通过1 << 16得到),自然的在这种读写锁中,读锁和写锁的个数都不能超过65535个(条件是每次增加1的,如果递增是跳跃的将会更少)。在计算读锁数量的时候将状态左移16位,而计算排它锁会与65535“按位求与”操作,如下图所示。

QQ图片20140110195308

图 5-28 读写锁中的数量计算及限制
写锁的功能与“ReentrantLock”基本一致,区域在于它会在tryAcquire操作的时候,判定状态的时候会更加复杂一些(因此有些时候它的性能未必好)。
读锁也会写入队列,Node的类型被改为:“Node.SHARED”这种类型,lock()时候调用的是AQS的acquireShared(int)方法,进一步调用tryAcquireShared()操作里面只需要检测是否有排它锁,如果没有则可以尝试通过CAS修改锁的状态,如果没有修改成功,则会自旋这个动作(可能会有很多线程在这自旋开销CPU)。如果这个自旋的过程中检测到排它锁竞争成功,那么tryAcquireShared()会返回-1,从而会走如排它锁的Node类似的流程,可能也会被park住,等待排它锁相应的线程最终调用unpark()动作来唤醒。
这就是Java提供的这种读写锁,不过这并不是共享锁的诠释,在共享锁里面也有多种机制 ,或许这种读写锁只是其中一种而已。在这种锁下面,读和写的操作本身是互斥的,但是读可以多个一起发生。这样的锁理论上是非常适合应用在“读多写少”的环境下(当然我们所讲的读多写少是读的比例远远大于写,而不是多一点点),理论上讲这样锁征用的粒度会大大降低,同时系统的瓶颈会减少,效率得到总体提升。
在本节中我们除了学习到AQS的内在,还应看到Java通过一个AQS队列解决了许多问题,这个是Java层面的队列模型,其实我们也可以利用许多队列模型来解决自己的问题,甚至于可以改写模型模型来满足自己的需求,在本章的5.6.1节中将会详细介绍。
关于Lock及AQS的一些补充:
1、 Lock的操作不仅仅局限于lock()/unlock(),因为这样线程可能进入WAITING状态,这个时候如果没有unpark()就没法唤醒它,可能会一直“睡”下去,可以尝试用tryLock()、tryLock(long , TimeUnit)来做一些尝试加锁或超时来满足某些特定场景的需要。例如有些时候发现尝试加锁无法加上,先释放已经成功对其它对象添加的锁,过一小会再来尝试,这样在某些场合下可以避免“死锁”哦。
2、 lockInterruptibly() 它允许抛出InterruptException异常,也就是当外部发起了中断操作,程序内部有可能会抛出这种异常,但是并不是绝对会抛出异常的,大家仔细看看代码便清楚了。
3、 newCondition()操作,是返回一个Condition的对象,Condition只是一个接口,它要求实现await()、awaitUninterruptibly()、awaitNanos(long)、await(long , TimeUnit)、awaitUntil(Date)、signal()、signalAll()方法,AbstractQueuedSynchronizer中有一个内部类叫做ConditionObject实现了这个接口,它也是一个类似于队列的实现,具体可以参考源码。大多数情况下可以直接使用,当然觉得自己比较牛逼的话也可以参考源码自己来实现。
4、 在AQS的Node中有每个Node自己的状态(waitStatus),我们这里归纳一下,分别包含:
SIGNAL 从前面的代码状态转换可以看得出是前面有线程在运行,需要前面线程结束后,调用unpark()方法才能激活自己,值为:-1
CANCELLED 当AQS发起取消或fullyRelease()时,会是这个状态。值为1,也是几个状态中唯一一个大于0的状态,所以前面判定状态大于0就基本等价于是CANCELLED的意思。
CONDITION 线程基于Condition对象发生了等待,进入了相应的队列,自然也需要Condition对象来激活,值为-2。
PROPAGATE 读写锁中,当读锁最开始没有获取到操作权限,得到后会发起一个doReleaseShared()动作,内部也是一个循环,当判定后续的节点状态为0时,尝试通过CAS自旋方式将状态修改为这个状态,表示节点可以运行。
状态0 初始化状态,也代表正在尝试去获取临界资源的线程所对应的Node的状态。

转载于:https://my.oschina.net/u/1185936/blog/857268

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/259419.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

str045漏洞提权linux,Linux运维知识之CVE-2016-5195 Dirtycow: Linux内核提权漏洞

本文主要向大家介Linux运维知识之CVE-2016-5195 Dirtycow&#xff1a; Linux内核提权漏洞绍了&#xff0c;通过具体的内容向大家展现&#xff0c;希望对大家学习Linux运维知识有所帮助。CVE-2016-5195 Dirtycow&#xff1a; Linux内核提权漏洞以下都是github上找的源码&#xf…

编程如写作

昨晚似乎是个适合写作的夜&#xff0c;不论是自己还是朋友&#xff0c;都比平常更容易被触动。看着微博上朋友们的心路&#xff0c;想写点什么却似乎找不出非常值得大书特书的主题&#xff0c;只是歪坐在电脑旁&#xff0c;喝着咖啡&#xff0c;单曲循环着仓木麻衣的《time aft…

C++中cin、cin.get()、cin.getline()、getline()等函数的用法

转载&#xff1a;http://www.cnblogs.com/flatfoosie/archive/2010/12/22/1914055.html c输入流函数主要以下几个&#xff1a; 1、cin 2、cin.get() 3、cin.getline() 4、getline() 附:cin.ignore();cin.get()//跳过一个字符,例如不想要的回车,空格等字符 1、cin>>…

工作环境总结(1)开发环境搭建

1、安装git 安装文件&#xff1a;Git-2.12.0-64-bit.exe 下载地址&#xff1a;https://github.com/git-for-windows/git/releases/download/v2.12.0.windows.1/Git-2.12.0-64-bit.exe 在git bash中配置&#xff0c;git bash命令行中执行&#xff08;只有使用到egit时使用&…

c语言烟花百度云,C语言实现放烟花的程序

这是一个利用C语言编写放烟花的程序(同时也可以播放音乐)&#xff0c;供大家参考&#xff0c;具体内容如下代码如下#pragma once#include#include //图形界面库头文件#include //计算圆形的轨迹坐标#include#include#include#include#pragma comment(lib,"winmm.lib"…

决定人生的七条公式

1 .积跬步以致千里&#xff0c;积怠惰以致深渊 1.01^365 37.80.99^365 0.032.拖延症 U EV/ID U完成任务的程度 E对成功的信心 V 对任务的愉悦度 I 你的分心程度 D你多久会获得回报3.三天打鱼两天晒网&#xff0c;终将一无所获 1.01^3 x 0.99^2 < 1.01 4.爱因斯坦的成…

strncpy与strcpy的区别与注意事项

strncpy 是 C语言的库函数之一&#xff0c;来自 C语言标准库&#xff0c;定义于 string.h&#xff0c;char *strncpy(char *dest, char *src, int n)&#xff0c;把src所指字符串的前n个字节复制到dest所指的数组中&#xff0c;并返回指向dest的指针。 strcpy只是复制字符串&am…

使用ssh公钥实现免密码登录

ssh 无密码登录要使用公钥与私钥。linux下可以用用ssh-keygen生成公钥/私钥对&#xff0c;下面我以CentOS为例。 有机器A(192.168.1.155)&#xff0c;B(192.168.1.181)。现想A通过ssh免密码登录到B。 首先以root账户登陆为例。 1.在A机下生成公钥/私钥对。 [rootA ~]# ssh-keyg…

15款的视频处理软件免费下载

因为需要购买昂贵的视频处理软件和高性能图形计算机&#xff0c;所以视频处理是一项比较耗费金钱的技术活。正是由于这样&#xff0c;一部分人选择使用性能较好的免费在线编辑软件&#xff0c;无需太多视频处理知识便可在浏览器中剪切和编辑视频。然而&#xff0c;当我们无法连…

液位系统c语言程序,超声波自动测量物体液位系统的设计

超声波自动测量物体液位系统的设计(任务书,毕业论文15000字)摘要本系统以STC89C52单片机为核心&#xff0c;通过硬件电路连接和软件程序的编写实现通用型超声波自动测量物体液位系统的设计。其主要原理是由单片机控制超声波发射电路发射超声波&#xff0c;超声波接收电路接收遇…

android-sdk-windows版本号下载

Android SDK 4.0.3 开发环境配置及执行 近期又装了一次最新版本号的ADK环境 眼下最新版是Android SDK 4.0.3 本文的插图和文本尽管是Android2.2的 步骤都是一样的&#xff0c;假设安装的过程中遇到什么问题&#xff0c;能够留言&#xff0c;我会尽快回复&#xff01; 系统环境的…

string中c_str()、data()、copy(p,n)函数的用法

转载&#xff1a;http://www.cnblogs.com/qlwy/archive/2012/03/25/2416937.html 标准库的string类提供了3个成员函数来从一个string得到c类型的字符数组&#xff1a;c_str()、data()、copy(p,n)。 1. c_str()&#xff1a;生成一个const char*指针&#xff0c;指向以空字符终止…

POJ2402 Palindrome Numbers 回文数

题目链接: http://poj.org/problem?id2402 题目大意就是让你找到第n个回文数是什么. 第一个思路当然是一个一个地构造回文数直到找到第n个回文数为止(也许大部分人一开始都是这样的思路). 很明显找到第n个之前的所有操作都是浪费, 这也是这个方法的最大弱点. 抱着侥幸心理(谁知…

离散卷积的c语言编程实验,数字信号处理实验一离散卷积c语言编程.ppt

数字信号处理实验一离散卷积c语言编程实验一 离散卷积的C语言编程实验 DSP实验室 2005 实验性质 综合设计性实验 实验目的 1 了解和认识常用的各种信号&#xff1b; 2 掌握卷积的定义和计算方法&#xff1b; 3 掌握在计算机中生成以及绘制信号序列图的方法。 实验原理 离散时间…

async-await原理解析

在用async包裹的方法体中&#xff0c;可以使用await关键字以同步的方式编写异步调用的代码。那么它的内部实现原理是什么样的呢&#xff1f;我们是否可以自定义await以实现定制性的需求呢&#xff1f;先来看一个简单的例子&#xff1a; 1 class Test {2 public sta…

emacs-w3m查看html帮助手册

<?xml version"1.0" encoding"utf-8"?> emacs-w3m查看html帮助手册emacs-w3m查看html帮助手册 Table of Contents 1. 使用效果2. 为什么要用emacs-w3m来查看html的帮助手册&#xff1f;3. 什么是w3m?4. 配置5. 额外资源1 使用效果 使用快捷键C-c …

c语言生命游戏代码大全,c++生命游戏源码

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼glViewport( 0, 0, width, height );glMatrixMode( GL_PROJECTION );glLoadIdentity( );}//程序入口int main(int argc, char *argv[]){//随机生成细胞的状态MapRand();std::cout<//SDL初始化const SDL_VideoInfo* info NULL;i…

初学React,setState后获取到的thisstate没变,还是初始state?

问题&#xff1a;(javascript)初学React&#xff0c;setState后获取到的thisstate没变&#xff0c;还是初始state&#xff1f;描述: getInitialState(){return {data:[]};},componentDidMount(){var data [ { author: "Pete Hunt", text: "This is one comment…

sizeof(数组名)和sizeof(指针)

转载&#xff1a;http://blog.csdn.net/kangroger/article/details/20653255 在做这道题时&#xff1a; 32位环境下&#xff0c;int *pnew int[10];请问sizeof(p)的值为&#xff08;&#xff09; A、4 B、10 C、40 D、8 我以为正确答…

工作中的问题

今天写一专题页面&#xff0c;写出的结果在各个浏览器下都不同&#xff0c;心情不好。。。 就是红线的地方老对不齐。。。 在朋友指导下改了下样式好了 右边代码结构 1 <div class"fr Img"> 2 <h3>相关专题</h3> 3 <a href"#"…