1. 定义
AbstractQueuedSynchronizer简称为AQS ,字面意思是抽象的队列同步器。技术解释是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态。
2. 为何称AQS为JUC体系的基石?
与AQS相关的包括Semaphore信号量、CyelicBarier、ReentrantReadWriteLock、CountDownLatch、ReentrantLock。
3. 作用
加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要队列。
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢? 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
4. AQS体系结构
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
AQS的int变量:AQS的同步状态State成员变量,0自由状态可以抢占;大于等于1,有线程占用,排队等待
AQS的CLH队列:CLH: Craig, Landin and Hagersten队列,是个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO)
内部类Node(Node类在AQS类内部):Node的int变量是Node的等待状态waitState成员变量,表示等候区其它线程的等待状态,队列中每个排队的个体就是一个Node
5. AQS的队列机制
双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位,真正的第一个有数据的节点,是从第二个节点开始的。
插入新节点时,其前驱节点的waitState被设为-1,自身的waitState被设为0,即队尾节点的waitState为0。如果前驱节点的waitStatus是SIGNAL状态(-1),即shouldParkAfterFailedAcquire方法会返回true,程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起
正常情况下,当锁被释放后,之前哨兵节点的waitState被设为0,且从队列中移除,将会被GC。此时队列中的队首节点去抢占锁,其节点变为新的哨兵节点继续占位。
仅在等待队列中FIFO,也就是说可能存在插队的情况(非公平锁)
6. 代码示例
以办理银行业务为例
假设A,B,C三人去银行办业务,A先到,此时窗口无人,直接办理,类比state=0,处于自由状态,谁先抢占谁就执行。
B是第2个,B一看A占用受理窗口,只能去候客区等待, 类比 state=1,B抢占后发现state还是=1,只能进入AQS队列中(用node创建链表队列,将哨兵节点的waitStatus设为-1,自身的waitStatus设为0),等待A执行完成,再次尝试去抢占。
C是第3个,C一看A占用受理窗口,只能去候客区等待,类比C只能进入AQS队列中, 但是前面是B, FIFO, 在B之后插入C(将前驱节点B的waitStatus设为-1,自身的waitStatus设为0)。
正常情况下,A释放锁后,之前的哨兵节点被GC,B去抢占锁,B所在的节点变为新的哨兵节点占位。
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
//假设A,B,C三人去银行办业务,A先到,此时窗口无人,直接办理,类比state=0,处于自由状态,谁先抢占谁就执行
//假设A办理业务虚耗20分钟,估计长期占用窗口
new Thread(()->{
reentrantLock.lock();
try {
System.out.println("------come in A");
TimeUnit.MINUTES.sleep(20);
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
},"A").start();
//B是第2个,B一看A占用受理窗口,只能去候客区等待, 类比 state=1,B先抢占发现state还是=1,只能进入AQS队列中(用node创建链表队列,将哨兵节点的waitStatus设为-1,自身的waitStatus设为0),等待A执行完成,再次尝试去抢占
//正常情况下,A释放锁后,之前的哨兵节点被GC,B去抢占锁,B所在的节点变为新的哨兵节点占位
new Thread(()->{
reentrantLock.lock();
try {
System.out.println("------come in B");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
},"B").start();
//C是第3个,C一看A占用受理窗口,只能去候客区等待,类比 C只能进入AQS队列中, 但是前面是B, FIFO, 在B之后插入C(将前驱节点B的waitStatus设为-1,自身的waitStatus设为0)
new Thread(()->{
reentrantLock.lock();
try {
System.out.println("------come in C");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
},"C").start();
}
}