AQS
AQS全称是AbstractQueuedSynchronizer,它是java.util.concurrent包下的抽象类,是并发工具包的基础类,我们使用的很多JUC包下的工具类,例如ReentrantLock,CountDownLatch,CyclicBarrier、Semaphore等都是对AQS的实现,利用AQS我们可以简单高效的构建出同步器。
本文通过对最常用的可重入锁ReentrantLock源码进行分析,梳理其实现原理。本文不涉及Condition相关内容,这部分以后会单独成文。
主要结构
首先,我们看一下AbstractQueuedSynchronizer抽象类的结构
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 队列的头节点,即持有锁的线程
*/
private transient volatile Node head;
/**
* 队列的节点,新加入的节点放在队尾,形成队列
*/
private transient volatile Node tail;
/**
* 同步锁的状态,0表示没有线程持有锁,大于0表示有线程持有锁,对于可重入锁
* 这个值会大于1
*/
private volatile int state;
......
/**
* ......
*
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
*
* 等待队列采用的是CLH队列的一种变体,它是一个虚拟的双向队列,队列中每个节点封装请求共享资源的线程
* 同时保存了当前节点在队列中的状态,前驱及后继节点,如上图所示,但需要注意的是,
* head节点表示当前持有共享资源的线程,可以认为阻塞队列不包含head节点
* ......
*/
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;
/** 本节点在condition队列中 */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
/**
* 节点的等待状态,非负意味着取消的等待,不再争抢锁了
*/
volatile int waitStatus;
/**
* 前驱节点
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 线程对象
*/
volatile Thread thread;
/**
* 链接condition队列或者用来表示共享模式
*/
Node nextWaiter;
......
}
}
OK看完AQS大体结构,再来看它的实现类ReentrantLock,在内部维护了一个Sync对象,Sync继承了AQS抽象类,并提供了两种实现方式,分别应用于公平锁和非公平锁。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
/**
* lock方法由子类实现,公平锁和非公平锁竞争共享资源的方式不一样
*/
abstract void lock();
final boolean nonfairTryAcquire(int acquires) {
......
}
protected final boolean tryRelease(int releases) {
......
}
}
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
......
}
}
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
上面的代码比较多,可能看起来很乱,我们在这里梳理一下,
- 在ReentrantLock中维护了一个同步器Sync对象,它有公平锁和非公平锁两种实现方式,取决于ReentrantLock初始化时构造函数传的值是true还是false,默认是非公平锁。所以nonfairTryAcquire方法写在了Sync类中
- 查看AbstractQueuedSynchronizer源码知道提供了两种资源竞争方式,即独占和共享,分别对应两组方法tryAcquire+tryRelease或者tryAcquireShared+tryReleaseShared,由于ReentrantLock在设计上是独占锁,所以只对tryAcquire+tryRelease进行了实现
- tryAcquire的语义是尝试获取锁,无法获取则加入阻塞队列中。对于ReentrantLock而言,公平锁和非公平锁尝试获取锁的方式不一样,非公平锁会直接竞争锁,而公平锁会判断当前的独占线程是否为自己(这个后面会展开)。总结一下:tryAcquire由同步器自己实现具体竞争锁的逻辑,方法的结果决定了线程是否会加入等待队列。
线程抢锁
调用ReentrantLock.lock方法的线程,如果持有锁可以继续执行,反正则会阻塞等待锁释放。下面我们看一下线程竞争锁的过程。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* 父类AQS的方法,tryAcquire先尝试获取一下锁,如果返回true则获取成功了,直接结束
* 否则,将线程加入阻塞队列,等待锁释放
* addWaiter方法是创建一个等待队列的节点,将节点加入队列中
* acquireQueued是将线程挂起等待唤醒
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* 尝试获取锁,返回值表示是否获取到锁
* 返回false表示有其他线程持有锁,返回true表示获取锁成功
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// c == 0 当前没有线程持有锁
if (c == 0) {
//由于是公平锁,则需要判断队列中是否有线程在等待,
//如果没有,通过CAS抢锁,抢锁成功则设置自己为当前锁的独占线程
//如果CAS不成功,则表示锁被别的线程抢了
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//当前有线程持有锁,如果当前独占线程是自己,则表示是锁重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//没有获取到锁,回到acquire方法,将执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
return false;
}
}
// if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
通过上面acquire方法可以看到,如果tryAcquire返回false表示尝试获取锁失败,会执行addWaiter和acquireQueued方法
/**
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
*/
/**
* 将线程包装成node节点并加入队列的队尾
*/
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 pred = tail;
if (pred != null) {
//将队列之前的尾巴设置为新节点的前驱
node.prev = pred;
if (compareAndSetTail(pred, node)) {
//双向连接,入队成功,新节点变成了尾巴,然后return
pred.next = node;
return node;
}
}
//执行到这里,说明tail=null(队列为空)或者CAS失败了(线程竞争失败)
enq(node);
return node;
}
/**
* 自旋方式入队,如果队列为空或者线程竞争入队失败,则一直循环加入队列
* 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//队列为空的情况,CAS设置tail = head = new Node() 这时head是一个虚拟节点
//这里没有return,执行完后继续for循环,下次就到else分支了
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//节点入队,CAS设置tail,如果CAS失败则继续循环,直到入队成功
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* 从addWaiter方法返回后,说明节点入队成功,执行acquireQueued方法
* 这个方法包含对线程进行挂起,以及唤醒后处理(线程排队以及锁的处理)
* 这个方法非常重要,下面梳理一下主要流程:
* 节点入队成功后,尝试获取锁,如果获取成功就不挂起了,线程继续运行
* 如果获取锁失败,判断是否需要挂起线程,判断依据是前驱节点waitStatus是否为-1
* 如果前驱节点是-1,则调用LockSupport.park挂起线程
*/
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(持有锁),同时前驱的节点出队(释放锁)
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 当前节点获取锁失败,那么当前线程需要挂起,等待被唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* 判断是否需要挂起节点的线程,这个方法通过前驱节点状态进行判断,对当前节点进行不同的处理
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前驱节点是正常等待状态,则当前节点需要被挂起
return true;
if (ws > 0) {
//判断前驱节点的状态,如果前驱节点取消了排队,则一直向前寻找,直到找到还在排队中的节点
//因为挂起线程的唤醒是依赖前驱节点的,所以当前节点的前驱节点一定要是"活"的
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//到这里说明前驱节点的状态是0,-2,-3(新节点入队时状态都是0)
//将前驱节点的状态设置为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//结合上面的代码知道返回false会再次进入循环,
//下一次循环就会走第一个if分支返回true
return false;
}
/**
* 在这里挂起线程,等待被唤醒
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
执行到这里,线程加锁的操作完成,对于ReentrantLock而言,只有头节点的线程持有锁,队列中的线程处于阻塞状态,直到被LockSupport.unpark唤醒。
线程解锁
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//解铃还须系铃人
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果是锁重入,要全部释放了才会返回true
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
/**
* 线程解锁的方法,此时的node是头节点head
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 找到后继节点,并唤醒它
* 后继节点可能是取消了排队,从队尾往前找,循环结束得到的是排在队列最前面的waitStatus <= 0节点
* LockSupport.unpark唤醒此节点
*/
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);
}
线程被唤醒后,从LockSupport.park继续执行,进入acquireQueued下一个循环,此时node的前驱是head,再次进入抢锁的逻辑了
// private final boolean parkAndCheckInterrupt() {
// LockSupport.park(this);
// return Thread.interrupted();
// }
总结:
加锁
- tryAcquire-线程尝试获取锁,如果不存在锁竞争,就直接返回,它没有操作阻塞队列
- addWaiter-将线程包装成node节点并CAS加入阻塞队列队尾
- acquireQueued-自旋的方式获取锁,如果不能获取,则设置前驱节点为waitStatus=-1,表示等待被唤醒,然后线程挂起
- waitStatus=-1的意思是代表后继节点需要被唤醒,为什么这么说呢?我们通过addWaiter方法知道新加入队尾的节点waitStatus都是0,之后acquireQueued方法会将前驱节点的状态更新为-1(SIGNAL),而自己的状态还是0,也就是说waitStatus=-1是后继节点更新的,通过这个状态知道后继节点是否等待被唤醒。
解锁
- tryRelease-尝试释放锁,它同样没有操作阻塞队列
- unparkSuccessor-当前节点的线程释放锁了,唤醒它的后继节点来抢锁
公平锁与非公平锁
ReentrantLock内部提供了公平锁和非公平锁两种实现,这里再扩展一下两者的区别,我们注意看他们的tryAcquire方法的区别
/**
* 公平锁
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//公平锁和非公平锁区别就在这里
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
/**
* 非公平锁
*/
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* 这是父类Sync中的方法,贴过来方便对比
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
可以看到,公平锁和非公平锁的区别在于
1.非公平锁在lock方法中一上来就会进行一次抢锁
2.tryAcquire方法中,公平锁多了一个!hasQueuedPredecessors()判断,它的语义是:
在线程尝试获取锁时,如果刚好当前没有线程持有锁,公平锁会去判断队列中还有没有在排队的线程(先来后到的原则),但非公平锁会直接尝试抢锁。
以上就是非公平锁和公平锁的区别,非公平锁在入队前进行两次抢锁,如果两次都没有抢到,就跟公平锁一样,进入队列中乖乖排队,非公平锁由于不需要保证时间顺序性,显然性能更好,但是可能会出现线程饥饿的情况。
文中很多内容参考了以下两位大佬的文章,并在此基础上增加了一些个人的思考,原文质量非常高,如果有没讲清楚的地方不妨移步
https://www.javadoop.com/post/AbstractQueuedSynchronizer
https://blog.csdn.net/hancoder/article/details/120954315