AQS是什么
字面意思:抽象的队列同步器
在java.utils.concurrent.locks包下
AbstractOwnableSynchronizer
AbstractQueuedLongSynchronizer
AbstractQueuedSynchronizer
通常地:AbstractQueuedSynchronizer简称为AQS
技术解释
是用来构建锁或者其它同步器组件的公共基础部分的抽象实现,时重量级基础框架及整个JUC体系的基石,用于解决锁分配给谁的问题,通过一个抽象的的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态。
CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
AQS为什么是JUC内容中最重要的基石
- 和AQS有关
-
进一步理解锁和同步器的关系
锁,面向锁的使用者:定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。
同步器,面向锁的实现者:比如Java并发大神DougLee,提出统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。是一切锁和同步组件实现的公共基础部分。
AQS能干嘛
- 加锁就会导致阻塞,有阻塞就需要排队,实现排队必然需要队列。
- 抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
- AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
AQS内部体系架构
AQS自身
int 变量
- AQS的同步状态State成员变量:private volatile int state。
AQS的CLH队列
- 三个大牛的名字组成,是一个双向队列。
内部类Node
Node的等待状态waitState成员变量:volatile int waitStatus
SHARED:表示线程以共享的模式等待锁
EXCLUSIVE:表示线程正在以独占的方式等待锁
waitState代表等候区中其他线程的等待状态,队列中每个排队的个体就是一个Node
waitState的状态:
-
CANCELLED = 1:线程被取消了
-
SIGNAL = -1:后继线程需要唤醒,线程已经准备好了,等待资源释放
-
CONDITION = -2:等待condition唤醒
-
PROPAGATE = -1:共享式同步状态获取将会无条件地传播下去(当前线程处于SHARED的情况下,该字段才会使用)
-
等于0:初始值
AQS源码分析
Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的。
ReentrantLock
公平锁和非公平锁
NonfairSync和FairSync都是继承了Sync的静态内部类,Sync继承了AQS
公平锁需要判断是否有前置节点。
公平锁:讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中
非公平锁:不管是否有等待队列,如果可以获取锁,立刻占有锁对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获得锁,他还是需要参加锁竞争,后来的线程可能插队夺锁。
acquire
该方法有三条流程:
- tryAcquire
- addWaiter–>enq入队操作
- acquireQueued–>调用cancelAcquire
在双向链表中,第一个节点为虚节点,也叫哨兵节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始。
在acquireQueued方法中,还会再次调用tryAcquire来抢锁,如果争抢失败,就会进入shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法中。他们总是会将前置节点的waitStatus改为-1,然后将自己挂起LockSupport.park()。
park方法会在以下三种情况下继续向下执行:
- 被unpark
- interrupt
- 其他不合逻辑的返回
unlock
- sync.release(1)
- tryRelease(arg)
- unparkSuccessor
总结
整个ReentrantLock的加锁过程,可以分为三个阶段:
- 尝试加锁
- 加锁失败,线程入队列
- 线程入队列后,进入阻塞状态