3.深入理解AQS、ReentrantLock
3.1AQS
3.1.1AQS简介
AQS即队列同步器AbstractQueuedSynchronizer(后面简称AQS)是实现锁和有关同步器的一个基础框架。
在JDK5中,Doug Lea在并发包中加入了大量的同步工具,例如==重入锁(ReentrantLock)、读写锁(ReentrantReadWriteLock)、信号量(Semaphore)、CountDownLatch(倒计时锁)==等,都是基于AQS的。
其内部通过一个被标识为volatile的名为state的变量来控制多个线程之间的同步状态。多个线程之间可以通过AQS来独占式或共享式的抢占资源。
基于AQS,可以很方便的实现Java中不具备的功能。
例如,在锁这个问题上,Java中提供的是synchronized关键字,用这个关键字可以很方便的实现多个线程之间的同步。但这个关键字也有很多缺陷,比如:
- 不支持超时的获取锁:一个线程一旦没有从synchronized上获取锁,就会卡在这里,没有机会逃脱。所以通常由synchronized造成的死锁是无解的。
- 不可响应中断。
- 不能尝试获取锁。如果尝试获取时没获取到,立刻返回,synchronized不具备这一特性。
而ReentrantLock基于AQS将上述几点都做到了。
3.1.2核心结构
从AbstractQueuedSynchronizer的名字可以看出,AQS中一定是基于队列实现的(Queue)。
在AQS内部,是通过链表实现的队列。
链表的每个元素是其内部类Node的一个实现。然后AQS通过实例变量head指向队列的头,通过实例变量tail指向队列的尾。
其源码定义如下:
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
static final class Node {
/** 标识为共享式 */
static final Node SHARED = new Node();
/** 标识为独占式 */
static final Node EXCLUSIVE = null;
/** 同步队列中等待的线程等待超时或被中断,需要从等待队列中取消等待,进入该状态的节点状态将不再变化 */
static final int CANCELLED = 1;
/** 当前节点的后继节点处于等待状态,且当前节点释放了同步状态,需要通过unpark唤醒后继节点,让其继续运行 */
static final int SIGNAL = -1;
/** 当前节点等待在某一Condition上,当其他线程调用这个Conditino的signal方法后,该节点将从等待队列恢复到同步队列中,使其有机会获取同步状态 */
static final int CONDITION = -2;
/** 表示下一次共享式同步状态获取状态将无条件的传播下去 */
static final int PROPAGATE = -3;
/* 当前节点的等待状态,取值为上述几个常量之一,另外,值为0表示初始状态 */
volatile int waitStatus;
/* 前驱节点 */
volatile Node prev;
/* 后继节点 */
volatile Node next;
/* 等待获取同步状态的线程 */
volatile Thread thread;
/* 等待队列中的后继节点 */
Node nextWaiter;
// ...
}
(1)设计模型
(2)组成部分
AQS 主要由三部分组成
- state 同步状态
- Node 组成的 CLH 队列
- ConditionObject 条件变量(包含 Node 组成的条件单向队列)。
state 用 volatile 来修饰,保证了我们操作的可见性,所以任何线程通过 getState() 获得状态都是可以得到最新值,但是 setState() 无法保证原子性,因此 AQS 给我们提供了 compareAndSetState 方法利用底层 UnSafe 的 CAS 功能来实现原子性。
(3)State关键字
对于 AQS 来说,线程同步的关键是对 state 的操作,可以说获取、释放资源是否成功都是由 state 决定的,比如 state>0 代表可获取资源,否则无法获取,所以 state 的具体语义由实现者去定义,现有的 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 定义的 state 语义都不一样。
- ReentrantLock 的 state 用来表示是否有锁资源,变量记录了锁的重入次数
- ReentrantReadWriteLock 的 state 高 16 位代表读锁状态,低 16 位代表写锁状态
- Semaphore 的 state 用来表示可用信号的个数
- CountDownLatch 的 state 用来表示计数器的值
3.1.3实现的两类队列
- 同步队列:服务于线程阻塞等待获取资源
- 条件队列:服务于线程因某个条件不满足而进入等待状态。 条件队列中的线程实际上已经获取到了资源,但是没有能够继续执行下去的条件,所以被打入条件队列并释放持有的资源,以让渡其它线程执行,如果未来某个时刻条件得以满足,则该线程会被从条件队列转移到同步队列,继续参与竞争资源,以继续向下执行。
(1)同步队列
①CLH
同步队列是基于链表实现的双向队列,也是 CLH 锁的变种。CLH 锁是 AQS 队列同步器实现的基础。
以下图为CLH的构成:
- CLH 锁是有由 Craig, Landin, and Hagersten 这三个人发明的锁,取了三个人名字的首字母,所以叫 CLH Lock。
- CLH 锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。
- CLH 队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
②Node
AQS 以内部类 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;
static final int PROPAGATE = -3;
/** 线程等待状态 */
volatile int waitStatus;
/** 前驱结点 */
volatile Node prev;
/** 后置结点 */
volatile Node next;
/** 持有的线程对象 */
volatile Thread thread;
/** 对于独占模式而言,指向下一个处于 CONDITION 等待状态的结点;对于共享模式而言,则为 SHARED 结点 */
Node nextWaiter;
// ... 省略方法定义
}
Node 在 CLH 的基础上进行了变种。
CLH 是单向队列,其主要特点是自旋检查前驱节点的 locked 状态。
而 AQS 同步队列是 双向队列,每个节点也有状态 waitStatus,而其并不是一直对前驱节点的状态自旋判断,而是自旋一段时间后阻塞让出 cpu 时间片(上下文切换),等待前驱节点主动唤醒后继节点。
waitStatus 有如下 5 中状态:
- CANCELLED = 1 表示当前结点已取消调度。当超时或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL = -1 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为 SIGNAL。
- CONDITION = -2 表示结点等待在 Condition 上,当其他线程调用了 Condition 的 signal() 方法后,CONDITION 状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE = -3 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- INITIAL = 0 新结点入队时的默认状态。
从上面的代码中可以看出,位于 CLH 链表中的线程以 2 种模式在等待资源,即 SHARED 和 EXCLUSIVE,其中 SHARED 表示共享模式,而 EXCLUSIVE 表示独占模式。
共享模式与独占模式的主要区别在于,同一时刻独占模式只能有一个线程获取到资源,而共享模式在同一时刻可以有多个线程获取到资源。典型的场景就是读写锁,读操作可以有多个线程同时获取到读锁资源,而写操作同一时刻只能有一个线程获取到写锁资源,其它线程在尝试获取资源时都会被阻塞。
③主要行为
AQS 类成员变量 head 和 tail 字段分别指向同步队列的头结点和尾结点:
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
其中 head 表示同步队列的头结点,而 tail 则表示同步队列的尾结点,具体组织形式如下图:
当调用 AQS 的 acquire 方法获取资源时,如果资源不足则当前线程会被封装成 Node 结点添加到同步队列的末端(入队),头结点 head 用于记录当前正在持有资源的线程结点,而 head 的后继结点就