目录
一、AQS概述
AQS,全称为Abstract Queued Synchronizer,译为抽象的队列同步器。它是java.util.concurrent包中,它提供了一套完整的同步编程框架。我们常用的ReentrantLock(可重入锁)、CountDownLatch(门闩)、ReadWriteLock(读写锁)等同步器都是基于AQS实现的,它们在实现锁的过程中都是依赖AQS来完成核心的加锁/解锁逻辑的,有了AQS,这些同步器就无需关心底层线程调度的细节,只需要实现他们各自的逻辑即可,相当于AQS抽象了整体的流程,然后使用模板方法设计模式,将一些需要同步器自己实现的逻辑暴露出去,强制子类自己实现。
在AQS内部,通过一个volatile的int类型的变量state来控制全局同步状态,0表示当前没有线程占有锁,可以直接尝试加锁,非0表示锁已经被占有了,需要排队。AQS结合一个先进先出(FIFO)的等待队列,实现了排队和阻塞机制。
二、AQS类图结构和重要属性
从类图可以看出,在ReentrantLock中定义了AQS的子类Sync,Sync类是继承自AQS抽象队列同步器的,可以通过Sync实现对于加锁/解锁,并且Sync在ReentrantLock中有两个实现子类:NonfairSync(非公平锁)和FairSync(公平锁)。
AQS底层维护了一个先进先出(FIFO)的双向队列,这个队列是基于链表实现的,如果线程竞争锁失败,那么就会进入到这个同步队列中进行等待。当获得锁的线程释放锁之后,会从队列中唤醒一个线程。
双向队列是基于Node节点实现的,当线程需要入队列等待锁时,会将每个过来获取锁的线程、节点状态等信息封装成一个Node对象,进行入队操作。
Node节点结构如下:
// 将正在等待获取锁的线程封装为链表的一个节点
static final class Node {
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
/**
* 当前节点的等待状态
*/
// 线程取消:1
static final int CANCELLED = 1;
// 线程等待唤醒:-1. 当前节点在释放或取消锁时必须唤醒其后继节点
static final int SIGNAL = -1;
// 线程等待:-2
static final int CONDITION = -2;
// 传播:-3。用于共享锁,表示共享式同步状态的传递
static final int PROPAGATE = -3;
// 节点等待状态
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 节点入队的线程,node节点中存放的都是一个个Thread
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节点的状态(waitStatus)有如下四种:
- CANCELLED = 1:表示当前节点从同步队列中取消,即当前线程被取消;
- SIGNAL = -1:表示后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行;
- CONDITION = -2:表示当前节点在等待condition,也就是在condition queue中,只有使用到condition时才有;
- PROPAGATE = -3:表示下一次共享式同步状态获取将会无条件传播下去;
AQS中几个重要的属性:
// 等待队列的头节点(队头)
private transient volatile Node head;
// 等待队列的尾节点(队尾)
private transient volatile Node tail;
// AQS的同步状态,使用volatile修饰保证其可见性
private volatile int state;
// 直接操作内存的unsafe工具包,主要是用来执行CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 各个变量的内存地址偏移量
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
接下来我们基于ReentrantLock来分析AQS源码,了解其获取锁、释放锁流程。
三、AQS源码解析之加锁流程
首先编写一个ReentrantLock的简单示例:这里涉及到三个线程抢占锁。
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
System.out.println(Thread.currentThread().getName() + "获取锁...");
try {
// 模拟耗费一段时间
TimeUnit.SECONDS.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lock.unlock();
}, "线程A").start();
new Thread(() -> {
lock.lock();
System.out.println(Thread.currentThread().getName() + "获取锁...");
lock.unlock();
}, "线程B").start();
new Thread(() -> {
lock.lock();
System.out.println(Thread.currentThread().getName() + "获取锁...");
lock.unlock();
}, "线程C").start();
}
}
前面介绍到,在ReentrantLock类中,定义了一个Sync类,而Sync又继承自AQS抽象队列同步器,如下图:
先看下ReentrantLock类的构造方法:
public ReentrantLock() {
// 默认创建的是非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
// 传true的话,创建公平锁; 传false的话,创建非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
sync成员ReentrantLock提供了两种模式获取锁:
- 非公平锁:默认创建的就是非公平锁。简单理解,非公平锁,就是真正获取到锁的顺序,并不一定是申请锁时候的顺序,有可能,最后一个线程过来申请锁,却是最先成功获取锁的线程。
- 公平锁:公平锁则是严格遵循先来后到,先来申请锁资源的线程,肯定也是先获取到锁的。
这里主要分析非公平锁的独占方式获取锁/释放锁整体流程。
非公平锁的独占加锁入口:
public void lock() {
sync.lock();
}
如上图,Sync的lock()方法是一个抽象方法,由子类实现,这里是非公平锁,所以我们查看NonfairSync#lock方法。
final void lock() {
// 非公平锁,比较暴力,一上来就先执行一次CAS抢占锁,如果CAS失败,则执行acquire(1)标准获取锁流程
if (compareAndSetState(0, 1))
// 设置当前独占线程,就是当前线程(exclusiveOwnerThread)
setExclusiveOwnerThread(Thread.currentThread());
else
// 调用AQS的模板方法acquire(int arg)加锁
acquire(1);
}
基于案例代码,线程A率先执行lock.lock()来申请获取锁,所以它比较暴力,上来直接CAS修改state,因为此时没有其它线程持有锁,所以全局的同步状态state为0,CAS操作能执行成功,然后线程A就调用setExclusiveOwnerThread()方法将它作为当前独占这把锁的线程。
此时AQS的状态图如下:
我们看到,非公平锁有点不讲武德,一上来,就先直接一下CAS想把全局同步状态state改成1,这也是与公平锁的区别之一,我们对比一下公平锁的lock()方法。
显然,公平锁就比较优雅。
前面介绍到锁已经被线程A获取了,那么此时轮到线程B调用sync.lock()方法:
下面我们看一下acquire()方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
获得独占锁就是从acquire()方法开始的。这个方法中分别调用了三个方法:tryAcquire()、addWaiter()、acquireQueued()。
这个方法的主要逻辑是:
- 通过tryAcquire()尝试获取独占锁,如果成功返回true,失败返回false;
- 如果tryAcquire()失败,则会通过addWaiter()将当前线程封装成Node添加到AQS队列尾部;
- 执行acquireQueued(),将Node作为参数,通过自旋去尝试获取锁,将线程抢占不到资源后挂起;
1)、tryAcquire()
tryAcquire()是AQS中的一个模板方法,强制子类去实现,否则抛出UnsupportedOperationException异常
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
这里是非公平锁,所以我们查看NonfairSync#tryAcquire中的实现:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 执行非公平的尝试加锁
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前AQS的state同步状态的值
int c = getState();
// 如果为0, 会尝试使用CAS获取锁. 因为有可能锁刚好被别的线程释放了,所以这里尝试加锁
// 需要注意的是,非公平锁在这里不会执行hasQueuedPredecessors()判断是否有任何线程等待获取的时间比当前线程长,直接尝试加锁。
if (c == 0) {
// CAS更新state
if (compareAndSetState(0, acquires)) {
// 如果CAS成功拿到锁,则将当前线程设置为持有这把锁的线程,并返回
setExclusiveOwnerThread(current);
return true;
}
}
// 如果state不为0,说明有线程正在持有锁。这里其实是判断锁的重入。判断当前持有锁的线程是不是自己, 如果是自己的话,则直接更新state的值即可。
else if (current == getExclusiveOwnerThread()) {
// 累加state,可以理解为: 锁的重入次数加上acquires次
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 其它情况返回false
return false;
}
对照如上代码,线程B执行nonfairTryAcquire():
- a、先获取到当前线程:线程B
- b、获取到当前同步状态state = 1
- c、判断state不为0,不会执行cas操作
- d、判断当前持有锁的线程是不是当前线程,很显然,当前线程是线程B,当前持有锁是线程A;
- e、于是线程B执行nonfairTryAcquire()返回false,表示线程B获取锁失败;
线程B执行tryAcquire()返回false:
那么取反之后就是true,那么线程B还需执行下一步:addWaiter(Node.EXCLUSIVE)以独占方式加入等待队列。
2)、addWaiter()
addWaiter()整体流程为:将当前线程封装成一个Node对象,然后判断等待队列的尾节点是否为空,不为空的话,则将新创建的Node执行入队操作,修改链表指向;如果尾节点为空,则调用enq()执行入队,enq()内部是一个自旋操作,知道入队成功。
// 将当前线程根据给定模式(独占/共享)封装成Node节点,插入到排队的队列中
private Node addWaiter(Node mode) {
// 将当前线程封装为Node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 获取等待队列的尾节点
Node pred = tail;
// 如果等待队列的尾节点不为空
if (pred != null) {
// 修改队列指针: 当前Node节点的前驱节点指向等待队列的尾节点
node.prev = pred;
// CAS设置当前Node节点为新的等待队列的尾节点
if (compareAndSetTail(pred, node)) {
// 修改队列指针: 原等待队列的尾节点的后继节点指向当前Node节点
pred.next = node;
return node;
}
}
// 如果等待队列的尾节点为空, enq()执行入队操作, enq()采用自旋的方式,直到入队成功 (本方法内部采用自旋)
enq(node);
// 返回封装的Node节点
return node;
}
我们还是对照案例,这里线程B执行addWaiter(),进来判断tail尾节点是否为空,因为当前AQS的状态图如下:
此时尾节点tail为空,所以会执行enq()方法:
// 将Node节点插入到等待队列中,这里还涉及到第一次获取锁的时候,等待队列需要初始化
private Node enq(final Node node) {
// 自旋
for (;;) {
// 获取等待队列的尾节点
Node t = tail;
// 当第一个线程来获取锁的时候,此时等待队列是空的,即等待队列的尾节点为null,此时需要执行队列的初始化
if (t == null) { // Must initialize
// 直接创建了一个空的Node, 作为虚拟节点存在, 采用CAS将这个Node设置为等待队列的头节点
if (compareAndSetHead(new Node()))
// 等待队列的尾节点也指向这个空的虚拟节点.
// 即执行等待队列初始化的时候,首节点、尾节点都是指向新创建的这个虚拟Node节点
tail = head;
} else {
// 修改队列指针指向:当前Node节点的前驱节点 指向 等待队列的尾节点
node.prev = t;
// CAS设置尾部节点为当前Node节点,即当前Node节点成为等待队列新的尾节点
if (compareAndSetTail(t, node)) {
// 修改队列指针指向: 等待队列旧的尾节点的后继节点 指向 当前Node(等待队列新的尾节点)
t.next = node;
// 返回等待队列旧的尾节点
return t;
}
}
}
}
线程B进来,获取等待队列的尾节点tail,tail == null返回为true,说明此时AQS的队列还未初始化,我们需要先初始化。
这里直接通过new Node()创建了一个空的Node对象,此节点相当于一个虚拟节点,或者说是哨兵节点,然后设置为队列的头节点,并把它指向尾节点。此时AQS的状态图如下:
此时,队列已经完成了初始化,因为enq()里面是一个for (;;)自旋,所以线程B第二次执行的时候,这一次尾节点tail已经不为空了,所以会走下面的逻辑:
将线程B对应Node节点加入到等待队列中,并修改前驱、后继节点的指向。此时AQS的状态图如下:
此时线程B的addWaiter()方法已经执行完成,接下来将会执行acquireQueued()方法。
3)、acquireQueued()
acquireQueued()的主要作用是将抢占不到资源后的线程直接park挂起。
// 将线程抢占不到资源后挂起
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 这里又是一个自旋
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 判断前驱节点是不是就是head头节点
// 如果前驱节点就是head头节点的话,则再次调用tryAcquire尝试获取锁
// 需要注意的是,这块代码包含在for自旋中,也就是说,如果某个线程之前被park()之后,当它被unpark()唤醒之后,还会继续执行下面的代码:判断前驱节点是头节点,并且再次尝试获取锁
if (p == head && tryAcquire(arg)) {
// 获取锁成功的话,则设置当前Node节点为新的等待队列的头节点, 并将当前Node节点的线程、前驱节点都置空
setHead(node);
// 前驱节点的后继节点也置空
p.next = null; // help GC 帮助GC回收,实际上就是将之前的虚拟头节点进行出队,原先虚拟头节点的后继节点成为新的虚拟头节点
failed = false;
return interrupted;
}
// 检查并更新未能获取的节点的状态
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果标志位failed还是true的话,则取消尝试获取锁
if (failed)
cancelAcquire(node);
}
}
可以看到,acquireQueued()里边又是一个for(;;)自旋,还是对照案例说明一下。
线程B进来,此时线程B的前驱节点就是head头节点,但是因为线程A还未释放锁,所以tryAcquire()尝试获取锁肯定也是失败的,返回false。
根据上面代码会进入shouldParkAfterFailedAcquire()方法:
// 检查并更新未能获取的节点的状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱节点的等待状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// CAS将前驱节点的waitStatus等待状态置为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
此时线程B的前驱节点的等待状态waitStatus=0,执行流程如下图:
这里将会调用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)将线程B的前驱节点,也就是目前的head节点的waitStatus修改为-1,然后shouldParkAfterFailedAcquire(p, node)方法返回false,不会执行if里面的逻辑。
此时AQS的状态图如下:
因为是一个自旋,所以线程B第二次执行同样的逻辑:
线程B再次执行shouldParkAfterFailedAcquire()方法:
所以shouldParkAfterFailedAcquire(p, node)第二次执行是返回true,需要执行parkAndCheckInterrupt()方法:
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
return Thread.interrupted();
}
可以看到,this就是线程B,也就是说,线程B调用了LockSupport.park()在这里阻塞等待获取锁了。
跟线程B获取锁流程类似,因为线程A执行比较耗时,还未释放锁,此时线程C也来获取锁,tryAcquire(arg)肯定返回false,同样会执行线程C的addWaiter()入队:
此时AQS的状态图如下:
同样的,线程C在执行acquireQueued()里边的shouldParkAfterFailedAcquire(p, node)时,会将前驱节点,也就是线程B的Node的等待状态waitStatus修改为-1。此时AQS的状态图如下:
接着,线程C也执行park方法阻塞等待获取锁:
至此,线程B、线程C都在acquireQueued()方法中阻塞着,等待其他线程unpark之后再次去尝试获取锁,如下图:
以上就是AQS中独占锁加锁、入队的详细过程。
四、AQS源码解析之解锁流程
AQS的解锁入口:
public void unlock() {
sync.release(1);
}
同样调用的是sync的release()方法,由于sync类继承自AQS,并且没有重写release()方法,所以这里实际上调用的是AQS的release()方法:
public final boolean release(int arg) {
// tryRelease(): 尝试释放锁。返回true,表示当前线程释放锁成功
if (tryRelease(arg)) {
// 获取等待队列的头节点
Node h = head;
// 等待队列的头节点不为空,并且节点等待状态不等于0
if (h != null && h.waitStatus != 0)
// 唤醒等待队列的头节点的后继节点(如果存在的话)
unparkSuccessor(h);
return true;
}
return false;
}
首先会调用tryRelease()方法尝试释放锁,跟tryAcquire()一样,tryRelease()也是一个模板,AQS强制其子类去实现:
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
我们直接看ReentrantLock.Sync#tryRelease方法:
protected final boolean tryRelease(int releases) {
// 计算AQS当前状态state的值减去releases后的值
int c = getState() - releases;
// 如果释放锁的线程不是当前正在持有锁的线程,将会抛出IllegalMonitorStateException异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 释放锁是否成功标识
boolean free = false;
// 如果state已经减到为0,说明当前线程已经完成退出锁
if (c == 0) {
// 修改标识位为true
free = true;
// 因为当前持有锁的线程已经释放锁,所以需将exclusiveOwnerThread置空,表示这把锁目前没有线程正在持有
setExclusiveOwnerThread(null);
}
// 重新设置state的值
setState(c);
return free;
}
还是对照前面的案例分析,假设线程A终于要释放锁了,进入tryRelease(1)方法:
- 1、获取到当前AQS同步状态state,值为1,减去此次释放的1,那么得出c=0;
- 2、判断释放锁的线程是不是当前正在持有锁的线程,正常情况下都是,如果不是,将会抛出IllegalMonitorStateException异常;
- 3、因为c==0,调用setExclusiveOwnerThread()将当前持有锁的线程置空;
- 4、重新设置AQS同步状态state的值为0,tryRelease(1)方法返回true;
先来看下当前AQS的状态图:
进入if判断,首先获取到队列头节点,此时队列头节点肯定不为空,并且节点等待状态waitStatus=-1,不等于0,那么就会执行unparkSuccessor(h)方法,唤醒等待队列中头节点后面一个节点:
// 唤醒node节点的后继节点(如果存在)
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
// 获取node节点的等待状态
int ws = node.waitStatus;
// 节点等待状态如果小于0的话,则使用CAS将其waitStatus修改为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 获取到node节点的后继节点
Node s = node.next;
// 后继节点为空 或者 后继节点的等待状态大于0(取消获取锁)
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)
// 调用unpark()唤醒节点对应的线程
LockSupport.unpark(s.thread);
}
执行完前面的unpark后,此时AQS的状态图如下:
由于线程B已经被unpark,还记得之前我们说过的线程B阻塞的地方么,那就是acquireQueued()方法:
线程B执行tryAcquire(),由于当前AQS的同步状态state的值为0,cas操作能成功,所以线程B抢占得到锁:
此时AQS的状态图如下:
以上就是AQS释放锁的整体流程。
五、公平锁与非公平锁在AQS实现中的区别?
- 非公平锁:一上来就先执行一次CAS抢锁,如果抢占不成功,执行正常的获取锁流程。并且在tryAcquire()方法中,非公平锁在这里不会执行hasQueuedPredecessors()判断是否有任何线程等待获取的时间比当前线程长,直接尝试加锁。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
- 公平锁:执行正常获取锁流程。在tryAcquire()方法中,公平锁会执行hasQueuedPredecessors()判断是否有任何线程等待获取的时间比当前线程长,严格遵循先来后到。
final void lock() {
acquire(1);
}
六、独占锁和共享锁
- 独占锁:指该锁只能同时被一个线程持有;
- 共享锁:指该锁可以被多个线程同时持有;
举个生活中的例子,比如我们使用打车软件打车,独占锁就好比我们打快车或者专车,一辆车只能让一个客户打到,不能两个客户同时打到一辆车;共享锁就好比打拼车,可以有多个客户一起打到同一辆车。