LockSupport
LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。
LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker,long deadline)3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。
CLH队列锁
CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
当一个线程需要获取锁时:
- 创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用
- 线程A对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred
线程B需要获得锁,同样的流程再来一遍
- 线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false)
- 当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点
如上图所示,前驱结点释放锁,线程A的myPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁。
CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail)。CLH队列锁常用在SMP体系结构下。
Java中的AQS是CLH队列锁的一种变体实现。(接下来讲解的)
注:扩展知识点
SMP(Symmetric Multi-Processor)。即对称多处理器结构,指server中多个CPU对称工作,每一个CPU訪问内存地址所需时间同样。其主要特征是共享,包括对CPU,内存,I/O等进行共享。SMP的长处是可以保证内存一致性。缺点是这些共享的资源非常可能成为性能瓶颈。随着CPU数量的添加,每一个CPU都要訪问同样的内存资源,可能导致内存訪问冲突,可能会导致CPU资源的浪费。经常使用的PC机就属于这样的。
非一致存储访问,将CPU分为CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、I/O槽口等,模块之间可以通过互联模块相互访问,访问本地内存(本CPU模块的内存)的速度将远远高于访问远地内存(其他CPU模块的内存)的速度,这也是非一致存储访问的由来。NUMA较好地解决SMP的扩展问题,当CPU数量增加时,因为访问远地内存的延时远远超过本地内存,系统性能无法线性增加。
CLH唯一的缺点是在NUMA系统结构下性能很差,但是在SMP系统结构下该法还是非常有效的。解决NUMA系统结构的思路是MCS队列锁。
AbstractQueuedSynchronizer
问:AbstractQueuedSynchronizer 是什么?
队列同步器AbstractQueuedSynchronizer(以下简称同步器或AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。
问:AbstractQueuedSynchronizer 能干什么?
AQS是一个同步框架,通过维护一个int类型的变量state和一个先进先出队列,实现共享资源的访问控制和线程阻塞,其提供了一些基类实现排队和阻塞机制,
子类可以通过继承AQS并重写其提供的抽象类实现一个同步工具,同步工具的语义由子类决定。
在JUC包下,通过AQS实现的同步工具类包含以下几个
ReentantLock
ReentantReadWriteLock
Semaphore
CountDownLatch
Worker(ThredPoolExecutor类中)
在AQS里由一个int型的state来代表这个状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法
getState()
setState(int newState)
compareAndSetState(int expect,int update)
进行操作,因为它们能够保证状态的改变是安全的。
在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,
同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(上述的5个同步工具类)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。可以这样理解二者之间的关系:
锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
问:AbstractQueuedSynchronizer 内部使用哪些方法实现的?
实现自定义同步组件时,将会调用同步器提供的模板方法
这些模板方法同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。
可重写的方法
访问或修改同步状态的方法
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
- getState():获取当前同步状态。
- setState(int newState):设置当前同步状态。
- compareAndSetState(int expect,int update): 使用CAS设置当前状态,该方法能够保证状态设置的原子性。
实现自己的独占锁 不可重入
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author : monco
* @date : 2019/10/11 1:46
* className: SelfLock
* description: 实现我们自己独占锁,不可重入 原理参照 CLH队列锁
*/
public class SelfLock implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
/* 尝试去拿锁 */
@Override
protected boolean tryAcquire(int arg) {
// CAS 操作
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/* 尝试去释放锁 */
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
/**
* 将内部类实例化
*/
private final Sync sync = new Sync();
@Override
public void lock() {
System.out.println(Thread.currentThread().getName() + " ready get lock");
sync.acquire(1);
System.out.println(Thread.currentThread().getName() + " already got lock");
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
@Override
public void unlock() {
System.out.println(Thread.currentThread().getName() + " ready release lock");
sync.release(1);
System.out.println(Thread.currentThread().getName() + " already released lock");
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
测试:
import com.monco.util.SleepTools;
import java.util.concurrent.locks.Lock;
/**
* @author : monco
* @date : 2019/10/11 1:46
* className: TestMyLock
* description: 测试自己实现锁
*/
public class TestMyLock {
public void test() {
final Lock lock = new SelfLock();
class Worker extends Thread {
@Override
public void run() {
lock.lock();
try {
SleepTools.second(1);
} finally {
lock.unlock();
}
}
}
// 启动4个子线程
for (int i = 0; i < 4; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
// 主线程每隔1秒换行
for (int i = 0; i < 10; i++) {
SleepTools.second(1);
System.out.println();
}
}
public static void main(String[] args) {
TestMyLock testMyLock = new TestMyLock();
testMyLock.test();
}
}
结果:
问:为什么实现了上述方法,却不能实现锁的可重入?要怎样改进才可以做到锁的可重入?
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author monco
* @data 2020/7/4 18:04
* @description : 实现独占锁,可重入
*/
public class ReenterSelfLock implements Lock {
/**
* 静态内部类,自定义同步器
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 尝试拿锁
*
* @param arg
* @return
*/
@Override
protected boolean tryAcquire(int arg) {
// 循环CAS 将当前线程拿到锁 或者拿不到锁 但是 拿到锁的线程和当前线程为统一线程 则返回true 允许进入锁的内部
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
setState(getState() + 1);
return true;
}
return false;
}
/**
* 尝试解锁
*
* @param arg
* @return
*/
@Override
protected boolean tryRelease(int arg) {
// 判断拿到锁的线程和当前线程不是同一线程 则返回异常 或者state==0 证明当前线索没有拿到锁 也返回异常
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
// 将拿锁状态重置
setState(getState() - 1);
if (getState() == 0) {
// 释放当前拿锁线程
setExclusiveOwnerThread(null);
}
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() > 0;
}
Condition newCondition() {
return new ConditionObject();
}
}
private final Sync sync = new Sync();
@Override
public void lock() {
System.out.println(Thread.currentThread().getName()+" ready get lock");
sync.acquire(1);
System.out.println(Thread.currentThread().getName()+" already got lock");
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
System.out.println(Thread.currentThread().getName()+" ready release lock");
sync.release(1);
System.out.println(Thread.currentThread().getName()+" already released lock");
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
}
AQS中的数据结构-节点和同步队列
数据结构 : 节点Node
既然说Java中的AQS是CLH队列锁的一种变体实现,毫无疑问,作为队列来说,必然要有一个节点的数据结构来保存我们前面所说的各种域,比如前驱节点,节点的状态等,这个数据结构就是AQS中的内部类Node。作为这个数据结构应该关心些什么信息?
- 线程信息,肯定要知道我是哪个线程
- 队列中线程状态,既然知道是哪一个线程,肯定还要知道线程当前处在什么状态,是已经取消了“获锁”请求,还是在“”等待中”,或者说“即将得到锁”
- 前驱和后继线程,因为是一个等待队列,那么也就需要知道当前线程前面的是哪个线程,当前线程后面的是哪个线程(因为当前线程释放锁以后,理当立马通知后继线程去获取锁)。
其中包括了:
线程的2种等待模式:
SHARED:表示线程以共享的模式等待锁(如ReadLock)
EXCLUSIVE:表示线程以互斥的模式等待锁(如ReetrantLock),互斥就是一把锁只能由一个线程持有,不能同时存在多个线程使用同一个锁。
线程在队列中的状态枚举:
CANCELLED:值为1,表示线程的获锁请求已经“取消”
SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用上
初始化Node对象时,默认为0
成员变量:
waitStatus:该int变量表示线程在队列中的状态,其值就是上述提到的CANCELLED、SIGNAL、CONDITION、PROPAGATE
prev:该变量类型为Node对象,表示该节点的前一个Node节点(前驱)
next:该变量类型为Node对象,表示该节点的后一个Node节点(后继)
thread:该变量类型为Thread对象,表示该节点的代表的线程
nextWaiter:该变量类型为Node对象,表示等待condition条件的Node节点
当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。
head和tail
AQS还拥有首节点(head)和尾节点(tail)两个引用,一个指向队列头节点,而另一个指向队列尾节点。
++注意:因为首节点head是不保存线程信息的节点,仅仅是因为数据结构设计上的需要,在数据结构上,这种做法往往叫做“空头节点链表”。对应的就有“非空头结点链表”++
节点在同步队列中的增加和移出
节点加入到同步队列
当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,也就是获取同步状态失败,AQS会将这个线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列的尾部。而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),
它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
首节点的变化
首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。
独占式同步状态获取与释放 - 获取
通过调用同步器的acquire(int arg)方法可以获取同步状态,主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:
首先调用自定义同步器实现的tryAcquire(int arg)
方法,该方法需要保证线程安全的获取同步状态。
如果同步状态获取失败(tryAcquire返回false),则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,
最后调用acquireQueued(Node node,int arg)
方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
addWaiter(Node node)方法中
将当前线程包装成Node后,队列不为空的情况下,先尝试把当前节点加入队列并成为尾节点,如果不成功或者队列为空进入enq(final Node node)方法
在enq(final Node node)
方法中,同步器通过“死循环”来保证节点的正确添加,这个死循环中,做了两件事,第一件,如果队列为空,初始化队列,new出一个空节点,并让首节点(head)和尾节点(tail)两个引用都指向这个空节点;第二件事,把当前节点加入队列。
在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。
节点进入同步队列之后,观察acquireQueued(Node node,int arg)方法
其实就是一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)。
在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个。
第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
第二,维护同步队列的FIFO原则。
当前线程获取到同步状态后,让首节点(head)这个引用指向自己所在节点。当同步状态获取成功后,当前线程就从acquire方法返回了。如果同步器实现的是锁,那就代表当前线程获得了锁。
独占式同步状态获取与释放 - 释放
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。
该方法执行时,会唤醒首节点(head)所指向节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程。
而在unparkSuccessor中
这段代码的意思,一般情况下,被唤醒的是head指向节点的后继节点线程,如果这个后继节点处于被cancel状态,(我推测开发者的思路这样的:后继节点处于被cancel状态,意味着当锁竞争激烈时,队列的第一个节点等了很久(一直被还未加入队列的节点抢走锁),包括后续的节点cancel的几率都比较大,所以)先从尾开始遍历,找到最前面且没有被cancel的节点。
独占式同步状态获取与释放 - 总结
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)
方法释放同步状态,然后唤醒head指向节点的后继节点。
共享式同步状态获取与释放
为什么要有共享式?
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以读写为例,如果一个程序在进行读操作,那么这一时刻写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。
在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。
因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)
方法返回值大于等于0。可以看到,在doAcquireShared(int arg)
方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
实现自己的共享锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author monco
* @data 2020/7/6 15:51
* @description : 怎么去实现共享锁
*/
public class TrinityLock implements Lock {
private final Sync sync = new Sync(4);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count);
}
/**
* @param reduceCount 扣减个数
* @return 返回小于0,表示当前线程获得同步状态失败
* 大于0,表示当前线程获得同步状态成功
*/
@Override
public int tryAcquireShared(int reduceCount) {
for (; ; ) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
/**
* @param returnCount 归还个数
* @return
*/
@Override
public boolean tryReleaseShared(int returnCount) {
for (; ; ) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
final ConditionObject newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
sync.tryAcquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquireShared(1) >= 0;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.tryReleaseShared(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
了解Condition的实现
Condition的数据结构
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类。
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。
如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
如图所示,同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中。
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。
通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。
成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。