关于AQS(AbstractQueuedSynchronizer),其实平时我们使用的最多的就是ReentrantLock,我们先看看ReentrantLock到底是个什么东东
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
原来ReentrantLock 是Lock的实现类之一,而且它还有一个非常重要的属性Sync,这个类可是关键哦。当我们调用ReentrantLock的Lock()方法加锁的时候发生什么?进入源码一看,原来它调用了sync的lock()方法
public void lock() {
sync.lock();
}
那这个sync又是什么东东呢?我们再点进源码一看,原来这个就是我们大名鼎鼎的AQS类的子类之一啊!!里面也有lock()方法
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
好啦,这里我们就看到了我们经常说的ReentrantLock原来是借助AQS实现的锁啊,那么接下来我们就要进入AQS的源码去看看啦,冲冲冲
这里的lock是一个抽象方法,它有不同的实现类
abstract void lock();
盲猜,也是就我们所说的公平锁和非公平锁了,毕竟面试官问你它的优势,你肯定会说它既能实现公平锁又能实现非公平锁
果不其然
我们先来看看公平锁的实现原理,公平锁的实现里面调用acquire()方法,并且传入了参数1
final void lock() {
acquire(1);
}
继续刨根,这里的acquire()的大概逻辑就出来了。我们先说一下大概的逻辑,再往里面深扒。首先这里tryAcquire方法是尝试获取锁,如果失败了addWaiter方法是进入一个队列,这个队列是双向链表维护的;进入队列之后,这个线程还是不得死心,它还要再挣扎一下,也就是我们通常所说的自旋(2次),于是它继续调用acquireQueued方法,最后实在拿不到锁了,它就睡眠自己了。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这就是AQS去加锁的一个大致流程。看到这里你可能还不觉得Doug Lea有多牛逼,但是等会你就叹服
我们先去看一下tryAcquire方法,尝试获取一下锁。
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;
}
下面我们就一起畅游吧,首先拿到当前运行的线程current ,再通过getState方法获取锁的状态,这里的state属性表示锁的状态,默认为0表示无锁。同时为了保证线程之间的可见性,使用volatile关键字修饰private volatile int state;
假如此时状态为0,进入第一个if判断,这里通过hasQueuedPredecessors
方法判断自己是否需要入队。我们进入这个方法里面看看
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这个方法里面有两个Node节点,分别是头尾节点,初始值都为null
,此时我们是第一个线程进来拿锁,所以队列并没有被初始化,因此h != t
为false
,后面的&&
就自然短路了。整个方法返回false
。
好,那就继续回到咱们上面的尝试获取锁的第一个if代码块
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
由于hasQueuedPredecessors
方法返回false
,前面加了!
,所以整个为true
。继续判断&&
后面的代码,这里就有了CAS原子操作了。compareAndSetState(0, acquires)
是以CAS的方式将锁的状态从0修改为1。假如线程拿到了锁,返回true
,进入下面的代码块setExclusiveOwnerThread(current)
,这个方法就是为了把当前线程设置为拿到锁的线程,为啥呢?后面在判断可重入锁的时候用。最后整个tryAcquire
方法返回了true
。
该线程拿到了锁,进入了我们的业务逻辑代码开始执行…
这里只是模拟第一个线程来拿锁的情况,怎么样,挺得住吗
接下来咱们开始看看面试官都喜欢问的可重入锁,其实很简单,也在咱们的tryAcquire
方法中。假如拿到锁的线程在业务逻辑代码中需要再次拿到锁,依旧会走上面的流程来到咱们的tryAcquire
中,只不过此时的state已经被它自己修改为了1
,所以进入这个代码块
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
首先判断当前线程是不是持有锁的线程啊?毋庸置疑,肯定是的,那就把锁的状态+1
就完了,然后返回true
,业务逻辑代码正常执行。怎么样?是不是很简单。原来可重入的意思就是线程可以再次拿到锁,只不过把锁的状态值增加1
而已。
上面的过程你是不是感觉很简单嘛?确实,假如面试管问你AQS,你首先告诉他,多线程在交替执行的时候,AQS的队列并没有初始化,也没什么卵用。 他可能会觉得,这小子可能真看过源码
接下来咱们讲点有趣的。多线程不再交替执行,也就是说存在竞争了,那咋办?Doug Lea都给你安排的明明白白的,不慌。
第一个线程在执行,第二个线程来了,它也会进入tryAcquire
方法,然后它发现锁的状态不为0,同时自己也不是持有锁的线程,那它只能可怜的拿到最后一行代码,返回false
。接下来主场戏就要开始了,真的精彩,有尿你都憋住!!!
还记得acquire
方法吗
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
此时tryAcquire
返回false
,前面加了!
,所以为true
,于是代码继续往后判断,成功进入addWaiter
方法,大胆的点进去
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)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
首先把当前线程封装为一个Node节点,然后判断tail
节点是否为null
,这里首尾节点都没有初始化,肯定为null
啊,所以直接走enq(node)
,再点进去
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里面是一个自旋的方法,第一次自旋的时候,先通过CAS的方式创建一个空节点,并让head
指向空节点,之所以叫空节点,是因为里面的thread
为null
。然后尾节点也指向这个空节点。然后再自旋一次,通过CAS的方式让当先线程所在的节点挂到空节点后面。
到此为止,第二个线程做了什么事情呢?首先去拿锁,拿不到;然后入队列,发现队列未初始化,自己去做了初始化的工作,并且把自己挂到了虚拟头节点的后面。接下来他该干嘛了呢?他要准备睡眠自己了,不过,他可不得这么轻易睡觉,你上床睡觉不得在被窝里挣扎一下哇???
话不多说,来人,上源码
进入acquireQueued
方法啦,其实就是询问自己是不是真的要睡眠了
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);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
第一感觉就是又在自旋对不对,是的AQS就是自旋+CAS+双向队列+park
。第一次自旋,先拿到当前线程所在Node节点的前一个节点final Node p = node.predecessor()
,然后判断前一个节点是不是虚拟头节点,如果是的话,他就去尝试获取锁(第一次挣扎),这里我们假设他获取锁失败。于是进入下面的方法shouldParkAfterFailedAcquire(p, node)
,这个方法是在干嘛呢?大胆点进去
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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
不要慌,代码很长,逻辑很短,全是注释,咱们一步一步来分析
首先,会拿到前一个节点的waitStatus
,咱们简称ws
。然后就是判断ws
的值,ws
默认为0
,所以第一次自旋进来,当前线程会把他前面的节点的ws
状态使用CAS
的方式修改为-1
,就是这句代码compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
,然后返回一个大大的false
。由于返回false
,所以
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
不会继续往下执行;代码进入第二次自旋,第二次自旋我们又假设没有获取到锁,(不是线程二不给力,只是后面再专门讲在竞争情况下拿到了锁该怎么维护这个双向队列,忍耐一下)所以再次进入shouldParkAfterFailedAcquire
方法,由于第一次自旋的时候ws
已经被修改为-1
了,所以这次直接返回true
。然后就执行后面的if语句后面的代码块了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
也就是说park
当前线程,是线程睡眠。
到这里我们发现,第二个线程在睡眠之前通过两次自旋
又做了两件事情
,第一件:把自己前一个节点ws
修改为-1
,第二件就是自己睡眠。
其实为啥也这样设计呢? 我自己感觉哈,给head虚拟节点两次挣扎的机会,就是为了进来不让线程park,这会设计用户态和内核态的切换,十分消耗性能。我们一直抱有侥幸心里,总觉得可能我刚入队,前面一个线程就释放锁了呢。那我是不是可以不用睡眠,直接执行呢?这种情况确实可能存在。但是我们是公平锁,只有head节点的后面第一个节点可以去尝试获取锁,其它后面进来的节点你就早点洗洗睡吧,你前面的都还是排队呢,你猴急啥。选择两次自旋也是出于对性能的考虑,如果自旋太多,会十分影响CPU的性能。这个设计真的非常巧妙。
后面再来第三个、第四个线程等等都是这样自旋两次,唯一不一样的就是在acquireQueued
方法中,因为自己的上一个节点不是head
虚拟节点,所以不会执行tryAcquire
方法,但是依旧会修改前面节点的ws
,然后睡眠。这里面我们模拟了线程交替执行获取锁,模拟了第一个线程占用锁,后面所有的线程拿不到锁入队列的过程,就问你Doug Lea强不强???
听到这里,可能有小伙伴要为线程二正名了,为什么他就那么苦逼,每次拿不到锁。好的,那这次咱们就让他硬气一回,让他拿到锁,当队列中的Node拿到锁了又该如何去维护这个链表呢?
愣着干嘛??上菜了
假如线程二在自旋的过程中拿到锁了。开心,终于轮到我执行了
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
进入到if代码块中,首先是setHead
方法,咱们去看看他干了什么事情
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
这个方法就是把head
节点指向获取到锁的节点,然后把节点中的thread
置为null
,然后把他的前面阶段断开(这个以前的head虚拟节点就没有了引用,就会被GC回收了)。
看懂了吗?巧妙吗?折服了吗?
拿到锁的线程去执行了,然后他所处的Node变为了head虚拟节点,简直太优秀了!!!
到此,咱们公平锁的基本上就讲完了,这才讲了一半,哈哈哈,不过相信你已经可以自己去分析非公平锁的源码了,一步一步进去看,肯定可以看明白的
其实非公平锁也不难,我们大概看一下
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
这是他的lock方法,每个线程一上来,不管队列中有没有人派对,自己先去抢占锁,抢不到再说
后面有时间再阅读解锁和唤醒的源码。这是自己看了源码的一些心得体会,自己记录看一下,同时也希望帮助到正在啃源码的你。有错的地方可以在评论区留言交流