基本结构
AQS 的基本结构包含了一个同步状态变量 state
和一个等待队列,其中state
变量用来表示同步状态,等待队列用来存储等待获取同步状态的线程。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
protected AbstractQueuedSynchronizer() { }
//内部类,Node对象代表一个等待获取锁的线程,头节点一般是当前持有锁的线程
static final class Node {
//当前线程在等待共享锁
static final Node SHARED = new Node();
//当前线程在等待独占锁
static final Node EXCLUSIVE = null;
//waitStatus 常量值,表示等待的线程被取消
static final int CANCELLED = 1;
//waitStatus 常量值。表示后继线程需要unpark
static final int SIGNAL = -1;
//waitStatus 常量值。表示线程正在Condition队列中
static final int CONDITION = -2;
//waitStatus 常量值。表示线程的acquireShared行为需要无条件的向队列的下一个节点传递。用在共享锁的场景。
static final int PROPAGATE = -3;
//节点状态 默认值是0,可选值CANCELLED、SIGNAL、CONDITION、PROPAGATE
volatile int waitStatus;
//等待队列是一个双向链表
volatile Node prev;
volatile Node next;
//等待的线程
volatile Thread thread;
/**
*如果节点再Condition等待队列中,则该字段指向下一个节点,如果节点在AQS同步队列中,则为一个
*标志位,其值为 SHARED或者null
*/
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() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
//等待队列的头节点和尾节点
private transient volatile Node head;
private transient volatile Node tail;
//锁状态,默认值0表示无锁。
private volatile int state;
// 省略其他方法和属性
}
AQS
中定义了一个 Node
类来表示等待队列中的节点,其中 waitStatus
表示节点状态,prev
和 next
表示前驱和后继节点,thread
存储线程,nextWaiter
指向线程节点在 Condition
等待队列中的下一个节点。
AQS
中的 head
和tail
分别表示等待队列的头和尾节点,state
表示同步状态,作用类似一个计数器0
表示无锁,大于0
的数值表示加了多少次锁。
AQS
派生出来的锁种类包括共享锁和独占锁。共享锁是指允许多个线程同时操作锁状态;独占锁是指任何时刻只允许一条线程操作锁状态变量。共享锁和独占锁根据获取锁的方式又分为公平锁和非公平锁。
acquire:获取独占锁
acquire()
方法传递一个int
型参数,这个参数的作用就是用来更新锁状态state
,如传递arg=1
参数表示加锁1
次。
方法体中首先会调用tryAcquire
尝试获取锁,如果获取锁失败线会调用addWaiter
方法将线程封装成一个 Node
节点加并入到等待队列中,加入队列后调用acquireQueued
方法,acquireQueued
是一个自旋方法,线程通过自旋的方式来获取锁,如果自旋获取锁失败,线程将进入阻塞状态。
public final void acquire(int arg) {
//1.尝试获取锁
if (!tryAcquire(arg) &&
//2.获取锁失败,线程将被加入到等待队列,并进入自旋
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.tryAcquire:尝试获取独占锁
tryAcquire()
方法是一个空方法,提供一个钩子给子类实现获取锁逻辑。不同场景下,获取锁的逻辑是不同的,比如春运购买候补火车票,当有人退票时12306系统就将票优先提供给先排队的用户;而电商的秒杀活动,只要用户操作,用户就有可能抢到商品。
独占锁ReentrantLock
的公平锁和非公锁的获取锁的逻辑就是通过tryAcquire
来实现。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
2.addWaiter:线程添加到AQS等待队列
获取锁失败的线程会被封装成一个Node
节点,然后通过compareAndSetTail
添加到AQS
等待队列的末尾。
private Node addWaiter(Node mode) {
//将调用addWaiter的线程封装成node节点。mode模式是EXCLUSIVE
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
//cas将node追加到等待队列的末尾
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果cas失败,将通过自旋来设置等待队列末尾的节点
enq(node);
return node;
}
并发情况下,compareAndSetTail
将节点追加到等待队列末尾不一定一次执行成功,如果发生了失败的情况,需要调用enq
方法,以自旋的方式来重试将前面的追加操作。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//等待队列第一次使用,tail和head都为null
if (compareAndSetHead(new Node()))
//初始化时,头节点是一个空节点,将头节点赋值给尾节点
tail = head;
} else {
//等待队列已经被初始化,cas 设置队列尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
3.acquireQueued:自旋获取独占锁
将线程添加到AQS
等待队列后,线程将启动自旋模式,在自旋方法中,线程如果不是头节点或者是头节点但是获取锁失败线程将进入阻塞状态。线程从阻塞状态被唤醒后,将重新进入自旋模式,重复前面的操作。
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)) {
//如果设置锁成功,则将该线程节点设置成头节点
setHead(node);
//help gc
p.next = null;
failed = false;
return interrupted;
}
//校验线程的前置节点是否被取消
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞线程,线程如果需要中断,请调用acquireInterruptibly方法
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
线程进入阻塞前,需要确保它能够被成功唤醒,所以需要调用shouldParkAfterFailedAcquire
校验一下它的前置节点是否已经被取消,并且告诉它的前置节点,有后继节点需要它来唤醒。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前置节点已经知道它有后置节点需要唤醒,返回true
return true;
if (ws > 0) {
//前置节点已经取消
do {
//找到离node节点最近的未取消的前置节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//告诉前置节点有后置节点需要它唤醒
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
release 释放独占锁
release()
方法的int
参数表示需要释放几次锁,锁状态state
的值可能大于1
,比如可重入锁ReentrantLock
。
方法体中,会先调用tryRelease
尝试释放锁,tryRelease
不一定会返回true
,比如ReentrantLock
中线程获取了几次锁,就需要释放几次。当锁全部被释放完之后,线程才会尝试唤醒等待队列中的线程。
public final boolean release(int arg) {
//1.尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//2.唤醒头节点的下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
1.tryRelease,尝试释放独占锁
tryRelease()
空方法,尝试释放锁,需要子类去实现。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
2.unparkSuccessor,唤醒线程
unparkSuccessor()
唤醒等待队列中的线程,唤醒线程前校验一下线程是否被取消。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//头节点已经完成了释放后继节点的操作
compareAndSetWaitStatus(node, ws, 0);
//如果当前节点的后继节点被取消,则从尾部遍历等待队列,找到离头节点最近的一个未被取消的节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
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 获取共享锁
共享锁获取锁的流程与独占锁是一样的,线程先尝试获取锁,如果失败,线程将追加到AQS
等待队列的末尾,随后线程启动自旋,自旋获取锁失败,线程进入阻塞状态。tryAcquireShared
也是一个空方法,由它的子类来实现。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
//1.自旋获取共享锁
doAcquireShared(arg);
}
1. doAcquireShared
线程将进入自旋状态,如果线程获取锁成功,将会扩散释放锁的操作。因为在共享锁的模式下,允许多个线程操作锁状态。
private void doAcquireShared(int arg) {
//将线程追加到等待队列的末尾,与独占锁是一样的
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//线程是否是等待队列的头节点
final Node p = node.predecessor();
if (p == head) {
//尝试获取锁
int r = tryAcquireShared(arg);
//如果共享锁没有被消耗完,继续尝试唤醒其他的线程
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//自旋获取锁失败,阻塞线程,与独占锁一样的。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
如果int r = tryAcquireShared(arg);
的返回值>=0
,说明此时还有多余的共享锁供其他线程获取,所以线程可以调用setHeadAndPropagate
扩散唤醒线程的操作。
private void setHeadAndPropagate(Node node, int propagate) {
//保存旧的头节点
Node h = head;
//设置新的头节点
setHead(node);
//h== null 只是一个条件判断,只有在队列没有初始化的条件下,h 才可能为null,目前的共享锁体系中没有为null的情况,不代表未来没有。
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
//s == null 说明node是最后一个节点
doReleaseShared();
}
}
共享锁允许多个线程同时操作锁状态属性state
,所以线程释放锁后,也有可能需要唤醒多个阻塞线程。考虑到提高唤醒线程的效率,在共享锁中,以一种扩散、线程自旋的方式来唤醒阻塞线程,如:t1
唤醒了t2
后,t1
继续自旋,并且t2
加入到自旋来唤醒阻塞,以此类推。自旋线程跳出循环的依据是头节点是否改变过if (h == head) break;
,只要阻塞线程唤醒并获取锁成功后就会将自身设置为头节点,所以如果头节点没有改变过说明已经没有新的阻塞线程被释放过,也不说明线程不需要自旋了。
private void doReleaseShared() {
// 自旋尝试唤醒线程
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//SIGNAL 说明头节点有需要唤醒的后继线程,
if (ws == Node.SIGNAL) {
//将线程的状态设置成0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒线程,独占锁和共享锁是一样的。
unparkSuccessor(h);
}
//线程释放锁后,但是头节点的状态还是0,需要提醒其他线程尝试唤醒线程
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
releaseShared 释放共享锁
tryReleaseShared
是一个空方法,由子类来实现。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//唤醒线程,参考上面的分析
doReleaseShared();
return true;
}
return false;
}
总结
AQS
提供了一套加锁、释放锁的流程模版,并且派生出共享锁和独占锁。共享锁是指允许多个线程同时操作锁状态;独占锁是指任何时刻只允许一条线程操作锁状态变量。共享锁和独占锁根据获取锁的方式又分为公平锁和非公平锁。
独占锁获取锁时,首先调用tryAcquire
尝试获取锁,如果获取锁失败,那么线程会被封装成Node
节点,添加到AQS
等待队列等待它的前置线程唤醒。当线程执行完任务后,会调用tryRelease
释放锁,最后通过next
找到后置节点唤醒它。
共享锁和独占锁的获取逻辑基本是一样的,先尝试获取锁,如果获取锁失败也是添加到等待队列并进行自旋。相比于独占锁,共享锁的区别在于线程获取锁成功后,会继续尝试去唤醒线程,如:t1唤醒t2,t1、t2继续尝试唤醒其他线程,以一种扩散的方式唤醒其他线程。
在不同的场景下,加锁和解锁的方式可能不一样的,如AQS
提供独占锁的tryAcquire
和tryRelease
两个空方法给子类覆盖使用来实现自己获取锁的逻辑。