背景
AQS(AbstractQueuedSynchronizer)抽象的同步队列,内部通过维护一个volatile int state (共享资源)和一个FIFO等待队列。获取和释放同步队列的方法有子类负责实现。
AQS是一个抽象类,里面所有方法都默认实现,子类只需要重写具体方法即可。
框架
AQS定义两种资源获取方式:EXCLUSIVE(ReentrantLock)和SHARED(Semaphore和CountDownLatch等)。
不同的同步器实现时只需要实现共享资源的获取和释放方式即可,至于等待队列的维护AQS已经在顶层实现好了。自定义同步器可以根据需要实现如下方法:
isHeldExclusively:是否独占资源,用到conditions才需要实现它。
tryAcquire:独占方式获取锁,成功返回true,失败返回false。
tryRelease:独占方式释放锁,成功返回ture,失败返回false。
tryAcquireShared:共享方式获取锁,返回值等于0,表示当前线程获取锁成功,但不能继续唤醒后继线程。返回值小于0,表示没有资源可用。返回值大于0,表示当前线程获取锁成功,可以继续唤醒后继线程。
tryReleaseShared:共享方式释放锁,在CountDownLatch中状态值减为0的时候返回true,在Semaphore实现中当前线程释放成功则返回true。
以ReentrantLock为例,state初始为0,当有线程获取获取到锁时,会将state加1,表示为锁定状态。其它线程在获取就会失败,只有当前获取到锁的线程执行了unlock才会释放锁,state为0,其它线程才有机会获取锁。
源码
acquire(int arg)
独占模式获取锁的顶层入口,由AQS内部实现。如果获取到资源,线程直接返回,否则会进入等待队列,整个过程忽略中断。
public final void acquire(int arg) {
/*
tryAcquire 独占模式尝试获取锁,模板方法,有具体子类实现
addWaiter 创建node节点
acquireQueued 执行入队操作,该方法里面会尝试在获取一次锁,若获取失败,会被挂起
selfInterrupt 补中断
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
代码执行流程:
- 调用tryAcquire尝试获取锁,成功返回,失败执行acquireQueued
- 调用addWaiter创建node节点,标记为EXCLUSIVE,且加入到队列的尾部
- 调用acquireQueued,如果前驱为head,会在尝试获取锁,如果不是,找到安全点之后将自己挂起 在此过程中如果外部线程执行了中断,是不理会的,只有获取到资源之后才会自我中断,把中断补上。
tryAcquire(int arg)
尝试获取锁,成功返回true,失败返回false
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
初学者看到这里可能会有疑问,既然是空实现为啥不定义成abstract方法?
原因是:如果实现独占锁,只需要实现tryAcquire/tryRelease,如果实现共享锁,只需要实现tryAcquireShared/tryReleaseShared,如果定义成抽象方法,每个实现类都需要实现其它模式的接口。
addWaiter(Node mode)
把当前线程包装成Node,并加入到队列的尾部。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //创建node节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // 尝试快速加入尾部
pred.next = node;
return node;
}
}
enq(node); // 自旋加入尾部,直到加入尾部才会退出
return node;
}
enq(final Node node)
private Node enq(final Node node) {
for (;;) { // 自旋
Node t = tail;
if (t == null) { // 队列为空,也说明head和tail的创建时延迟创建,如果没有竞争,head和tail都是null
if (compareAndSetHead(new Node())) // 创建个空节点,并让head指向它
tail = head;
} else { // 放入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
第一次循环:
刚开始初始化,head是null,tail是null
创建个空节点new Node() head、tail同时指向 new node()
第二次循环:
把node的prev指向new Node(),node cas成tail,此时tail指向node
上一个tail的next指向node
因为cpu执行是分片执行,在某个时刻会造成双向链表不完整的情况:
时刻1:node.prev = t
时刻2:cas 成功
时刻3:执行其它功能
会造成node.next为null的情况
acquireQueued(final Node node, int arg)
通过tryAcquire获取资源失败,addWaiter加入队尾之后,下一步需要正式休息了,不过在休息之前,还需要做一些事情,要告知自己的前驱,前驱释放锁之后记得通知自己一下,做完这步就会真正去休息了。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 记录是否发生错误
try {
boolean interrupted = false; // 是否执行了中断
for (;;) {
final Node p = node.predecessor(); // 获取前驱
if (p == head && tryAcquire(arg)) { // 如果是head且尝试获取锁成功,tryAcquire 模板方法,有具体子类实现
setHead(node); // 设置head,同一时刻只有一个线程获取锁成功,这里setHead 是线程安全的,且一定能成功
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); // 取消获取
}
}
shouldParkAfterFailedAcquire(Node pred, Node node)
此方法是检查状态,看看是否真正可以去休息了,找到一个前驱是非取消的状态。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 获取前驱状态
if (ws == Node.SIGNAL) // signal 可以挂起操作了
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev; // 如果前驱节点取消,会一直向前找,直到找到一个正常等待的节点
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);// 把前驱节点置成SIGNAL,意思就是释放锁之后记得唤醒我
}
return false;
}
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
parkAndCheckInterrupt()
如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 挂起
return Thread.interrupted(); // 返回当前线程中断状态,interrupted会消除中断标志位
}
简单总结下获取资源流程:
- tryAcquire()资源,成功返回true,失败返回false
- 如果获取失败,执行addWaiter,并将当前线程封装成node节点,加入队列的尾部
- acquireQueued 使线程在队列中休息,休息之前在检查一次自己的前驱是否是head(代表当前获取锁的线程),如果是head,并且获取锁成功,会把自己设置成head,如果前驱不是head,会执行shouldParkAfterFailedAcquire
- shouldParkAfterFailedAcquire 循环找到一个前驱是非取消的节点,并把前驱的状态设置成SIGNAL(释放锁记得通知我)
- parkAndCheckInterrupt 挂起线程
以上就是互斥锁获取锁的线程,下面说下互斥锁的释放过程:
release(int arg)
互斥锁的释放顶层方法是release,release源码:
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁,模板方法,有子类具体实现
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继线程
return true;
}
return false;
}
tryRelease(int arg)
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
unparkSuccessor(Node node)
该方法的主要作用是,找到后继节点,并唤醒后继节点,使后继节点继续去争抢锁。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; // 获取当前节点状态,当前node节点就是获取锁的节点
if (ws < 0) // 如果ws状态小于0,置成0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next; // 后继节点
if (s == null || s.waitStatus > 0) {
/*
如果后继节点已经取消,就从tail向前找,找到一个正常的节点,此处之所以从tail向前遍历,就是end方法加入队列的时候,
某个时刻某个节点没有后继节点,但前驱节点都是有的
*/
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒线程
}
以上代码是互斥锁的获取和释放,相对来说还是比较简单的,下面会接着介绍共享锁的获取和释放过程。
acquireShared(int arg)
共享锁获取的顶层方法,对中断不敏感。
public final void acquireShared(int arg) {
/*
tryAcquireShared 模板方法,有子类负责实现
< 0 没有可用资源,入队列等待
*/
if (tryAcquireShared(arg) < 0) // 尝试获取锁
doAcquireShared(arg);
}
/**
* 共享模式获取锁 模板方法,实现类实现
* 返回值:
* 小于0 获取锁失败
* 等于0 获取锁成功,但不能继续向下传播
* 大于0 获取锁成功,可以继续向下传播
* @param arg
* @return
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
tryAcquireShared 尝试获取锁,成功则直接返回,失败执行doAcquireShared加入等待队列,直至获取锁为止。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); // 创建等待节点 node
boolean failed = true; // 是否发生错误
try {
boolean interrupted = false; // 外部是否执行中断操作,此处只是记录中断标志,当该线程获取锁之后,才会补中断操作,设置中断标识
for (;;) {
final Node p = node.predecessor(); // 前驱
if (p == head) { // 若前驱是head,head表示获取锁的线程,只有前驱是head节点,才有机会去获取锁
int r = tryAcquireShared(arg); // 尝试获取锁,模板方法,有具体子类实现
/*
r 有三种情况:
返回负数,表示没有空闲锁
返回0,表示当前线程获取锁成功,但不能继续向下传播(不可以唤醒后驱节点)
返回大于0,表示当前线程获取锁成功,可以继续向下传播
tryAcquireShared 模板方法,有子类根据实际情况自己实现
*/
if (r >= 0) {
setHeadAndPropagate(node, r); // 设置heaed并且继续向下传播
p.next = null; // help GC
if (interrupted) // 如果在park过程中,该线程被打断,就补中断异常
selfInterrupt(); // 自己打断自己
failed = false;
return;
}
}
/*
走到此处,说明当前节点的前驱节点不是head,会真正执行挂起操作
shouldParkAfterFailedAcquire 找安全点休息(会把当前节点的前驱waitstaus置成SIGNAL,此状态说明当前驱节点释放锁,负责唤醒后驱节点)
parkAndCheckInterrupt 挂起操作,且当被唤醒的时候返回中断状态
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); //执行过程中如果被中断或者发生错误会取消改节点
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below 把当前head封锁到当前栈中,后面会使用
setHead(node); // 设置head节点,把head执行node节点
/ * 时刻1:t3调用releaseShared -> doReleaseShared -> unparkSuccessor,完了之后head的等待状态为0
* 时刻2:t1由于t3释放了信号量,被t3唤醒,调用Semaphore.NonfairSync的tryAcquireShared,返回值为0
* 时刻3:t4调用releaseShared,读到此时h.waitStatus为0(此时读到的head和时刻1中为同一个head),将等待状态置为PROPAGATE
* 时刻4:t1获取信号量成功,调用setHeadAndPropagate时,可以读到h.waitStatus < 0,从而可以接下来调用doReleaseShared唤醒t2
*/
/*
如果propagate传播值大于0 才会继续向下传播
head 为null 说明没有竞争
h.waitStatus 分为两种情况:
1:SIGNAL
2: PROPAGATE
*/
if (propagate > 0 || h == null || h.waitStatus < 0) {
/*
node.next 为null并不是真正null,在中间时刻有可能双向链表处于断档状态,具体看end创建节点方法
若不为null,后驱节点是共享节点 也执行唤醒操作
*/
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared(); // 释放共享锁
}
}
自己获取到锁之后,如果还有剩余资源,还需要唤醒后继节点,这就是共享锁和独占锁的区别。
private void doReleaseShared() {
/*
private static class Thread1 extends Thread {
@Override
public void run() {
sem.acquireUninterruptibly();
}
}
private static class Thread2 extends Thread {
@Override
public void run() {
sem.release();
}
}
Thread t1 = new Thread1();
Thread t2 = new Thread1();
Thread t3 = new Thread2();
Thread t4 = new Thread2();
*/
/*
head -> t1的node -> t2的node(也就是tail)
信号量释放的顺序为t3先释放,t4后释放:
时刻1: t3调用releaseShared,调用了unparkSuccessor(h),head的等待状态从-1变为0
时刻2: t1由于t3释放了信号量,被t3唤醒,调用Semaphore.NonfairSync的tryAcquireShared,返回值为0
时刻3: t4调用releaseShared,读到此时h.waitStatus为0(此时读到的head和时刻1中为同一个head),不满足条件,因此不会调用unparkSuccessor(h)
时刻4: t1获取信号量成功,调用setHeadAndPropagate时(执行setHead之前,head的值还是时刻1的head),因为不满足propagate > 0(时刻2的返回值也就是propagate==0),从而不会唤醒后继节点
加上PROPAGATE(传播)之后的情况:
信号量释放的顺序为t3先释放,t4后释放:
时刻1: t3调用releaseShared,调用了unparkSuccessor(h),head的等待状态从-1变为0
时刻2: t1由于t3释放了信号量,被t3唤醒,调用Semaphore.NonfairSync的tryAcquireShared,返回值为0
时刻3: t4调用releaseShared,读到此时h.waitStatus为0(此时读到的head和时刻1中为同一个head),不满足条件,因此不会调用unparkSuccessor(h),
会执行ws=0 把当前head的waitStatus状态改为向下传播
时刻4: t1获取信号量成功,调用setHeadAndPropagate时(执行setHead之前,head的值还是时刻1的head),因为不满足propagate > 0(时刻2的返回值也就是propagate==0),从而不会唤醒后继节点,
但满足 h.waitStatus < 0的条件,因此会唤醒t2线程
也就是说,上面会产生线程hang住bug的case在引入PROPAGATE后可以被规避掉。在PROPAGATE引入之前,之所以可能会出现线程hang住的情况,就是在于
releaseShared有竞争的情况下,可能会有队列中处于等待状态的节点因为第一个线程完成释放唤醒,第二个线程获取到锁,但还没设置好head,又有新线程释放锁,但是读到老的head状态为0导致释放但不唤醒,最终后一个等待线程既没有被释放线程唤醒,也没有被持锁线程唤醒。
*/
for (;;) {
// head -> A
Node h = head; // 从head节点开始唤醒,head节点就代表当前时刻获取到锁的节点
if (h != null && h != tail) { // 至少两个节点
int ws = h.waitStatus; // head节点的状态
if (ws == Node.SIGNAL) { // 如果为signal,负责唤醒后继节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 把head waitStatus置成初始状态
continue; // loop to recheck cases
unparkSuccessor(h); // 唤醒后继线程
}
/*
如果ws为0,说明多个释放锁的线程拿到的head是同一个,为保证队列的活跃性把当前head指向的节点 waitStatus置为PROPAGATE
这样其它线程能够继续传播,唤醒节点
*/
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// head -> B
if (h == head)// loop if head changed 如果head没有改变退出循环,否则一直循环
break;
}
}
简单总结一下共享锁获取流程:
1: 尝试获取锁,成功返回,失败调用doAcquireShared加入等待队列,等待被唤醒
2:被唤醒之后调用setHeadAndPropagate,如果还有剩余资源,还要负责唤醒后继节点