Java中的并发包大家应该都或多或少的了解过,说到并发包也就不得不提我们今天要说的AbstractQueuedSynchronizer,简称AQS,这个是很多并发工具类的实现基础
1public abstract class AbstractQueuedSynchronizer
2extends AbstractOwnableSynchronizer
3implements java.io.Serializable
类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依 赖于它,如常用的ReentrantLock、Semaphore、CountDownLatch
深入探究A Q S
先来看这个图,图中有颜色的为Method,无颜色的为Attribution
总的来说,AQS框架共分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据
当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程
道理也很简单,就像我们说的,这个东西是一个抽象的同步器,它将加锁和解锁这些操作交给了具体的实现类 来自己实现,就像这样
当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,获取锁成功之后便直接执行相应的逻辑,对于获取锁失败的流程,进入第三层和第四层的等待队列处
理,而这些处理方式均依赖于第五层的基础数据提供层这样给大家说的话,应该很容易就可以理解了
AQS 的实现数据结构
研究过AQS的同学应该对这个图都很熟悉了,AQS的核心就是state+Node+CLH变体双向队列
核心思想就是通过一个v o la tile 类型state状态来表示共享资源的状态, 如果被请求的资源空闲, 就将获得共享资源的线程设置为当前有效的线程, 然后修改state为锁定状态, 其它的线程及时可见
共享资源被占用之后,其它线程肯定不能直接就返回失败啊,这样这个并发包的高效就没得了,所以就引入了 一个双向队列,这个双向等待队列放置那些暂时还未抢到共享资源的线程,来完成等待唤醒机制
实际上,AQS的运行中的这个CLH变体的双向队列,不知存储未抢到共享资源的线程,而抢到共享资源的这个 线程也会作为队列的头节点head存在
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
这么说大家应该就很容易懂了吧,就是大家一起抢共享资源,抢到的就是有效线程,放到双向队列的head头 节点,没抢到的就依次往后排
我们接着看一下N o d e 节点是怎么做的
这个是Node节点的属性值和含义
简单解释一下,waitStatus就是节点在队列中的状态,Thread就是当前节点的线程,prev和next是前驱指针和后继指针
这里的重点就是waitStatus属性
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。 0:新结点入队时的默认状态。
正是由于这个特点, 负值表示结点处于有效等待状态, 而正值表示结点已被取消。所以源码中很多地方用> 0 、<0来判断结点的状态是否正常同步状态s tate
AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情 况。
对于这个state,AQS也是提供了几个方法
这几个方法都是final类型的,子类是无法修改的
在AQS中的是有两种加锁模式的,一种是共享式, 一种是独占式,共享式也很简单,就是通过控制AQS中的state数值即可
state是AQS中的volatile类型,具有可见性,用于记录加锁状态和重入的次数,当然不只是重入次数,其实这个state在不同的实现类中是有不同的意义的
【ReentrantLock】:state用于记录锁的持有状态和重入次数,state=0表示没有线程持有锁;state=1表示有一个线程持有锁;state=N表示exclusiveOwnerThread这个线程N次重入了这个锁。
【ReentrantReadWriteLock】:state用于记录读写锁的占用状态和持有线程数量(读锁)、重入次数(写锁),state的高16位记录持有读锁的线程数量,低16位记录写锁线程重入次数,如果这16位的值是0,表示没 有线程占用锁,否则表示有线程持有锁。
另外针对读锁,每个线程获取到的读锁次数由本地线程变量中的HoldCounter记录。
【Semaphore】:state用于计数。state=N表示还有N个信号量可以分配出去,state=0表示没有信号量了,此时所有需要acquire信号量的线程都等着;
【CountDownLatch】:state也用于计数,每次countDown都减一,减到0的时候唤醒被await阻塞的线程。
切记: 区分开v o la tile 类型的state属性和Node节点中的waitStatus属性抢占共享资源也是有两种方式的: 公平锁和非公平锁
大家用过ReentrantLock的同学肯定都知道,默认的是非公平锁,但是我们可以传入一个参数设置为公平锁
按照R e e n t rantLock来说一下公平锁和非公平锁
公平锁,是公平的,可以保证获取锁的线程按照先来后到的顺序,获取到锁。
非公平锁,各个线程获取到锁的顺序,不一定和它们申请的先后顺序一致,有可能后来的线程,反而先获取到 了锁。
在实现上,公平锁在进行lock时,首先会进行tryAcquire()操作。
在tryAcquire中,会判断等待队列中是否已经有别的线程在等待了。如果队列中已经有别的线程了, 则tryAcquire失败,则将自己加入队列。
如果队列中没有别的线程,则进行获取锁的操作。
非公平锁,在进行lock时,会直接尝试进行加锁,如果成功,则获取到锁,如果失败,则进行和公平锁相同 的动作。
从公平锁和非公平的实现上来看,他们的操作基本相同,唯一的区别在于,在lock时,非公平锁会直接先进 行尝试加锁的操作。
当前一个线程完成了锁的使用,并且释放了,而且此时等待队列非空时,如果这是有新线程申请锁,那么,公 平锁和非公平锁的表现就会出现差异。
公平锁
优点:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占 用,导致得不到CPU执行。
缺点:整体吞吐效率相对非公平锁要低,等待队列中除一个线程以外的所有线程都会阻塞,CPU唤醒线程的开 销比非公平锁要大。
非公平锁
优点:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高。 缺点:在高并发环境下可能造成线程优先级反转和饿死现象。
AQS作为并发编程的框架,为很多其他同步工具提供了良好的解决方案。下面列出了JUC中的几种同步工具, 大体介绍一下AQS的应用场景:
结束语
感谢大家能够做我最初的读者和传播者,请大家相信,只要你给我一
份爱,我终究会还你们一页情的。
欢迎大家关注我的公众号【左耳君】,探索技术,分享生活
哦对了,后续所有的文章都会更新到这里
https://github.com/DayuMM2021/Java