1. 定义
AQS是Abstract Queued Sychronizier(抽象排队同步器)的简称,是并发编程中比较核心的一个组件。
这个组件是JUC(java.util.concurrent)这个包下面lock锁的一个底层实现。我们可以用它来实现这个lock底层的重入锁,就是去实现锁的竞争,这样的一个同步器。
JUC包
是Java并发工具包,提供了高效、线程安全的数据结构和并发工具,帮助开发者更方便地进行多线程编程,避免传统 synchronized 和 wait/notify 方式的局限性。
CLH的全称是 Craig、Landin、Hagersten,由三位计算机科学家(Craig S. Landin、Edwin Hagersten 等)的姓氏首字母组成。这是并发编程中一种经典的锁算法,全称通常写作 Craig, Landin, and Hagersten Locks。
AQS 的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH 变体队列和原本的 CLH 锁队列的区别主要有两点:
- 由 自旋 优化为 自旋 + 阻塞 :自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。
- 由 单向队列 优化为 双向队列 :在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 next 指针,成为了双向队列。
2. 提供的两种锁机制
AQS是多线程同步器,它是J.U.C包中多个组件的底层实现,比如说像lock、CountDownlatch、Semaphore都用到了AQS。从本质上来说,AQS提供了两种锁的机制,分别是排它锁和共享锁。
这也是AQS定义的两种资源共享方式:要么是独占,要么是共享。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
2.1 共享锁
共享锁也称为读锁,就是在同一个时刻允许多个线程同时获得这样一个锁的资源,比如CountDownLatch以及Semaphore都用到了AQS中的共享锁的功能。
2.2 排他锁
所谓排他锁就是存在多个线程去竞争同一共享资源的时候,同一个时刻只允许一个线程去访问这样一个共享资源。也就是说多个线程中只有一个线程去获得这样一个锁的资源。比如lock中的Reentrantlock重入锁。它的一个实现就是用到了AQS中的一个排他锁的功能。
那么AQS作为互斥锁来说,他的整体设计体系中,需要解决三个核心的问题:
- 互斥变量的设计,以及如何保证多线程同时更新互斥变量的时候,线程的安全性。
- 未竞争到锁资源的线程的等待。以及竞争到锁的资源,释放锁之后的唤醒。
- 锁竞争的公平性和非公平性。AQS采用了一个int类型的互斥变量
state
,用来记录锁竞争的一个状态。0表示当前没有任何线程竞争锁资源,而大于等于1表示已经有线程正在持有锁资源。
3. 工作原理
一个线程来获取锁资源的时候,首先会判断state
是否等于0,也就是说它是无锁状态。如果是,则把这个state
更新成1,表示占用到锁。
而这个过程中,如果存在同时多次做这样的一个操作,就会导致线程安全性问题。因此 AQS 采用了CAS机制 ,去保证state
互斥变量更新的一个原子性。未获得到锁的线程通过Unsafe类中的park方法去进行阻塞,把阻塞的线程按照先进先出的原则,去加入到双向链表的一个结构中。
当获得锁资源的线程释放锁之后,会从这样一个双向链表的头部去唤醒下一个等待的线程,再去竞争锁。
4. 锁竞争的公平性和非公平性问题
4.1 公平锁
最后关于锁竞争的公平性和非公平性的问题,AQS的处理方法是在竞争锁资源时候,公平锁需要去判断双向链表中是否有阻塞的线程,如果有的话,需要排队等待。
4.2 非公平锁
而非公平锁的处理方式是,不管双向链表中是否存在等待竞争锁的线程,那么他都会直接去尝试更改互斥变量state
去竞争锁。
假设在一个临界点,获得锁的线程释放锁,此时state等于0。而当前的这个线程去抢占锁的时候,正好可以把state修改成1,那么这个时候就表示他可以拿到锁,而这个过程是非公平的。
参考资料: