AQS 详解及源码分析
1.概述
AQS 是什么?全称为 AbstractQueuedSynchronizer,是 JDK 中的一个抽象类。首先我们看看继承它的有哪些类:
基本上所有 JUC 并发包中的类都和它有关系,AQS 是用来构建锁或者其他同步器组件(读写锁等)的重量级基础框架及整个JUC体系的基石,通过内置的 FIFO 队列来完成对资源获取线程的排队工作,并通过一个 int 类型变量表示持有锁的状态。
通过 AQS 可以维持对共享资源的并发操作。
2.构成
通过这个 UML 图,可以得知 AQS 的来源和内部的组成,AQS 抽象类中还维护了两个内部类:Node 和ConditionObject 这两个内部类对于共享资源的维护其到关键的作用。Node(1) 用于数据封装、COnditionObject 用户状态维护,可以初步的认为 AQS 是一个由同步器和双向链表组成的 FIFO 队列。
如下图:
state 维护了对共享资源的状态,通过状态的定义,可以描述共享资源的状态,是被线程持有还是可以获取等状态。并且对 state 的更改具有原子性。
(1):这里是一个双向链表队列 FIFO,将线程封装成结点加入到这个队列中,Node 由 waitstatus + 前后指针组成,waitstatus 用来标识在这个队列中线程的一个等待状态。
3.LockSupport
3.1 概述
研究 AQS 的源码前,需要先了解 LockSupport,用于创建锁和其他同步类的基本线程阻塞原语,线程等待唤醒机制的升级版。
LockSupport 类使用了一种名为 permit(许可)的概念来做到阻塞和唤醒线程的功能,每一个线程都有一个许可,许可(permit)只有两个值1和0,每一个线程默认为0。
可以把许可看成是一种(0, 1)信号量(semaphore),但与 Semaphore(JUC 的一个辅助同步类)不同的是,许可的累加的上线为1。
JUC 中的辅助同步工具 Semaphore 中的许可是可以累加的,而 LockSupport 最多为1,无论发了多少张给指定线程
3.2 等待和唤醒方式
- 使用 Object 类中的 wait() 方法让线程阻塞,notify()、allnotify() 唤醒阻塞线程
- 使用 JUC 包中的 ReentrantLock 类中的 Condition 的 await() 方法让线程阻塞,single() 方法通知唤醒具体线程
- 使用 LockSupport 类可以阻塞当前线程和唤醒指定被阻塞线程 park() 和 unpark()
3.3 park() 和 unpark()
3.3.1 park()
当线程默认调用 park() 时,因为当前 permit 的默认值为0,那么当前线程就会阻塞,直到其他线程(unpark)将当前线程的 permit 设置为1时,阻塞线程才会被唤醒。然后将消耗掉一个 permit。
底层通过 unsafe 类实现(操作系统原语)
3.3.2 unpark()
调用 unpark() 方法后,指定线程的 permit 许可就会加1,但是多次调用是不会使 permit 增加(不会累加),会自动唤醒因park被阻塞的线程,并返回。
底层通过 unsafe 类实现(操作系统原语)
3.3.3 实例
public class LockSupportDemo {
public static void main(String[] args) {
Thread a = new Thread(() -> {
// 使线程阻塞
LockSupport.park();
// 等待被唤醒
System.out.println(Thread.currentThread().getName()+"\t 被唤醒了");
},"线程一");
// 这里我们让唤醒的线程后启动,测试是否需要先阻塞后唤醒
a.start();
Thread b = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
// unpark方法需要传递一个指定的线程
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName()+"\t 解锁线程一");
} catch (InterruptedException e) {
System.out.println(e);
}
},"线程二");
b.start