经过昨晚的培训.对AQS源码的理解有所加强,现在写个小笔记记录一下
同样,还是先写个测试代码,debug走一遍流程, 然后再总结一番即可.
测试代码
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class AqsTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
// 创建一个CyclicBarrier,以便于让50个线程同时去抢占锁资源
CyclicBarrier cyclicBarrier = new CyclicBarrier(50);
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "开始抢锁");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "抢锁成功");
Thread.currentThread().sleep(1000);
System.out.println(Thread.currentThread().getName() + "释放锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"线程"+i).start(); }
System.out.println("main over");
}
}
下面开始流程分析
1. 创建锁对象
Lock lock = new ReentrantLock();
本示例使用了重入锁,底层是非公平同步锁NonfairSync类, 先看一下类图关系
很明显,NonfairSync顶层类正是本篇的主角AQS
查看AQS源码,它有四种重要的属性需要我们去关注
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
private transient Thread exclusiveOwnerThread; #其实它并不在AbstractQueuedSynchronizer类中,父类属性
所以, 当我们在程序中new出一个Lock实例后.该Lock实例就自带这4个属性了,其中state初始值为0 ,余者皆是null. 而所谓的抢占锁资源,就是围绕着这4个属性展开. 下面我来阐述一下这四个属性意义
(1) head , CLH链表的头节点, 懒加载, 除了第一次加载之外,只能通过setHead方法去修改head节点. 注意: 如果CLH链表头节点存在,那么必须保证头节点的 waitStatus 值不能是1(CANCELLED), 即取消状态.
(2)tail , CLH链头的尾节点, 懒加载, 只能通过enq方法添加一个新wait节点去修改它.
(3)state, 用它来表示同步器的状态
(4)exclusiveOwnerThread , 独占模式下持有资源(AQS锁)的线程
好,至此,创建一个Lock实例及关联的信息应该是讲清楚了, 下面我们重点分析线程是如何抢占锁资源的,也即是分析一下lock方法到底干的啥?
2.抢占锁资源以及wait线程
线程如何抢占资源? 没抢到资源的线程又咋办呢?
下面以debug的方式来跟踪一下AQS的骚操作.
还有一个需要注意的地方是,这50个线程,现在都running状态,这些状态后面是会变化.......
先看下面截图
(1) 首个抢到资源的线程
为了便于记忆, 此处我选择让10号线程最先获取到锁资源, 具体逻辑如下.
去掉一些渣渣代码,程序会进入到下面这个方法
final void lock() {
// cas修改AQS的state状态值, 如果是0,就改成1.
if (compareAndSetState(0, 1)) // cas修改state状态值成功
// 将exclusiveOwnerThread 设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else // cas修改状态值失败
acquire(1); // 这里面的逻辑后面分析
}
说明:
(1)因为10号线程是第一个进来抢占资源的,此时,AQS四大属性中的state等于,所以这里cas修改值会成功,然后就会进行if操作,同时exclusiveOwnerThread的值修改成当前线程,即10号线程. 这也就表示10号线程抢到了锁资源.
(2) 在该方法中,AQS的4大属性中有2个属性发生了变化, 此时AQS4大属性中,state等于1,exclusiveOwnerThread是10号线程, head 和 tail仍然为null
(3) 请看下面的debug截图,证明以上分析的正确性
以上3张截图应该能证明,10号线程抢占锁资源成功!
此外,再补充一下上面提到的cas操作,即 compareAndSetState(0, 1)方法, 我们先看一下它的实现
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
这儿使用到了unsafe类,咱也就到底为止了, 但是方法里面的几个参数还是有必要弄懂的.
this : AQS
expect : 期望值
update : 更新后的值
stateOffset : 这就比较骚包了,我们直接看一下源码, 也使用到了unsafe类,然后就全靠自己意会了
stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
好,到这儿, 10号线程抢占到了锁资源,而且仍然持有锁的, 接下来,20号线程也开始抢锁了. (不要记了10号线程还拿着锁睡觉呢)
(2) 20号线程抢占锁资源
20号线程同样会进入到lock()方法,因为10号线程cas成功之后,AQS四大属性之一的state等于1.所以,此时20号线程进来cas会失败,然后进入else代码,
即进入acquire(1)这个方法.
请看下面的截图
分析完全正确.
然后,我们来看看accquire()方法里面干啥了......
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
哈哈,底层就是这3个飘红的方法.
咱们先来看看最简单的tryAccquire方法.注意,这儿会根据实例化锁的类型不同而逻辑不同, 咱们的测试代码使用的是非公平锁,所以看NonfairSync类中的实现即可. 源码如下
final boolean nonfairTryAcquire(int acquires) {
// 获取当前抢占锁资源的线程
final Thread current = Thread.currentThread();
// 获取AQS的state的值
int c = getState();
if (c == 0) {
// 如查state的值等于,使用cas将state值改成acquires
if (compareAndSetState(0, acquires)) {
// 将AQS的 exclusiveOwnerThread 改成当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前抢占锁资源的线程与AQS中的exclusiveOwnerThread是同一个线程
else if (current == getExclusiveOwnerThread()) {
// 将AQS的state值加上acquires
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 修改AQS的state属性值
setState(nextc);
return true;
}
return false;
}
这儿逻辑很简单,咱就不debug截图了, 该方法最后肯定返回false.
由于tryAccquire方法返回false,所以20号线程会进入后面的逻辑, 接着看addWaiter方法, 源码如下
private Node addWaiter(Node mode) {
// 以排它或者说是独占的模式创建一个node节点
Node node = new Node(Thread.currentThread(), mode);
// 将AQS四大属性中的tail赋值给一个临时变量pred
Node pred = tail; // 如果这个pred不为null
if (pred != null) { // if里面的逻辑就是创建的node节点与原CLH的tail节点互相挂载的过程
node.prev = pred; // 将pred节点挂到新创建的node节点的前一个节点上
if (compareAndSetTail(pred, node)) { // cas将新创建的node节点设置成CLH链表的尾节点,即tail
pred.next = node; // 将新创建的node节点挂到pred节点的下一个节点上
return node; // 返回新创建的node节点
}
}
// 如果pred节点等于null.就走enq方法, 由于20号线程进来就会走enq方法,所以以后我们重点分析它的执行逻辑
enq(node);
return node; // 返回新创建的node节点
}
private Node enq(final Node node) {
for (;;) {
// CLH链表的尾节点
Node t = tail;
if (t == null) { //初始化, 这也就是前面提到的懒加载问题
if (compareAndSetHead(new Node())) // 创建一个空的node节点,并将这个新创建的node节点设置成CLH链表的头节点
tail = head; // 将头节点赋值到尾节点,即,CLH中就只有一个空node
} else {
// 将CLH链表中的尾节点挂载到当前node节点的前一个节点上,即是当前节点成了CLH中尾节点
node.prev = t; // 挂前
if (compareAndSetTail(t, node)) { // 挂后
t.next = node;
return t;// 返回t节点,也是这个for循环的出口,注意呀,这个t节点就是当前传入节点的前一个节点,很关键呀~~~~~~~~~~~~
}
}
}
}
大家可以看到这个enq方法中有个无限循环,看到这种代码,一般都比较蛋疼, 而且在最前面,咱们也提到了使用enq方法时,会通过懒加载的方法去修改CLH链表的尾节点, 即然这么种重要,咱们有必须debug看看.
enq方法的一切都尽在截图与源码注释中. 最后再补充中一点, enq方法最后的 return t 并没有什么用 , 除了跳出for循环之外.
addWaiter方法最后返回的node仍然是包裹着抢占锁资源的线程的那个node节点, 唯一不同的是,这个node节点的pred位置已经挂载了一个空node.
还是截个图用来验明证身.
到此为止,addWaiter方法也说完了,接下该是acquireQueued方法了. 先看源码
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 当前节点的前一个节点p
final Node p = node.predecessor(); // 如果p是head节点,就让当前线程去尝试获取锁资源
if (p == head && tryAcquire(arg)) { // 当前线程获取锁资源成功
// 将当前节点node设置成头节点(印证了前面说到的只有通过setHead方法来修改CLH链表的头信息)
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果p节点不是头节点, 通过本方法中的for循环调用shouldParkAfterFailedAcquire修改p节点的waitStatus值
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 如果p节点的waitStatus值等于SIGNAL, 就是当前线程安全挂起 LockSupport.park(this);
interrupted = true;
}
} finally {
if (failed)
// 如果p是头节点,而且当前线程获取锁资源成功,在这儿就取消当前节点获取锁资源, 合理
cancelAcquire(node);
}
}
/**
*
* 检测和更新accquire资源失败的节点的状态. 如果当前node节点(thread)应该被阻塞,返回true.
* 该方法在循环抢占资源的过程中,主要控制信号量signal
*
* @param pred 当前节点node的前一个节点
* @param node 当前节点
* @return {@code true} 如果当前node节点中的线程应该被挂起,返回true
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前一个节点的waitStatus值
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 当前node节点可以释放状态.表示可以安全的挂起
*/
return true; // 返回true, 当前node节点(thread)会被挂起park,同时也会跳出acquireQueued中for循环
if (ws > 0) {
/*
* 如果pred节点是取消状态, 那么就将pred节点的前一个节点挂载到当前node节点的前一个节点上.
* 循环调用,移除CLH链上所有设置为取消状态的节点
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 将node挂载到pred节点的下一个节点上. (因为CLH中的节点是双向链表,所以需要互相挂载)
pred.next = node;
} else {
/*
* 将pred节点的waitState设置成signal
* 为啥需要这些样呢?
* 配合着acquireQueued方法就不难理解,只能将状态设置成signal, 当前节点node(Thread)才会被挂起park,
* 这时,你会看到线程由Running状态变成了WAIT状态
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; // 返回false时,继续执行acquireQueued方法中的死循环
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
public static void park(Object blocker) {
Thread t = Thread.currentThread(); // 看到没有,当前线程
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
由于上面这几个方法算是lock加锁过程中核心方法,所以,我们debug截图记录
20号线程第一次抢占锁资源失败.
代码继续往下执行,就会进入到 shouldParkAfterFailedAcquire(p,node)方法中. 请注意呀,本次p就是head节点,而node节点中的thread就是20号线程.
由于shouldParkAfterFailedAcquire方法内部逻辑比较简单,咱就不截图了.
第一次进入时,pred的节点中的waitStatus等于0,所以会进入else代码块,然后通过 compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 方法,将p节点的waitStatus值设置成SIGNAL, 返回false,退出shouldParkAfterFailedAcquire方法.
acquireQueued()内部的for(;;)循环继续执行, p还是head节点,然后调用tryAcquire()方法又去抢一次锁,如果此时10号线程释放了锁资源, tryAcquire抢占资源有可能会成功,不过,此时咱们10号线程还持有锁资源,所以20线程本次抢占锁资源肯定会失败.
20号线程第二次抢占锁资源失败.
然后又会进入shouldParkAfterFailedAcquire(p,node)方法, 不过这次p节点的waitStatus值为SIGNAL, 所以该方法返回true.
因为shouldParkAfterFailedAcquire(p,node)方法返回true,所以程序就可以进入到parkAndCheckInterrupt()方法了.
请记住,20号线程是卡LockSupport.park方法上的.
接着我们让30号线程来抢占资源
(3) 30号线程抢占锁资源
因为大体逻辑与前面20线程抢占锁资源一样,所以略过一些渣渣逻辑, 看一些关键地方.
1. addWaiter方法
与20号线程不同是,20号线程执行addWaiter方法时,CLH中无没有节点,而此时CLH中的tail节点就是20号线程的node节点.请看载图
逻辑进入到if代码块中, 这儿主要就是干了一件事,将新创建的30号线程node挂到原先20号线程node节点后面,即CLH中,30号线程node成功尾节点
addWaiter方法执行完毕.
2. acquireQueued方法
此时,acquireQueued方法入参节点是30线程节点,由上面链表图或debug截图可知,它的前一个节点node是20号线程节点, 并非head节点. 所以,程序会进入到shouldParkAfterFailedAcquire(p,node)方法. 这次p是20号线程节点, node是30号线程节点. 在该方法内部会将20号线程node的waitStattus修改成SIGNAL,然后通过acquireQueued的for循环进入到parkAndCheckInterrupt()方法. 在该方法中,会将当前线程30号线程park挂起.
到此为止,50个线程的情况是: 10号线程持有锁 , 20号线程和30线程在CLH链表中, 处理WAIT状态.
(4)40号线程抢占锁资源
与前30号线程抢占锁资源真的是完全一样了,只是此时40号线程的node会是CLH链表的tail节点.
到此为此,挂起的线程有20号线程,30号线程,40号线程
下面记录释放锁的流程
(5)10号线程释放锁资源
释放锁资源总的来说,逻辑比较简单
protected final boolean tryRelease(int releases) {
// AQS的state属性值减1
int c = getState() - releases;
// 如果当前线程不等于AQS中的exclusiveOwnerThread线程抛个异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
// 将AQS的exclusiveOwnerThread属性值设置为null
setExclusiveOwnerThread(null);
}
// 修改AQS的state属性值
setState(c);
return free;
}
如果tryRelease方法返回true, 说明10线程释放锁资源成功.如果这时50号线程去抢占锁资源,完成有可能抢锁成功. 对于20,30,40这个三个线程而言,它们仍然处于park状态,并没有被唤醒. 为了验证上面所说的,我现在让50号线程去抢占锁资源.
所以, 上面的结论完全正确. 那么release()方法后面的代码(标红部分)是干嘛的呢?
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
咱们接着分析unparkSuccessor方法
private void unparkSuccessor(Node node) {
// 头节点的waitStatus值
int ws = node.waitStatus;
if (ws < 0)
// 如果头节点waitStatus值小于0,通过cas设置成0
compareAndSetWaitStatus(node, ws, 0); // 获取头节点的下一个节点s
Node s = node.next;
// 如果下一个节s是null 或者是取消状态,那么就需要找出真正可以唤醒unpark的线程节点
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唤醒s节点的thread
LockSupport.unpark(s.thread);
}
F8执行之后,咱们debug切换到20号线程.
然后这个20号线程仍然会在acquireQueued方法中运行,尝试着去抢占锁资源. 大家别忘acquireQueued方法中的for死循环哟~~~~~~~~~~就是靠着这个死循环去抢锁资源的.......
当然AQS不会让它一直去抢锁资源,它会通过waitStatus这个属性值去控制, 如果当前线程的waitStatus值是SIGNAL,那么对不起,就不能让你再去抢资源了,
把你park吧. 等下次有线程将你unpark之后, 你再来抢一次锁资源吧,如果还是抢不到,继续park吧!
注意: 不是所有被unpark的线程都有资格去tryAcquire锁资源,有仅且有是位head节点的下一个节点才有资格.
言归正传,20号线程抢了一次锁资源,但是没抢到,所以继续park吧
(6)让50号线程释放锁资源
下面我让50号线程释放锁资源,让park着的线程去抢占锁资源,看看是啥情况?
按照前面的套路, 这时debug应该切换到20号线程
由于锁资源处于空置状态 , 所以20号线程肯定可以抢占成功.
20号线程抢锁成功, 从而跳出acquireQueued()方法中的死循环, 然后执行自定义的业务逻辑,本例就是20号线程也可以sleep(1000).
还有一点值得注意的是, 20号线程抢到锁了,从会从CLH链表移除到,所以此时CLH链表中是head, 30号线程, 40号线程.
30号线程同理可知, 最后我们来看看AQS是如何来处理CLH链表中的最后一个节点,40号线程的
(7) 40号线程抢占锁资源
本质上与前面没啥不同, 唯一的区别就是,因为40号线程是CLH中最后一个node,它抢占到锁之后,也会从CLH链表中移除.
好, 到这里,AQS的独占锁原理应该是分析明白了, 下面总结一下关键信息点
(8) 总结
- AQS使用 exclusiveOwnerThread 来表示占有锁资源的线程
- AQS使用state表示锁资源是否被占用, 重入锁就是使用state与exclusiveOwnerThread 配合来实现
- AQS会引用两个Node节点,分别是head与tail, head与tail都是懒加载实现的, setHead方法是唯一能修改CLH链表头节点的方法, addWaiter(enq)就是唯一能修改尾节点的方法
- AQS通过head节点去抢占锁资源(其实质是通过head节点去操作head节点的next节点)
- AQS通过head节点去唤醒park线程(因为存在着边际条件, 所以用到Node的waitState属性值作判断)
- 抢到锁资源的node线程节点,会从CLH链表中移除.
- 线程在释放掉锁资源的同时,也会unpark挂起的线程, 而且这个被unpark的线程一定是head节点next指向的线程节点
- 被unpark的线程,拥有一次抢占锁资源的资格,如果抢占失败,又会被park,直到被释放锁资源的线程再次unpark
- CLH链表中,越是靠近head节点的线程,就越早有可能被unpark,然后尝试抢占锁资源
- 每个新加入的线程,都有一次抢占锁资源的机会,如果抢占失败,就会通过addWaiter方法,挂到CLH链表的尾部,这时,它只有被unpark了,才有机会去抢占锁资源
- Node节点的waitStatus有4个属性值, 比较重要就是0和-1, 当waitStatus等于-1时,表示这个线程可以安全的park挂起, 而在unpark时,waitStatus值又会由-1 就成0.
在shouldParkAfterFailedAcquire方法中.将head节点的waitStatus值设置成SIGNAL, 以便于后面park线程
在unparkSuccessor方法中,会将head节点的值由SIGNAL修改成0,然后去unpark挂起的线程
暂时就到这儿吧, 以后有补充了....