回答
AQS
是 AbstractQueuedSynchronizer 简称,它是 J.U.C 包中多个组件的底座,如 ReentrantLock
、CountDownLatch
、Semaphore
、CyclicBarrier
都是基于 AQS 来实现的。
AQS
底层利用 volatile 类型的 int state 来表示同步状态,该字段的含义取决于实现的同步器,如在ReentrantLock
中,state 表示持有锁的数量,在 Semaphore
中,state 表示可用的许可证数量。当线程来获取同步锁时,如果 status = 0,说明目前没有任何线程占有锁,该线程可以获得锁并设置 state = 1。如果 state > 0,则说明有线程正在持有锁,则线程必须加入同步队列进行等待(不考虑重入)。
AQS
内部还维护着一个 FIFO 的双向同步队列,该队列通过 Node 的实例来构建的,每个 Node 代表一个等待获取资源的线程。获取锁失败的线程都会被包装成一个 Node 节点加入队列中并进入阻塞状态。
AQS 有两种锁机制:排他锁和共享锁。
- 排他锁:同一个时间只能有一个线程访问该共享资源,为独占。
- 共享锁:允许多个线程同时访问共享资源,一般来说它是读锁。
详解
AQS 设计
先看 AQS 有哪些重要属性:
// 头结点
private transient volatile Node head;
// 尾节点
private transient volatile Node tail;
// AQS 中最重要的变量。它表示当前锁资源的状态
// = 0 时,表示当前锁没有被占用
// > 0 时,表示有线程持有锁
// state 可以大于 1,因为锁支持可重入
private volatile int state;
// 当前持有所有的线程
// 这个变量位于 AbstractOwnableSynchronizer 中,为了更好地理解,大明哥将其放到这里来了
private transient Thread exclusiveOwnerThread;
AQS 内部有一个 FIFO 的同步队列,即 CLH 同步队列,如果某个线程获取锁失败,则会被包装为一个 Node 节点加入到 CLH 同步队列中,同时被阻塞。其结构如下:
head
:CLH 队列的头结点,我们可以认为它就是持有锁的当前线程的节点。而且,head 节点是延迟初始化的,只有它在首次使用时才会被创建。有两点需要注意:- 阻塞队列不包含 head 节点
- 如果 head 节点存在,那么它的
waitStatus
一定不会是CANCELLED
。这说明 head 不会表示一个已取消其等待状态的线程。
tail
:CLH 队列的尾节点,当某个线程获取锁失败时,会被加入到 CLH 队列的尾部,同时 tail 会指向这个节点。
每一个等待的线程都会被包装成一个 Node 实例:
static final class Node {
/** 标识节点当前处于共享模式下 */
static final Node SHARED = new Node();
/** 标识节点当前处于独占模式下 */
static final Node EXCLUSIVE = null;
/** 表示此线程因为超时或中断而不再需要争夺这个锁了 */
static final int CANCELLED = 1;
/** 表示当前节点的线程释放或取消时,应该通知后继节点的线程 */
static final int SIGNAL = -1;
/** 表示节点在条件队列中,与条件变量相关联 */
static final int CONDITION = -2;
/** 表示下一个 acquireShared 应该无条件地传播 */
static final int PROPAGATE = -3;
/* 节点状态,有上面四个值(1、-1、-2、-3) */
volatile int waitStatus;
/* 前驱节点 */
volatile Node prev;
/* 后继节点*/
volatile Node next;
/* 包装的线程*/
volatile Thread thread;
/* 这个字段用于链接下一个等待在条件上的节点 ,与 next 意义一致,但是它用于条件队列*/
Node nextWaiter;
}
下图是 AQS 的模型图:
AQS 底层采用模板方法模式,子类需要实现对共享成员变量 state 的获取和释放,即获取锁和释放锁。至于 CLH 队列的维护、线程的阻塞和唤醒由 AQS 实现。