重入锁ReentrantLock与AQS
Posted gonghaiyu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重入锁ReentrantLock与AQS相关的知识,希望对你有一定的参考价值。
重入锁ReentrantLock与AQS
几个基本概念
park与unpark
阻塞与唤醒。这个在UnSafe章节已经详细讲解。park相当于将线程挂起。将当前执行线程上下文从CPU切换到缓存保存,将CPU让出执行其他任务。
公平锁与非公平锁
在执行lock操作时,公平锁会先去判断阻塞队列当中有没有线程,如果有,将该线程放入到队尾,所有等待线程先后执行。而非公平锁的操作是,某个线程尝试去获取锁的时候,先执行CAS看是否可以将状态为从0改为1,也就是说,非公平锁拿到锁的这个线程是随机的。
公平锁的优点是等待锁的线程按顺序执行,缺点是吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
羊群效应
羊群效应就是多个线程同时竞争同一个锁时,假设锁被某个线程占用,如果多个线程是成千上万,当锁被释放时,同时唤醒这么多线程去竞争刚释放的锁,这时就会发生羊群效应。海量的竞争必然造成CPU、内存资源的剧增与浪费。最终却只有一个线程能获取成功,其他线程还是得老老实实回到等待状态。
AQS的FIFO等待队列就是用来解决羊群效应问题的。AQS中维持一个等待队列,队列每个节点只关心其前面节点的状态 ,线程唤醒也只能唤醒队头的等待线程。这个思路已经Zookeeper的分布式锁的改进方法中应用。
ReentrantLock例子
因为ReentrantLock相比于synchronized具有对共享资源竞争时,有可中断、可限时、公平锁的特点。所以,下面例子特意考虑可中断、可限时的使用。假设有5只猴子抢吃一袋香蕉,该袋中装有10根香蕉。规定如下,每抢到袋子,从中取出一根香蕉。然后让出袋子给其他猴子抢,包括自己。代码如下。
public class ReentrantLockTest {
volatile static int bananas = 10;//一个包含10根香蕉的袋子
static Lock lock = new ReentrantLock(true);//设置公平锁
static Condition grabCondition = lock.newCondition();
static ExecutorService executorService = Executors.newCachedThreadPool();
static boolean putBack = true;
private static void grabAndPutBack() throws InterruptedException {
lock.lock();
if (!putBack) {
try {
grabCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (bananas <= 0) {
System.exit(1);//当香蕉数<0时,强制进程退出。
return;
}
System.out.println(Thread.currentThread().getName() + " get the bananas " + bananas);
bananas--;
Thread.sleep(new Random().nextInt(500));//模拟随机执行时长
putBack = true;
System.out.println(Thread.currentThread().getName() + " put back the left bananas " + bananas);
grabCondition.signalAll();
lock.unlock();
}
public static void main(String[] args) {
IntStream.range(0, 5).forEach(i -> executorService.submit(() -> { //用5个线程表示5只猴子
try {
System.out.println(Thread.currentThread().getName() + " begin to grab banana");
do {
grabAndPutBack();
} while (bananas > 0);
} catch (Exception e) {
e.printStackTrace();
}
}));
executorService.shutdown();
}
}
这个案例很有趣,当设置new ReentrantLock(false)
非公平锁时,发现了全部香蕉都被第一个占用香蕉袋的猴子抢去了。这个就是可重入锁的缺点,就是让其他的4只猴子一直在等待,第一个拿到香蕉的猴子因为它的可重入性(可重入性,就是当获得锁的线程解锁后,重新来获取锁的时候会判断自己以前是否获取过锁,如果获取过就无需竞争,直接获取),不需要获取锁,直接拔掉所有香蕉。这样显然惹毛了其他猴子,占用CPU资源。
但当设置为公平锁时,会发现猴子们很有礼貌,抢到香蕉后就转给排在后面的猴子。这个就是等待队列节点。每个节点其实维护了一个线程及获取共享资源的状态信息。
这里还有个问题,就是为啥系统要强制退出?因为系统的主线程检测到banana数量为0时,退出当前的while循环。
下面以追根溯源的方式分析ReentrantLock的内部结构。因为重入锁ReentrantLock内部使用了AQS对共性资源进行控制访问。将ReentrantLock与AQS放在一起进行探讨比较合适。以下是ReentrantLock的类图。
从上图我们可以看出,一共包含以下几个核心类。
-
可重入锁类ReentrantLock:实现了Lock接口,内部类有Sync、NonfairSync、FairSync,构造ReentrantLock时可以指定是非公平锁(NonfairSync)还是公平锁(FairSync)。
-
同步锁Sync:抽象类,也是ReentrantLock内部类,因为无论是非公平锁还是公平锁,释放锁的逻辑都一样,所以,Sytnc自己实现了tryRelease方法,但获取锁逻辑不一样,所以tryAccquire方法由它的子类NonfairSync、FairSync自己实现。
-
抽象队列同步器AbstractQueuedSynchronizer(AQS):抽象类,代码中却没有一个抽象方法,其中获取锁(tryAcquire方法)和释放锁(tryRelease方法)并没有提供默认实现,需要子类重写这两个方法实现具体逻辑。
-
节点对象Node:AQS的内部类,本质上是一个双向链表,每个节点内部维持了一个获取锁的线程。
-
ConditionObject: 提供了条件锁的同步实现,实现了Condition接口。
下面详细讲解每个类的内部逻辑。
Lock接口
Lock接口是在JDK1.5中引入的,提供了比synchronized方法更易于扩展的锁操作。Lock允许更多的结构,可以有不同的属性,也可以支持多个相关的Condition对象。所以,Lock实现了比synchronized方法和语句块更加广泛的操作。比如,synchronized只能是非公平锁。
Lock是多线程控制访问共享资源的工具,Lock接口提供了独占访问共享资源的方式:在某个时刻仅仅一个线程能获取锁,对共享资源的所有访问都必须获取锁。但是有些Lock接口的实现能并发地访问共享资源,比如可重入读写锁ReentrantReadWriteLock,内部维持了两个实现了Lock接口的锁,一个是读锁 ReadLock,一个是写锁 WriteLock。通过两个锁实现了读读不互斥,读写、写写互斥。所以,很适合读多写少的情况,但是如果读太多,将导致写线程"饥饿",长时间修改不了共享数据。所以JDK1.8中提供了 StampedLock类,它是读写锁的一个改进版本。采用一种乐观方式的读策略,读时完全不会阻塞写操作。
Lock虽然灵活,但也带来了额外的责任。需要自己在代码中释放锁。在大多数情况下,应使用以下代码块。一定将释放锁放入finally中,以确保在必要时释放锁。
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
Lock接口主要包括获取锁、释放锁和获取锁条件对象。
接口方法 | 作用 |
---|---|
void lock() | 获取锁,如果获取不到锁,则当前线程挂起,处于休眠状态,直到获取锁为止。在Lock的实现类中,使用锁时能检测到错误倾向,如果调用导致死锁,可能会抛出一个未检查异常,建议在Lock锁的实现类中对异常处理写上注释。 |
void lockInterruptibly() throws InterruptedException | 可被中断获取锁,如果锁当前不可用,当前线程进行等待,直到(1)当前线程获取锁,(2)其他线程中断了当前线程,并且能支持锁的中断捕获。 |
boolean tryLock() | 非阻塞获取锁,没有获取到锁不会一直等待,会立即返回。 |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 获取锁,在给定的等待时间内获取锁就返回true,否则给定时间内拿不到锁,则返回false。 |
void unlock() | 释放锁。释放锁的实现通常会对线程释放加以限制:只有锁的持有者才能释放锁。如果违反了这条限制,则抛出检查时异常。 |
Condition newCondition() | 返回绑定到此锁实例的Condition实例。在等待条件之前,锁必须由当前线程持有,Condition.await()方法将原子性释放锁,并在等待返回前重新获取锁。 |
通过上面的方法了解到获取锁共有三种方式,分别是可中断、不可中断和定时。下面举个例子对可中断获取锁进行分析。有A、B两个线程同时通过Lock#lockInterruptibly阻塞获取某个锁时,如果此时A线程获取到了锁,则线程B只有继续等待;此时,A线程对B线程调用threadB.interrupt()方法,能够中断线程B的等待,让线程B可以先做其他事情。但是如果用内置锁synchronized,当一个线程处于等待某个锁的状态时,是无法被中断的,只有一直等待。这就是中断锁的灵活之处。注意,一个线程获取了锁之后,相当于已经切到CPU中执行了,是不会被Thread#interrupt()方法中断的。
Condition接口
Condition接口提供了一组与Object的本地监视器方法(wait,notify和notifyAll)功能相似的方法,Object的native监测方法是配合对象监测器在JAVA底层完成线程间的等待/通知机制。而Condition与Lock配合,是基于Java语言层面完成的等待/通知机制。Condition监测方法可以分多个对象,与Lock配合可以完成一个对象具有多个等待集的等待通知模式。Condition只能通过Lock#newCondition()方法获取,所以Condition是依赖于Lock的,而在调用这个方法之前,线程需要先获得锁。同时,在一个Lock中,可以获取多个Condition对象。
通过对比Object的监视器方法和Condition接口,可以更详细地了解Condition的特性,对比如下:
对比项 | Object Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 1.调用Lock.lock()获取 2.调用Lock.newCondition()获取Condition对象 |
调用方式 | 直接调用,如:object.wait() | 直接调用,如:condition.await() |
等待队列个数 | 一个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,在等待状态中不响应终端 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将来的某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
Condition也叫条件队列或者条件变量。为一个线程提供了暂停执行(等待)直到另一个线程通知某些条件状态现在为true时的方法。因为访问这种共享状态信息发生在多个不同的线程,必须为protected类型。所以某种形式的锁与条件相关。等待为一个条件提供的关键属性自动释放相关的锁并悬挂当前线程,像Object.wait()方法一样。
一个锁可以有多个条件,每个条件上可以有多个线程等待,通过调用await()方法,可以让线程在该条件下等待。当调用signalAll()方法,又可以唤醒该条件下的等待的线程。
一个Condition实例本质上绑定到一个锁,要为一个特定的锁获取一个Condition实例可以使用newCOndition()方法。
如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
condition可以通俗的理解为条件队列。当一个线程在调用了await方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
AbstractQueuedSynchronizer
AQS是JDK1.5提供的一个基于FIFO等待队列实现的同步器基础框架。JUC包里几乎所有有关的锁、多线程并发及线程同步都是基于AQS这个框架。AQS基于模板设计模式实现。整个类没有任何一个abstract的抽象方法,而是父类写有一个模板,需要子类去实现那些方法。否则直接调用父类的方法会抛出UnsupportedOperationException异常来提醒子类去修改。而AQS本身的核心思想是基于volatile int state属性配合Unsafe类的CAS原子性的操作来实现。当state的值为0时,表示该锁不被任何线程占有。 AQS同步器提供独占锁和共享锁两种方式来实现资源共享。
AQS是采用模板方法的设计模式构建的,它作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式(如Semaphore)与独占模式(如Reentrantlock,这两个模式的本质区别在于多个线程能不能共享一把锁),而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用:也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,这样做的好处是显而易见的,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁解锁的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可。
AQS定义两种资源共享方式:Exclusive(独占、只有一个线程执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
- 独占式资源共享
在谈synchronized的资源共享实现方式的时候,当线程A访问共享资源的时候,其它的线程全部被堵塞,直到线程A读写完毕,其它线程才能申请同步互斥锁从而访问共享资源。
以RenentrantLock为例,如何知道共享资源是否有线程正在被访问呢?其实,它有一个state变量初始值为0,表示未锁定状态。当线程A访问的时候,state+1,就代表该线程锁定了共享资源,其他线程将无法访问,而当线程A访问完共享资源以后,state-1,直到state等于0,就将释放对共享变量的锁定,其他线程将可以抢占式或者公平式争夺。当然,它支持可重入,那什么是可重入呢?同一线程可以重复锁定共享资源,每锁定一次state+1,也就是锁定多次。说明:锁定多少次就要释放多少次。
- 共享式资源共享
以CountDownLatch为例,共享资源可以被N个线程访问,也就是初始化的时候,state就被指定为N(N与线程个数相等),线程countDown()一次,state会CAS减1,直到所有线程执行完(state=0),那些await()的线程将被唤醒去执行执行剩余动作。
- 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功就返回。
- 没成功,则addWaiter()将线程加入等待队列的尾部,并标记为独享模式。
- acquireQueued()使线程在等待队列中休息,有机会时会去尝试获得资源。获得资源后返回。如果整个过程有中断过返回true,否则返回false。
- 如果线程在等待过程中中断过,它是不响应的。只是获得资源后才再进行自我中断selfInterrupt(),将中断补上。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r0wzk85b-1607175685230)(file:///C:/Users/ADMINI~1/AppData/Local/Temp/enhtmlclip/721070-20151102145743461-623794326.png)]
共享式流程(类似于独占式 ):
- tryAcquireShared()尝试获取资源,成功则直接返回。
- 失败则通过 doAcquireShared()进入等待队列,直到被唤醒或者中断并且成功获取资源才返回。
- 不同:独占式是只唤醒后继节点。共享式是唤醒后继,后继还会去唤醒它的后继,从而实现共享。
而 ReentrantReadWriteLock 实现了共享锁功能。这篇文章主要是从ReentrantLock的使用入手去分析AQS独占式锁的实现。
同步器AQS内部结构
static final class Node; // 维护由线程构成的节点
private transient volatile Node head; //线程节点的头结点
private transient volatile Node tail; //线程节点的尾节点
private volatile int state; //状态位 0 - 未被任何线程持有 等于1 已经有线程持有,大于1 重入锁次数
//父类AbstractOwnableSynchronizer
private transient Thread exclusiveOwnerThread;//父类AOS保持独占线程
AQS的同步队列基于FIFO的双向链表实现,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个Node其实是由线程封装,当线程争抢锁失败后会封装成Node加入到AQS队列中去。AQS中内部维护了一个头节点,尾节点,当前线程执行的state状态,当前持有锁的线程。
AQS内部通过state来控制同步状态,当执行lock时,如果state=0时,说明没有任何线程占有共享资源的锁,此时线程会获取到锁并把state设置为1;当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列(FIFO的双向队列)进行等待,并挂起当前线程。当获得锁的线程释放后,就会调用unlock方法,会从队列中唤醒下一个被挂起的节点(线程)。以下是AQS的内部结构。
阿里P7架构师带你深入分析AQS实现原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6btltHXG-1607175960329)(https://image.520mwx.com/static/c8fad1bddf6fb238550f222872772123.jpg)]
这是因为AQS只是作为一个基础组件,从上图可以看出countDownLatch等并发组件都依赖了它,它并不希望直接作为直接操作类对外输出,而更倾向于作为一个基础并发组件,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等。
Node内部类
AQS内部同步队列中的每个节点对象为Node对象。每个Node维护了线程、前后Node的指针和当前节点的等待状态等参数。
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; // 等待触发状态
static final int CONDITION = -2; // 等待唤醒条件
static final int PROPAGATE = -3; // 节点状态需要向后传播
volatile int waitStatus; // 等待状态有4种取值(除掉初始化状态)
//双向链表 这两个指针用于同步队列构建链表使用的 下面还有一个 nextWaiter 是用来构建等待单链表队列
volatile Node prev; //当前节点的前驱节点
volatile Node next;//当前节点的后继节点
volatile Thread thread; // 当前节点持有的线程,Node是对线程的封装,AQS实质上用Node构建了双向队列的线程,维护了线程的同步队列
Node nextWaiter;//存储在条件等待队列中的后继节点
关于waitStatus的状态说明:
0状态 为初始化状态。
CANCELLED = 1 , 当前节点为取消状态。在同步队列中等待的线程超时或者被中断,会将该节点的waitStatus的状态置为1。大于0表示异常线程。
SIGNAL = -1, 后继节点处于等待状态。即等待触发转到同步状态。如果当前节点的线程释放了同步状态或者是被取消,会通知后继状态为SIGNAL的节点。 当前节点的后继节点被PARK,当前节点释放时,必须调用UNPARK通知后面节点,当后面节点竞争时,会将前面节点更新为SIGNAL。
CONDITION = -2, 该Node的线程处于等待队列中(注意不是同步队列),当其他线程调用了Condition的signal() 方法后,该线程从等待队列当中移动到同步队列中,等待获取同步锁。
PROPAGATE = -3, 在共享模式中,该状态的Node的线程处于可运行状态。 共享模式下释放节点时设置的状态,被标记为当前状态是表示无限传播下去 。
nextWaiter: 等待队列中的后继节点,如果当前节点是共享的,nextWaiter是 SHARED常量,也就是说节点类型和等待队列中的后继节点共用同一个字段 。
这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。注意两种队列的作用和转换。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-odQzdxDZ-1607175685233)(https://image.520mwx.com/static/c8fad1bddf6fb238550f222872772123.jpg)]
AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个Node其实是由线程封装,当线程争抢锁失败后会封装成Node加入到AQS队列中去。
AQS内部分为共享模式(如Semaphore)和独占模式(如Reentrantlock),无论是共享模式还是独占模式的实现类,都维持着一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成Node结点并将线程当前必要的信息存储到node结点中,然后加入同步队列等会获取锁,而这系列操作都有AQS协助我们完成,这也是作为基础组件的原因,无论是Semaphore还是Reentrantlock,其内部绝大多数方法都是间接调用AQS完成的。
接下来我们看详细实现。
这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。
ConditionObject内部类
ConditionObject是AQS的内部类。在ConditionObject内部维护了一个单链的等待队列。在这个类中使用了两个指针firstWaiter和lastWaiter。这里主要看下await()、signal()和signalAll()方法。
从上面的实例中得知,我们可以通过Condition.await()将线程加入到等待队列。
关键成员变量
/** 等待队列的第一个节点. */
private transient Node firstWaiter;
/** 等待队列的最后一个节点. */
private transient Node lastWaiter;
/** 等待到退出后状态的是重新中断模式 */
private static final int REINTERRUPT = 1;
/** 等到到退出后状态是抛出InterruptedException异常模式 */
private static final int THROW_IE = -1;
等待队列
Condition是AQS的内部类。每个Condition对象都包含一个队列(等待队列)。等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。AQS有一个同步队列和多个等待队列,节点都是Node。等待队列的基本结构如下所示。
当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点加入到等待队列尾部。将Condition的lastWaiter修改指向为最新的队尾节点。 因为调用await()方法必须在lock.lock之后操作(参考示例代码),所以前面的操作不需要CAS保证。是线程安全的操作。
当线程调用了Condition的await()方法以后。线程就作为队列中的一个节点被加入到等待队列中去了。同时会释放锁的拥有。当从await方法返回的时候。当前线程一定会获取condition相关联的锁。
如果从队列(同步队列和等待队列)的角度去看await()方法,当调用await()方法时,相当于同步队列的首节点(获取锁的节点)移动到Condition的等待队列中。
调用该方法的线程成功的获取锁的线程,也就是同步队列的首节点,该方法会将当前线程构造成节点并加入到等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
当等待队列中的节点被唤醒的时候,则唤醒节点的线程开始尝试获取同步状态。如果不是通过 其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException异常信息。因为在lock.lock中,已经将所有线程加入到了同步队列中。
await方法
关于await, 当线程调用了await方法以后。线程就作为队列中的一个节点被加入到等待队列中去了。同时会释放锁的拥有。当从await方法返回的时候。一定会获取condition相关联的锁。当等待队列中的节点被唤醒的时候,则唤醒节点的线程开始尝试获取同步状态。如果不是通过 其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException异常信息。
public final void await() throws InterruptedException {
//等待可中断
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();//加入等待队列队尾
//释放锁将lock等待队列的下一个节点进行unpark通知
int savedState = fullyRelease(node);
int interruptMode = 0;
//循环判断是否在AQS的等待队列中,不在就进行park动作。之后不论是被unpark唤醒,还是中断,均会跳出次循环。
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//运行到此处说明已经被唤醒了,因为结束了循环。
//唤醒后,首先自旋获取锁,同时判断是否当前线程被中断了
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//清理队列中的状态不是Condition的任务,包括被唤醒的SIGNAL和被取消的CANCELLED任务
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//被中断,抛出异常
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
加入到等待队列。
/**
* 添加到等待队列
* @return 返回新节点
*/
private Node addConditionWaiter() {
Node t = lastWaiter;//获取条件队列中的最后节点
// 如果lastWaiter被取消,则把它清理干净.
if (t != null && t.waitStatus != Node.CONDITION) {//条件队列尾部节点状态不为等待,则从队列中删除
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);//用当前线程新建节点
if (t == null)//最后节点为空,表示等待队列中还没有节点
firstWaiter = node;//则当前节点为第一个节点
else
t.nextWaiter = node;//否则等待队列中有节点,增加该节点到队尾
lastWaiter = node;//等待队列中的lastWaiter指向新的队尾节点
return node;
}
signal/doSignal/signalAll方法
这几个方法都是唤醒在Condition上等待的线程。如上面示例,signalAll()就是唤醒等待队列中的所有线程。 Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
signal方法
调用signal()方法首先判断是否获取到了独占锁,如果没有获取到就抛出异常。这说明只有获取了独占锁的线程才能执行signal操作。然后获取等待队列中的第一个节点,也是等待最长时间的节点执行doSignal。
public final void signal() {
//获取独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取等待队列中的第一个等待节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
doSignal方法
doSignal主要操作有:将其移动到同步队列并且利用LockSupport唤醒节点中的线程。节点从等待队列移动到同步队列如下图所示:
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
/**
* 将头节点从等待队列移动到同步队列,移动成功返回true,否则表示节点在通知前已经被取消了
*/
final boolean transferForSignal(Node node) {
/*
* 在等待队列中的节点只有condition和cancelled两种状态,如果waitStatus状态不为CONDITION,说明任务已被取消。则更新失败。
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
*将当前节点加入到同步队列上,并尝试设置前置任务的waitStatus,以指示线程(可能)正在等待。如果取
*消或尝试设置waitStatus失败,则唤醒以重新同步(在这种情况下,waitStatus可能会暂时错误但无害)。
*/
Node p = enq(node);//此方法在同步期中已经讲解,将node节点加入到同步队列
int ws = p.waitStatus;//获取新节点的状态
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(nodeAQS源码探究_01 手写一个简化的ReentrantLock可重入锁
ReentrantLock核心源码分析,AQS独占模式,可重入锁
Java并发-- ReentrantLock 可重入锁实现原理1 - 获取锁
并发编程-AQS同步组件之重入锁ReentrantLock 读写锁ReentrantReadWriteLockCondition