AQS概述:
全称是AbstractQueuedSynchronizer,是一个阻塞式锁和相关的同步器工具的框架
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 提供了条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
使用AQS实现一个不可重入锁
先手敲一个可重入锁的实现会对AQS有更好的理解
自定义同步器
static class MySync extends AbstractQueuedSynchronizer{
@Override
//尝试获得锁
protected boolean tryAcquire(int arg) {
//保证修改是原子性的
if(compareAndSetState(0,1)){
//加上了锁,并设置owner为当前线程
setExclusiveOwnerThread(Thread.currentThread());
}
return false;
}
@Override
//尝试解锁
protected boolean tryRelease(int arg){
setExclusiveOwnerThread(null);
//先setnull再setState,原因是State是volatile修饰的变量,可以保证前面的代码不被重排序
setState(0);
return true;
}
@Override//是否持有独占锁
protected boolean isHeldExclusively() {
return getState()==1;
}
//条件变量
public Condition newCondition(){
return new ConditionObject();
}
}
自定义锁
class MyLock implements Lock{
private MySync sync = new MySync();
@Override//加锁,多次尝试加锁
public void lock() {
sync.acquire(1);
}
static class MySync extends AbstractQueuedSynchronizer{...}
@Override//加锁,可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override//尝试加锁,只试一次
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override//尝试加锁,带超时
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override//解锁
public void unlock() {
sync.release(1);
}
@Override//创建条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
测试代码:
MyLock lock = new MyLock();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
sleep(1);
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t2").start();
输出结果:
22:29:28.727 c.TestAqs [t1] - locking...
22:29:29.732 c.TestAqs [t1] - unlocking...
22:29:29.732 c.TestAqs [t2] - locking...
22:29:29.732 c.TestAqs [t2] - unlocking...
AQS的设计思想
获取锁的逻辑:
while(state 状态不允许获取) {
if(队列中还没有此线程) {
入队并阻塞
}
}
当前线程出队
释放锁的逻辑
if(state 状态允许了) {
恢复阻塞的线程(s)
}
要点
- state 设计
- state 使用 volatile 配合 cas 保证其修改时的原子性
- state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
- 阻塞恢复设计
- 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到
- 解决方法是使用 park & unpark 来实现线程的暂停和恢复,先 unpark 再 park 也没 问题
- park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
- park 线程还可以通过 interrupt 打断
- 队列设计
- 使用了 FIFO 先入先出队列,并不支持优先级队列
- 设计时借鉴了 CLH 队列,它是一种单向无锁队列,基于CAS实现了无锁,有快速和无阻塞的特点
部分源码解读
成员属性
state就是用于判断共享资源是否正在被占用的标记位, 使用volatile修饰可以保证state在线程之间可见,也就是说当一个线程修改了state的值,其他线程都能及时读到最新的state的值,然后因为使用AQS框架需要支持独占和共享模式,共享模式可以多个线程去加锁,所以就需要设置state为int值以代表加锁的线程数。
AQS为了对等待线程进行管理,维护了一个FIFO的双向链表,这里的head和tail分别是头结点和尾结点。这里的注释意思是懒惰地维护了一个双向链表,只有在需要用到双向链表的时候才创建。
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;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
内部类Node,Node主要存储了线程对象(Thread),以及节点在队列中的等待状态(waitStates),前后指针(prev,next)等信息。其中waitStates是一个枚举类型的值,主要包括四个状态:
- 0,节点默认的初始化值,或者已经释放锁
- 1,CANCELLED,表示当前线程对锁的获取已经取消了
- -1,SIGNAL表示当前节点的后续节点需要被唤醒
- -2,CONDITION表示当前节点等待一个Conditon条件唤醒
- -3,PROPAGATE表示共享模式下的传递锁
里面的方法predecessor方法就是获取前置的一个Node
方法解读
从一个对象加锁的流程来分析整个源码,调用lock方法,会调用里面的acquire方法,下面对acquire进行解读
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法被调用,首先会尝试调用tryAcquire方法,tryAcquire方法会尝试去获得锁,如果获得失败,那么将会返回false,返回false之后会调用addWaiter方法,将当前线程放入等待队列。
然后是将线程放入等待队列的addWaiter方法,总体概括就是,使用CAS的方法原子性地将当前节点添加到队列尾部。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 以下几行代码想把当前node加到链表的最后面去,也就是进到阻塞队列的最后
Node pred = tail;
// tail!=null => 队列不为空(tail==head的时候,其实队列是空的,不过不管这个吧)
if (pred != null) {
// 将当前的队尾节点,设置为自己的前驱
node.prev = pred;
// 用CAS把自己设置为队尾, 如果成功后,tail == node 了,这个节点成为阻塞队列新的尾巴
if (compareAndSetTail(pred, node)) {
// 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,
// 上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了
pred.next = node;
// 线程入队了,可以返回了
return node;
}
}
// 仔细看看上面的代码,如果会到这里,
// 说明 pred==null(队列是空的) 或者 CAS失败(有线程在竞争入队)
enq(node);
return node;
}
结局有两种,一种是队列为空,加入失败,一种是CAS失败,有线程正在竞争入队,那么接下来就会把节点穿给enq方法去把这个节点处理。
如果队列为空,enq方法会创建一个新的队列,然后再通过自旋的方式多次尝试把这个节点加入队列之中。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 之前说过,队列为空也会进来这里
if (t == null) { // Must initialize
// 初始化head节点
// 细心的读者会知道原来 head 和 tail 初始化的时候都是 null 的
// 还是一步CAS,你懂的,现在可能是很多线程同时进来呢
if (compareAndSetHead(new Node()))
// 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
// 这个时候有了head,但是tail还是null,设置一下,
// 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
// 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return
// 所以,设置完了以后,继续for循环,下次就到下面的else分支了
tail = head;
} else {
// 下面几行,和上一个方法 addWaiter 是一样的,
// 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
经过addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,意味着上面这段代码将进入selfInterrupt(),所以正常情况下,下面应该返回false,这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
// 注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列
// 所以当前节点可以去试抢一下锁
// 这里我们说一下,为什么可以去试试:
// 首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,
// enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程
// 也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,
// tryAcquire已经分析过了, 忘记了请往前看一下,就是简单用CAS试操作一下state
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 到这里,说明上面的if分支没有成功,要么当前node本来就不是队头,
// 要么就是tryAcquire(arg)没有抢赢别人,继续往下看
//关键的就是这个逻辑
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 什么时候 failed 会为 true???
// tryAcquire() 方法抛异常的情况
if (failed)
cancelAcquire(node);
}
}
. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞,如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1, 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false,shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败 。然后接着循环 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true 进入 parkAndCheckInterrupt,这个线程park住。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取上一个节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) {
// 上一个节点都在阻塞, 那么自己也阻塞好了
return true;
}
// > 0 表示取消状态
if (ws > 0) {
// 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 这次还没有阻塞
// 但下次如果重试不成功, 则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}