1、AQS简介
2、源码分析
2.1 线程阻塞
2.2 线程唤醒
AQS全名:AbstractQueuedSynchronizer,它就是Java的一个抽象类,它的出现是为了解决多线程竞争共享资源而引发的安全问题,细致点说AQS具备一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中,队列是双向队列。
常用的实现类是ReentrantLock(重入锁),ReentrantReadWriteLock(读写锁),CountdownLatch(计数器)等等,这些实现类都是通过内部类Sync继承AbstractQueuedSynchronizer,从而实现相应功能的。
先看下这张图,对Java的重入锁的代码结构有个大概的了解。
内部抽象类Sync继承AbstractQueuedSynchronizer,ReentrantReadWriteLock,CountdownLatch都是一样;
我们一般使用AQS功能的简单代码实现:
public class Demo {
static Lock lock = new ReentrantLock();
public static void test() {
lock.lock();
try {
// TODO:
}
catch (InterruptedException ex) {
}
finally {
lock.unlock();
}
}
}
重入锁通过加锁lock 和 解锁unlock操作进行多线程的同步控制操作。从上面代码我们可以猜想到,在多线程竞争情况下,当线程加锁操作获取不到锁,则线程要进入阻塞队列;当锁释放后,队列节点(线程)要能够获得锁;那么问题来了:
1、线程获取到锁具体是怎么实现的?
2、线程获取不到锁具体是怎么操作的?
3、锁释放后,队列节点是怎么触发获取锁的?
这些细节问题,一定要看源码才能得到答案。
以公平锁为例:
java.util.concurrent.locks.ReentrantLock$FairSync
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1); // 父类方法,1表示一次
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取锁的状态,其实就是计数器,0表示锁没有被任何线程获得
int c = getState();
if (c == 0) {
// 这里有三个方法:
// 1.hasQueuedPredecessors 有没有前序结点,如果有肯定轮不到当前线程,
// 这个方法比较巧妙,需要先理解队列设计思想才能看懂,后面有图分析
// 2.compareAndSetState 设置锁计数器=1,原子操作
// 3.setExclusiveOwnerThread 设置独占
// 三个操作都符合条件才算当前线程获得锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果当前线程已经获得锁,那么锁计数器state加1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
总结:AQS获取锁的机制就是维护一个int属性state,
java.util.concurrent.locks.AbstractQueuedSynchronizer
/**
* The synchronization state.
*/
private volatile int state;
public final void acquire(int arg) {
// 子类(公平/非公平)调用父类的这个方法
// 如果tryAcquire尝试加锁成功就没有后面方法什么事了
// 如果tryAcquire尝试加锁是失败,则先addWaiter把线程加到队列,然后再acquireQueued尝试获取锁。。。
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果线程被中断过,再调用一次interrupt方法,清楚中断状态
selfInterrupt();
}
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new 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; // 找尾结点,可能是null,比如:第二个线程过来
if (pred != null) {
// 如果排队的线程很多,给当前线程Node设置好前序结点
node.prev = pred;
// compareAndSetTail是将tail指向当前Node,这里是原子操作
if (compareAndSetTail(pred, node)) {
pred.next = node; // 给原尾结点设置后序结点,也就是当前Node
return node; // 设置完就return
}
}
// 当pred先序结点为空的时候,有可能需要初始化队列
enq(node);
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) { // 死循环操作
Node t = tail;
// 可以看到这里依然对tail做了if-else判断,为啥呢?
// 因为多线程,这里有可能tail就是非空,所以上面方法说有可能初始化
if (t == null) { // Must initialize
// 初始化分支
// compareAndSetHead是个原子操作,反正CAS开头的都是原子操作
// 需要注意的是,这里不是用的方法参数node,而是先创建了一个Node,并且head,tail都指向了这个空Node
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 已经初始化分支
// 这里把node设置成tail结点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t; //返回原来的tail,初始化的时候并不关心返回值,只在xxx的时候关心
}
}
}
}
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
// 核心方法
final boolean acquireQueued(final Node node, int arg) {
// 到这里,总结一下addWaiter方法
// addWaiter方法做的事情就是把当前线程封装成一个node,加入到队列中,并返回
boolean failed = true;
try {
boolean interrupted = false;
// 又一个死循环,直到return interrupted获取锁
for (;;) {
// 这个for循环的意思是:
// 1、如果当前线程确定还轮不到获取锁,则乖乖地进入队列,并且当前线程中断
// 2、如果当前线程可以获取锁,则死循环反复去尝试获取锁,当然,在公平锁模式下一次就OK了
final Node p = node.predecessor(); // 取前序结点
// 如果前序节点是head节点,则尝试获取锁,即第二次尝试获取锁(第一次是tryAcquire)
if (p == head && tryAcquire(arg)) { // 第二次tryAcquire,也就是在入队列前再尝试一把,万一锁被释放了呢!
// 如果获取到锁,把head指向当前node,把初始化创建的空节点GC
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; // 这个interrupted表示的是当前线程有没有被中断过
}
// shouldParkAfterFailedAcquire返回true表示前序节点还在排队,所以当前节点需要去park,进到parkAndCheckInterrupt方法
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)
// 前序节点还在排队呢,所以当前节点node只能挂起,安心地去排队
// 这里说下节点得SIGNAL状态,它的意思是如果锁被释放,应该通知SIGNAL状态的节点
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 下面的情况,node节点不需要去park,最终返回false使上层调用方法死循环直到获取锁
// 首先是ws>0,即waitStatus=cancelled=1(看内部类:Node)
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只能是0/-3,
/*
* 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.
*/
// 把前序节点的waitStatus设置为-1:SIGNAL,因为啥呢?
// 看上面if (ws == Node.SIGNAL)分支,只有前序节点waitStatus=-1,当前节点才能安心地去队列等待,
// 否则当前node会一直自旋获取锁,这显然是不合理的
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* Convenience method to park and then check if interrupted
* 调用底层线程挂起方法将线程挂起,并且返回挂起的状态,也就是检查一下
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 这里直接调park方法是将线程挂起
return Thread.interrupted();
}
总结:阻塞等待
AQS使用FIFO双向阻塞队列来保存被阻塞的线程,实现机制是,AQS通过其内部类Node封装线程,同时Node维护prev,next,waitStatus信息来实现双向队列;
针对节点的waitStatus属性(等待状态),要补充说明一下,它的取值有以下几种:
下面画图来说明以下阻塞队列的初始化和变化:
1、队列初始化
此时会构造一个空节点放进队列,锁的head和tail都指向这个空节点,空节点的thread,prev,next都是null;锁第一次被获得就会构造带有一个空节点的队列,当然当前线程直接就获得锁了,而不会入到队列。
2、如果有线程竞争,获取不到锁的线程就会被封装成Node节点入到队列中去,但不是替换空节点,而是跟在空节点的后面;
3、现在锁释放了,节点1获得锁了,看看队列的变化,队列会把head指向节点1,原来的空节点就等着被GC,节点1的thread,prev会被置空,next不变,因为如果节点1后面还有节点2的话,next就指向节点2;
总之一句话,除了初始化存在空节点以外,队列的head节点总是最后一个获得锁的节点;
线程唤醒的前提当然是线程被挂起,线程挂起操作在上面源码中有贴出来,线程挂起后,线程也就阻塞在这里:
LockSupport.park(this); // 这里直接调park方法是将线程挂起
上面acquireQueued方法是AQS的核心,其线程阻塞与获取锁都在这个里面,核心思想是:
1、如果前序节点还在排队(waitStatus=-1),后续节点直接挂起;
2、如果前序节点取消了(waitStatus=1),后续节点的逻辑中会把取消的前序节点删除(递归删除);
3、如果前序节点也是刚加进来的,节点状态还没定,也没有获得锁,那么当前线程要把前序节点的waitStatus设置为-1;
——第3点要好好理解,换句话说,队列里除了最后一个节点,其他节点的状态都是由其next节点来修改的。为啥要这样做呢?这是因为挂起的线程要解除挂起状态获取锁,这需要一个状态,看下面代码分析。
java.util.concurrent.locks.ReentrantLock$FairSync
public void unlock() {
sync.release(1);
}
java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
// 首先是尝试释放锁,有人问了,这释放锁还需要尝试吗?又不是获取锁,还可能获取不到
// 那确实存在释放不了的情况,什么情况呢? 那就是重入次数大于1的情况,按照重入锁的设计,重入几次就需要释放几次
if (tryRelease(arg)) {
// 锁释放成功,要唤醒后序挂起线程
Node h = head; // 这里拿到head节点
if (h != null && h.waitStatus != 0) // 判断head节点状态非0,即被修改过
// 如果h.waitStatus = 0,表示没有后续节点,能理解不?看上面第三点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 注意参数是head节点,因为唤醒后续节点总是从head往后找
/*
* 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.
*/
int ws = node.waitStatus;
if (ws < 0)
// 还原head节点状态为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 s = node.next;
// 校验状态,因为队列里面可能就没有其他竞争线程,或者next节点取消了
if (s == null || s.waitStatus > 0) {
s = null;
// 咋整呢?从后往前遍历,遍历到最靠前的一个状态正常的节点,这个节点就是要被唤醒的节点
// 这里需要注意的是,这里并没有删除取消的节点,因为取消是在获得锁的逻辑里面删除的
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) // 这里就能理解为啥都要给前序节点设置状态等于-1了吧,为啥不是-2,-3呢?因为那俩有其他用处
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒线程节点
}
java.util.concurrent.locks.ReentrantLock$Sync
// 这里逻辑就很简单了,判断state的值即可
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
最后再来个总结,要说AQS的原理,很多人会谈到队列,计数器,但是更为底层的支撑,我认为应该是CAS + LockSupport,如果你详细看过源码的话,会发现AQS到处都是CAS操作(CAS操作的本质就是乐观+自旋),线程的挂起和唤醒则是通过LockSupport来处理。队列只是其实现的一种方式,换句话说,即便用数组应该也能做的到。
AQS和Synchronized,要说这两者的区别,有很多,我不想一一列举了,意义不大,因为随着版本的演进,两者有很多地方都在相互靠拢。
最后谈一下LockSupport,其核心功能就park和unpark,挂起和解挂,我看有些文章说重入锁的挂起不会有线程的用户/内核态切换,这是错的,不管是ReentrantLock还是Synchronized,只要发生锁的竞争,最终都是会有线程的状态切换的(自旋失败)。
相对于wait/notify,LockSupport有很多优点,具体这里不累赘了,只举个例子吧:先做一次unpark,在做一次park,线程不会挂起;但是先做两次unpark,再做两次park,线程就会挂起。
比如说:你去澡堂洗澡,澡堂服务员要给你一个钥匙牌子挂在手上,才能进澡堂子,但是AQS是一个特殊的澡堂,它只给一个线程服务,而且它只有一个牌子,可以理解为:这个澡堂子只服务一个顾客,而且只有一个寄存柜,也就是只有一个牌子(钥匙),那么线程阻塞则好比你去洗澡,但是服务员没有牌子给你,你只能等,等那个牌子被释放,你可能问了,不是说只服务我一个顾客吗? 是的,不错,但是这个服务员缺心眼啊,它不支持重入啊,它并不会因为你已经拿到唯一的牌子了,就让你进去,所以这Java只能自己实现重入锁了!
再看下上面两个例子:
1、先做一次unpark,在做一次park
——unpark相当于洗好澡了,把牌子还回去,park相当于去洗澡,发现有牌子,直接拿着牌子去洗澡了,所以线程不会阻塞。
2、先做两次unpark,再做两次park
——做两次unpark,因为只有一个牌子,所以效果跟做一次unpark一样,接着,第一次park可以拿到牌子,第二次park就拿不到牌子了,所以线程阻塞。这里能看出来native代码傻了吧!