首页 文章详情

学Java的竟然有人不会AQS机制

大鱼仙人 | 264 2021-12-13 07:38 0 0 0
UniSMS (合一短信)


Java中的并发包大家应该都或多或少的了解过,说到并发包也就不得不提我们今天要说的AbstractQueuedSynchronizer,简称AQS,这个是很多并发工具类的实现基础

 

 public abstract class AbstractQueuedSynchronizer     extends AbstractOwnableSynchronizer     implements java.io.Serializable

 

类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLockSemaphoreCountDownLatch


                             


深入探究AQS 




先来看这个图,图中有颜色的为Method,无颜色的为Attribution

 

总的来说,AQS框架共分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据

 

当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程

 

道理也很简单,就像我们说的,这个东西是一个抽象的同步器,它将加锁和解锁这些操作交给了具体的实现类来自己实现,就像这样


 

当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,获取锁成功之后便直接执行相应的逻辑,对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层

 

这样给大家说的话,应该很容易就可以理解了

 

AQS的实现数据结构

 

研究过AQS的同学应该对这个图都很熟悉了,AQS的核心就是state+Node+CLH变体双向队列

 

核心思想就是通过一个volatile类型state状态来表示共享资源的状态,如果被请求的资源空闲,就将获得共享资源的线程设置为当前有效的线程,然后修改state为锁定状态,其它的线程及时可见

 

共享资源被占用之后,其它线程肯定不能直接就返回失败啊,这样这个并发包的高效就没得了,所以就引入了一个双向队列,这个双向等待队列放置那些暂时还未抢到共享资源的线程,来完成等待唤醒机制

 

实际上,AQS的运行中的这个CLH变体的双向队列,不知存储未抢到共享资源的线程,而抢到共享资源的这个线程也会作为队列的头节点head存在

 

CLH:CraigLandin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。


               

 

这么说大家应该就很容易懂了吧,就是大家一起抢共享资源,抢到的就是有效线程,放到双向队列的head头节点,没抢到的就依次往后排

 

我们接着看一下Node节点是怎么做的

 


这个是Node节点的属性值和含义

 

简单解释一下,waitStatus就是节点在队列中的状态,Thread就是当前节点的线程,prevnext是前驱指针和后继指针

 

这里的重点就是waitStatus属性



CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。


SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL


CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Conditionsignal()方法后,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











good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter