纠错与更改所有文章的图床

This commit is contained in:
Dragon
2021-03-22 20:01:15 +08:00
parent cdb811a48a
commit bf0d1bcccd
27 changed files with 767 additions and 642 deletions

View File

@@ -5,10 +5,11 @@ tags:
- AQS源码
categories:
- Java并发
- 原理
keywords: Java并发AQS源码
description: '万字系列长文讲解-Java并发体系-第四阶段-AQS源码解读-[1]。'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png'
abbrlink: 92c4503d
date: 2020-10-26 17:59:42
---
@@ -26,8 +27,8 @@ date: 2020-10-26 17:59:42
* <p>
* 可重入锁:
* 1、可重复可递归调用的锁在外层使用锁之后在内层仍然可以使用并且不发生死锁这样的锁就叫做可重入锁。
* 2、是指在同一个线程在外层方法获取锁的时候再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象)
* 不会因为之前已经获取过还没释放而阻塞
* 2、是指在同一个线程在外层方法获取锁的时候再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个
* 对象)不会因为之前已经获取过还没释放而阻塞
*/
public class ReEnterLockDemo {
@@ -307,8 +308,8 @@ LockSupport底层还是UNSAFE前面讲过
- 形象的理解
线程阻塞需要消耗凭证(permit)这个凭证最多只有1个。
当调用park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用;
如果有凭证,则会直接消耗掉这个凭证然后正常退出;
如果无凭证,就必须阻塞等待凭证可用;
而unpark则相反它会增加一个凭证但凭证最多只能有1个累加无效。
我们用LockSupport来测试下之前的异常场景
@@ -409,11 +410,11 @@ Process finished with exit code 0
**技术翻译:**是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石 通过内置的FIFO队列来完成资源获取线程的排队工作并通过一个int类变量`state`表示持有锁的状态。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0001.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0001.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0011.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0011.png">
AbstractOwnableSynchronizer
AbstractQueuedLongSynchronizer
@@ -425,7 +426,7 @@ AbstractQueuedSynchronizer
AQS是一个抽象的父类可以将其理解为一个框架。基于AQS这个框架我们可以实现多种同步器比如下方图中的几个Java内置的同步器。同时我们也可以基于AQS框架实现我们自己的同步器以满足不同的业务场景需求。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0002.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0002.png">
@@ -433,7 +434,7 @@ AQS是一个抽象的父类可以将其理解为一个框架。基于AQS这
加锁会导致阻塞:有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0003.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0003.png">
1、抢到资源的线程直接使用办理业务抢占不到资源的线程的必然涉及一种**排队等候机制**,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
@@ -514,7 +515,7 @@ Node 的数据结构其实也挺简单的,就是 thread + waitStatus + pre + n
## AQS队列基本结构
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0004.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0004.png">
注意排队队列不包括head也就是后文要说的哨兵节点
@@ -582,7 +583,7 @@ public class AQSDemo {
以这样的一个实际例子说明。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0005.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0005.png">
@@ -643,7 +644,8 @@ public class AQSDemo {
public final void acquire(int arg) { // 此时 arg == 1
/*
1、首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试。因为有可能直接就成功了呢,也就不需要进队 列排队了。
1、首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试。因为有可能直接就成功了呢,也就不需要
进队列排队了。
2、有可能成功的情况就是在走到这一步的时候前面占锁的线程刚好释放锁
*/
if (!tryAcquire(arg) &&
@@ -712,7 +714,8 @@ public class AQSDemo {
/*
1、假设tryAcquire(arg) 返回false那么代码将执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法首先需要执行addWaiter(Node.EXCLUSIVE)
1、假设tryAcquire(arg) 返回false那么代码将执行acquireQueued(addWaiter(Node.EXCLUSIVE),
arg)这个方法首先需要执行addWaiter(Node.EXCLUSIVE)
2、此方法的作用是把线程包装成node同时进入到队列中。参数mode此时是Node.EXCLUSIVE代表独占模式
3、以下几行代码想把当前node加到链表的最后面去也就是进到队列的最后
*/
@@ -728,7 +731,8 @@ public class AQSDemo {
// 用CAS把自己设置为队尾, 如果成功后tail == node 了,这个节点成为排队队列新的尾巴
if (compareAndSetTail(pred, node)) {
/*
1、进到这里说明设置成功当前node==tail, 将自己与之前的队尾相连,上面已经有 node.prev = pred加上下面这句也就实现了和之前的尾节点双向连接了
1、进到这里说明设置成功当前node==tail, 将自己与之前的队尾相连,上面已经有
node.prev = pred加上下面这句也就实现了和之前的尾节点双向连接了
*/
pred.next = node;
// 线程入队了,可以返回了
@@ -761,7 +765,7 @@ public class AQSDemo {
2、C在if逻辑里准备入队进行相应设置后变成下面这样。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0006.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0006.png">
@@ -778,7 +782,8 @@ public class AQSDemo {
Node t = tail;
/*
1、进入这个分支说明是队列为空的这种情况那么就准备初始化一个空的节点new Node()作为排队队列 的head。
1、进入这个分支说明是队列为空的这种情况那么就准备初始化一个空的节点new Node()
作为排队队列的head。
*/
if (t == null) { // Must initialize
/*
@@ -786,15 +791,22 @@ public class AQSDemo {
2、还是一步CAS因为可能是很多线程同时进来呢
*/
if (compareAndSetHead(new Node()))
/*
1、注意这里传的参数是new Node()说明是一个空的节点并不是我们B线程封装的节点这 个空节点只作为占位符称作傀儡节点或者哨兵节点。这个时候head节点的waitStatus==0, 看 new Node()构造方法就知道了。注意new Node()虽然是空节点但他不是null
2、这个时候有了head但是tail还是null设置一下把tail指向head放心马上就有线程要 来了到时候tail就要被抢了
3、注意这里只是设置了tail=head这里可没return哦。所以设置完了以后继续for循环 下次就到下面的else分支了
*/
/*
1、注意这里传的参数是new Node()说明是一个空的节点并不是我们B线程封装的节点
这个空节点只作为占位符,称作傀儡节点或者哨兵节点)。这个时候head节点的waitStatus==0,
看new Node()构造方法就知道了。注意new Node()虽然是空节点但他不是null
2、这个时候有了head但是tail还是null设置一下把tail指向head放心马上就有
线程要来了到时候tail就要被抢了
3、注意这里只是设置了tail=head这里可没return哦。所以设置完了以后继续for
循环下次就到下面的else分支了
*/
tail = head;
} else {
/*
1、下面几行和上一个方法 addWaiter 是一样的,只是这个套在无限循环里,就是将当前线程排到 队尾有线程竞争的话排不上重复排直到排上了再return 【这里看不懂的话就看下面的例子】 */
1、下面几行和上一个方法 addWaiter 是一样的,只是这个套在无限循环里,就是将当前
线程排到队尾有线程竞争的话排不上重复排直到排上了再return
【这里看不懂的话就看下面的例子】
*/
node.prev = t;
if (compareAndSetTail(t, node)) {
@@ -819,7 +831,7 @@ public class AQSDemo {
此时队列变成了下面的样子:
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0007.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0007.png">
3、然后if结束之后继续空的for循环B线程开始了第二轮循环。
@@ -831,11 +843,11 @@ public class AQSDemo {
2、`node.prev = t`进入if之后让B节点的prev指针指向t然后`compareAndSetTail(t, node)`设置尾节点
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0008.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0008.png">
3、CAS设置尾节点成功之后执行if里的逻辑
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0009.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0009.png">
@@ -851,7 +863,8 @@ public class AQSDemo {
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
2、acquireQueued这个方法参数node经过addWaiter(Node.EXCLUSIVE),此时已经进入排队队列队尾
3、注意一下如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话意味着上面这段代码将 进入selfInterrupt()
3、注意一下如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话意味着上面这段
代码将进入selfInterrupt()
4、这个方法非常重要真正的线程挂起然后被唤醒后去获取锁都在这个方法里了
*/
final boolean acquireQueued(final Node node, int arg) {
@@ -861,10 +874,13 @@ public class AQSDemo {
for (;;) {
final Node p = node.predecessor();
/*
1、p == head 说明当前节点虽然进到了排队队列但是是队列的第一个因为它的前驱是head(或者说 是哨兵节点因为head指向了哨兵节点
1、p == head 说明当前节点虽然进到了排队队列但是是队列的第一个因为它的前驱是head
或者说是哨兵节点因为head指向了哨兵节点
2、注意队列不包含head节点head一般指的是占有锁的线程head后面的才称为排队队列
3、所以当前节点可以去试抢一下锁
4、这里我们说一下为什么可以去试试它是排队队列队头所以作为队头可以去试一试能不能拿到 锁,因为可能之前的线程已经释放锁了。如果尝试成功,那它就不需要被挂起,直接拿锁,效率会高
4、这里我们说一下为什么可以去试试它是排队队列队头所以作为队头可以去试一试能不能
拿到锁,因为可能之前的线程已经释放锁了。如果尝试成功,那它就不需要被挂起,直接拿锁,
效率会高
5、tryAcquire已经分析过了, 忘记了请往前看一下就是简单用CAS试操作一下state
*/
if (p == head && tryAcquire(arg)) {
@@ -915,7 +931,9 @@ public class AQSDemo {
/*
1、前驱节点 waitStatus大于0 之前说过大于0说明前驱节点取消了排队。
2、这里需要知道这点进入阻塞队列排队的线程会被挂起而唤醒的操作是由前驱节点完成的。所以下面这块代码说 的是将当前节点的prev指向waitStatus<=0的节点简单说就是为了找个好爹因为你还得依赖它来唤醒呢如 果前驱节点取消了排队,找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的。
2、这里需要知道这点进入阻塞队列排队的线程会被挂起而唤醒的操作是由前驱节点完成的。所以
下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点简单说就是为了找个好爹因为你还
得依赖它来唤醒呢,如果前驱节点取消了排队,找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的。
*/
if (ws > 0) {
/*
@@ -935,8 +953,13 @@ public class AQSDemo {
/*
1、如果进入到这个分支意味着什么前驱节点的waitStatus不等于-1和1那也就是只可能是0-2-3
在我们前面的源码中都没有看到有设置waitStatus的所以每个新的node入队时waitStatu都是0
2、正常情况下前驱节点是之前的 tail那么它的 waitStatus 应该是 0用CAS将前驱节点的 waitStatus设置为Node.SIGNAL(也就是-1),表示我后面有节点需要被唤醒。
3、这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思Doug Lea 注释的是:代表后继节点需要 被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL然后阻塞等待被前驱唤醒。这里涉及的是两个问 题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码
2、正常情况下前驱节点是之前的 tail那么它的 waitStatus 应该是 0用CAS将前驱节点
的waitStatus设置为Node.SIGNAL(也就是-1),表示我后面有节点需要被唤醒
3、这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思Doug Lea 注释的是:代表后继
节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,
我们知道每个node 在入队的时候,都会把前驱节点的状态改为 SIGNAL然后阻塞等待被前驱唤醒。
这里涉及的是两个问 题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着
“waitStatus代表后继节点的状态”这种思路去看一遍源码。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
@@ -993,11 +1016,15 @@ public class AQSDemo {
}
/*
1、接下来说说如果shouldParkAfterFailedAcquire(p, node)返回false的情况
2、仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回 true的原因很简单前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说我都还没给前驱设置-1呢怎么 可能是true呢但是要看到这个方法是套在循环里的所以第二次进来的时候状态就是-1了。
2、仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会
返回true的原因很简单前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说我都还没给前驱
设置-1呢怎么可能是true呢但是要看到这个方法是套在循环里的所以第二次进来的时候状态就是-1了。
3、解释下为什么shouldParkAfterFailedAcquire(p, node)返回false的时候不直接挂起线程
主要是为了应对在经过这个方法后node已经是head的直接后继节点了。
4、假设返回fasle的时候node已经是head的直接后继节点了但是你直接挂起了线程就要走别人唤醒你的那几步代 码。那这里完全可以重新走一遍for循环直接尝试下获取锁可能会更快。注意是可能不代表一定因为你也无法确定 unparkSuccessor释放锁通知后继节点这个方法执行的快慢。但是你多尝试一次获取锁总归是快的。
4、假设返回fasle的时候node已经是head的直接后继节点了但是你直接挂起了线程就要走别人唤醒你的那
几步代码。那这里完全可以重新走一遍for循环直接尝试下获取锁可能会更快。注意是可能不代表一定因为
你也无法确定unparkSuccessor释放锁通知后继节点这个方法执行的快慢。但是你多尝试一次获取锁总归是快的。
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
@@ -1074,7 +1101,8 @@ private void unparkSuccessor(Node node) {
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
1、下面的代码就是唤醒后继节点但是有可能后继节点取消了等待waitStatus==1从队尾往前找找到 waitStatus<=0的所有节点中排在最前面的
1、下面的代码就是唤醒后继节点但是有可能后继节点取消了等待waitStatus==1从队尾往前找
找到waitStatus<=0的所有节点中排在最前面的
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
@@ -1169,7 +1197,7 @@ protected final void setExclusiveOwnerThread(Thread thread) {
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0010.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0010.png">