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