前言
JDK1.5 后,Java 引入了 juc 包,队列、重入锁、读写锁、信号量、屏障、Future 等等,为线程调度提供了一系列骚操作,而 AQS 为这些骚操作提供了底层支持。
无论怎样的多线程调度,无非就是让线程去竞争同一个资源,竞争成功就运行,竞争失败就挂起,并用一个数据结构维护这些失败者。所以,任何多线程的调度,都是围绕两个问题在讨论: 一如何竞争和释放资源,二怎样维护竞争资源失败的线程。对于前一个问题,线程调度方式不同,竞争和释放资源的方式就不同,那就让调度器自己去定义应该怎样去操作共享资源,而 AQS 主要解决后一个问题。
java.util.concurrent包(之后简称JUC包)中,提供了大量的同步与并发的工具类,是多线程编程的“利器”。AQS是Java并发编程非常重要的一个知识点,通过标题的名字就可以看出来他的难度,笔者通过多方面学习,以作记录,如有问题,随时沟通。
https://www.toutiao.com/a6822199058191352332/
什么是AQS
AQS全称为AbstractQueuedSynchronizer即抽象队列同步器。AQS 是用来构建锁或者其他相关的同步装置的基础框架,内部维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(CLH队列) ,JUC中同步器的实现如 ReentrantLock、 CountDownLatch、SemaPhore、 CyclicBarrier、FutureTask 等都是基于 AQS 框架实现。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
可以这么说,只要搞懂了AQS,那么J.U.C中绝大部分的api都能轻松掌握。
AQS的两种功能
AQS提供了两种形式的同步器:独占和共享
- 独占锁,每次只能有一个线程持有锁,比如给大家演示的ReentrantLock就是以独占方式实现的互斥锁
- 共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
AQS工作原理
实现原理: AQS的实现依赖内部的同步队列(FIFO的双向队列)和共享资源state,当前线程会尝试获取state同步状态(即竞争锁),如果竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒后继的一个阻塞的节点(线程),后继节点尝试获取锁。
AQS的内部结构
先来看看 AQS 有哪些属性,搞清楚这些基本就知道 AQS 是什么套路了,做到心中有数,直接上源码:
// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
private transient volatile Node head;
// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
private transient volatile Node tail;
// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
private volatile int state;
// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
看样子应该是很简单的吧,毕竟也就四个属性啊。AbstractQueuedSynchronizer 的等待队列示意如下所示,注意了,之后分析过程中所说的 queue,也就是阻塞队列 不包含 head,不包含 head,不包含 head。
等待队列中每个线程被包装成一个 Node 实例,数据结构是链表,一起看看源码吧:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 7373984972572414691L;
protected AbstractQueuedSynchronizer() { }
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等待状态,只有其他线程调用了Condition
* 的signal()后,该节点由等待队列进入到同步队列,尝试获取同步状态
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步状态获取将会无条件传播下去
*/
static final int PROPAGATE = -3;
/**
* 等待状态:
* SIGNAL: 表示这个节点的后继节点被阻塞,到时需要通知它。
* CANCELLED: 由于中断和超市,这个节点处于被删除的状态。处于被删除状态的节点不会转
* 换成其他状态,这个节点将被踢出同步队列,被GC回收。
* CONDITION: 表示这个节点再条件队列中,因为等待某个条件而被阻塞。
* PROPAGATE: 使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件
* 传播.
* 0: 新节点会处于这种状态。
* 这些int常量是给waitStatus用的,取值为上面的1、-1、-2、-3,或者0
* 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
* ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。
*/
volatile int waitStatus;
/**
* 队列中,节点的前一个节点
*/
volatile Node prev;
/**
* 节点的后继节点.
*/
volatile Node next;
/**
* 节点所拥有的线程.
*/
volatile Thread thread;
/**
* 条件队列中,节点的下一个等待节点.
*/
Node nextWaiter;
/**
* 判断节点时候是共享模式.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 获取前一个节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
}
AQS 的核心主要围绕,一个 volatile 修饰的成员域 state,一个 FIFO(先进先出,队尾加入,对头删除)队列展开。
其中同步状态(state)以及操作(主要用于标记同步器的状态),源码定义如下
-
state=0: 说明没有任何线程占有共享资源的锁。
-
state = 1或其他 说明有线程正在使用共享变量,其他线程必须加入同步队列进行等待。
private volatile int state;
protected final int getState()
protected final void setState(int newState)
protected final boolean compareAndSetState(int expect, int update)
其中队列分为两种:
- CLH同步等待队列: CLH队列是Craig,Landin and Hagersten三人发明的一种基于双向链表数据结构的队列,功能: 是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。运行机制: 申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。Java中的CLH队列是原CLH队列的一种变种,线程由原自旋机制改为阻塞机制,FIFO先入先出线程等待队列如图
- Condition条件队列: 它是一个单向链表,只有当程序中需要使用到condition的时候,才会存在condition单向链表,并且可能会有多个condition queue ,它不是必须存在的。
操作原理: 1.当获取到锁的线程对应的条件变量的await()方法被调用的时候,该线程就会释放锁,并把当前线程转为Node节点放到条件变量对应的条件队列中 同时底层调用LockSupport.park()挂起当前线程;2.直到有一个线程调用了条件变量的signal()或者signalAll()方法时,就会把条件队列中一个或者所有的节点都移动到AQS阻塞队列中,然后调用unpark方法进行授权,就等着获得锁;
AQS的同步队列操作示意:队列由Node组成,Node能获取其代表的Thread
上面的是基础知识,后面会多次用到,心里要时刻记着它们,心里想着这个结构图就可以了。再次强调,我说的阻塞队列不包含 head 节点。
自定义同步器
AQS使用了模板方法模式,自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,自定义同步器时需要重写下面几个AQS提供的模板方法:
- isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
AQS实现源码解析
一、获取锁过程
(1)acquire(int)
此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:
//独占模式获取资源
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
说明: 独占模式下获取资源/锁,忽略中断的影响。内部主要调用了三个方法,其中tryAcquire需要自定义实现。后面会对各个方法进行详细分析。acquire方法流程如下:
- tryAcquire() 尝试直接获取资源,如果成功则直接返回,失败进入第二步;
- addWaiter() 获取资源失败后,将当前线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued() 使线程在等待队列中自旋等待获取资源,一直获取到资源后才返回。如果在等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断(interrupt)是不响应的,在获取资源成功之后根据返回的中断状态调用 selfInterrupt() 方法再把中断状态补上。
(2)tryAcquire(int)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
什么?直接throw异常?说好的功能呢?好吧,还记得概述里讲的AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现吗?就是这里了!!!AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。
说明: 尝试获取资源,成功返回true。具体资源获取/释放方式交由自定义同步器实现。ReentrantLock中公平锁的实现如下:
//公平锁
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;
}
(3)addWaiter(Node)
此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。还是上源码吧:
// 此方法的作用是把线程包装成node,同时进入到队列中
// 参数mode此时是Node.EXCLUSIVE,代表独占模式
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加到链表的最后面去,也就是进到阻塞队列的最后
Node pred = tail;
// tail!=null => 队列不为空(tail==head的时候,其实队列是空的,不过不管这个吧)
if (pred != null) {
// 将当前的队尾节点,设置为自己的前驱
node.prev = pred;
// 用CAS把自己设置为队尾, 如果成功后,tail == node 了,这个节点成为阻塞队列新的尾巴
if (compareAndSetTail(pred, node)) {
// 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,
// 上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了
pred.next = node;
// 线程入队了,可以返回了
return node;
}
}
// 仔细看看上面的代码,如果会到这里,
// 说明 pred==null(队列是空的) 或者 CAS失败(有线程在竞争入队)
// 读者一定要跟上思路,如果没有跟上,建议先不要往下读了,往回仔细看,否则会浪费时间的
enq(node);
return node;
}
(3.1) enq(Node)
到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的。相信你一眼便看出这段代码的精华。CAS自旋volatile变量,是一种很经典的用法。
// 采用自旋的方式入队
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 之前说过,队列为空也会进来这里
if (t == null) { // Must initialize
// 初始化head节点
// 细心的读者会知道原来 head 和 tail 初始化的时候都是 null 的
// 还是一步CAS,你懂的,现在可能是很多线程同时进来呢
if (compareAndSetHead(new Node()))
// 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
// 这个时候有了head,但是tail还是null,设置一下,
// 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
// 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return
// 所以,设置完了以后,继续for循环,下次就到下面的else分支了
tail = head;
} else {
// 下面几行,和上一个方法 addWaiter 是一样的,
// 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
(4)acquireQueued(Node, int)
OK,通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样! 是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事: 在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键,还是上源码吧:
注意一下: 如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,意味着上面这段代码将进入selfInterrupt(),所以正常情况下,下面应该返回false。这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了
1 final boolean acquireQueued(final Node node, int arg) {
2 boolean failed = true;//标记是否成功拿到资源
3 try {
4 boolean interrupted = false;//标记等待过程中是否被中断过
5
6 //又是一个“自旋”!
7 for (;;) {
8 final Node p = node.predecessor();//拿到前驱
9 //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
10 if (p == head && tryAcquire(arg)) {
11 setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
12 p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
13 failed = false; // 成功获取资源
14 return interrupted;//返回等待过程中是否被中断过
15 }
16
17 //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
18 if (shouldParkAfterFailedAcquire(p, node) &&
19 parkAndCheckInterrupt())
20 interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
21 }
22 } finally {
23 if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
24 cancelAcquire(node);
25 }
26 }
说明: p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head,注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列,所以当前节点可以去试抢一下锁。
这里我们说一下,为什么可以去试试:
- 首先,它是队头,这个是第一个条件
- 其次,当前的head有可能是刚刚初始化的node, enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程
也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,tryAcquire已经分析过了, 忘记了请往前看一下,就是简单用CAS试操作一下state
(4.1)shouldParkAfterFailedAcquire(Node, Node)
这个方法说的是:"当前线程没有抢到锁,是否需要挂起当前线程?"此方法主要用于检查状态,看看自己是否真的可以去休息了(此处状态需参考前面定义的状态)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的状态
if (ws == Node.SIGNAL) //表示这个节点的后继节点被阻塞,到时需要通知它。
//前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
return true;
if (ws > 0) { // 前驱节点 waitStatus大于0 ,说明前驱节点取消了排队。
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 进入这个分支意味着,前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
// 在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0
// 正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0
// 用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1)
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
(4.2)parkAndCheckInterrupt()
如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
1 private final boolean parkAndCheckInterrupt() {
2 LockSupport.park(this);//调用park()使线程进入waiting状态
3 return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
4 }
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
(5)小结
到此,线程获取锁部分介绍完了,我们接下来再回到acquire()!
//独占模式获取资源
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
流程回顾:
- 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
- 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。
- 如果在整个等待过程中被中断过,则返回true,否则返回false。如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
二、释放锁的过程
2.1 release(int)
上一小节已经把acquire()说完了,这一小节就来讲讲它的反操作release()吧。此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。下面是release()的源码:
1 public final boolean release(int arg) {
2 if (tryRelease(arg)) {
3 Node h = head;//找到头结点
4 if (h != null && h.waitStatus != 0)
5 unparkSuccessor(h);//唤醒等待队列里的下一个线程
6 return true;
7 }
8 return false;
9 }
逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!
2.1.1 tryRelease(int)
此方法尝试去释放指定量的资源。下面是tryRelease()的源码:
1 protected boolean tryRelease(int arg) {
2 throw new UnsupportedOperationException();
3 }
跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
ReentrantLock 实现tryRelease方法:
// 回到ReentrantLock看tryRelease方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否完全释放锁
boolean free = false;
// 其实就是重入的问题,如果c==0,也就是说没有嵌套锁了,可以释放了,否则还不能释放掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2.1.2 unparkSuccessor(Node)
此方法用于唤醒等待队列中下一个线程。下面是源码:
// 唤醒后继节点,从上面调用处知道,参数node是head头结点
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
//下面的代码就是唤醒后继节点,从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
这个函数并不复杂。一句话概括: 用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!!And then, DO what you WANT!
3.小结
release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
最后,说下在AQS中阻塞队列和条件队列有什么关系?
1.当多个线程调用lock.lock()方法的时候,只有一个线程获取到可锁,其他的线程都会被转为Node节点丢到AQS的阻塞队列中,并做CAS自旋获取锁;
2.当获取到锁的线程对应的条件变量的await()方法被调用的时候,该线程就会释放锁,并把当前线程转为Node节点放到条件变量对应的条件队列中;
3.这个时候AQS的阻塞队列中又会有一个节点中的线程能得到锁了,如果这个线程又恰巧调用了对应条件变量的await()方法时,又会重复2的步骤,然后阻塞队列中又会有一个节点中的线程获得锁
4.然后,又有一个线程调用了条件变量的signal()或者signalAll()方法,就会把条件队列中一个或者所有的节点都移动到AQS阻塞队列中,然后调用unpark方法进行授权,就等着获得锁了;
一个锁对应一个阻塞队列,但是对应多个条件变量,每一个条件变量对应一个条件队列;其中,这两种队列中存放的都是Node节点,Node节点中封装了线程及其状态,下图所示:
至此 AQS获取锁和释放锁源码分析完了,共享锁模式不在单独介绍,与独占类似,欢迎交流。文章参考:
AQS入门:https://javadoop.com/post/AbstractQueuedSynchronizer
Java并发之AQS详解:https://www.cnblogs.com/waterystone/p/4920797.html
JUC源码分析—AQS:https://www.jianshu.com/p/a8d27ba5db49
https://maimai.cn/article/detail?fid=1381545997&efid=1Fj4YnXVqDnE1KsK0bsJ3A&use_rn=1
条件队列参考:
Condition入门:https://javadoop.com/post/AbstractQueuedSynchronizer-2
https://www.toutiao.com/i6789115835085488643/?in_ogs=2&traffic_source=CS1111&utm_source=MI&source=search_tab&utm_medium=wap_search&original_source=2&in_tfs=MI&channel=
https://www.toutiao.com/i6627328860121727502/?in_ogs=2&traffic_source=CS1111&utm_source=MI&source=search_tab&utm_medium=wap_search&original_source=2&in_tfs=MI&channel=
https://coderbee.net/index.php/concurrent/20131115/577