Java并发系列源码分析(三)--ReentrantReadWriteLock
简介
ReentrantReadWriteLock读写锁,顾名思义既能加读锁也能加写锁,在ReentrantLock中对一个资源加了锁就会导致其它线程不能对这个资源进行操作,如果在高并发写的操作下对资源加锁就能保证资源的正确性,但是在高并发读的操作下线程并不会更改资源的情况下,这样就会导致一个线程读取资源的时候其它读取资源的线程就需要等待这个线程执行完毕并释放锁,这样就会导致性能下降,因为多个线程读取资源的操作并不会带来线程不安全的问题,此时就可以使用ReentrantReadWriteLock读写锁,多个线程读操作能一起执行,读操作与写操作互斥,写操作与写操作互斥,读操作与读操作共享。
构造方法
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/**
* 初始化读写锁
* 默认使用的是非公平锁
*/
public ReentrantReadWriteLock() {
this(false);
}
/**
* 初始化读写锁
* 根据指定的参数来决定使用的是公平锁还是非公平锁
* @param fair
*/
public ReentrantReadWriteLock(boolean fair) {
//根据参数来决定锁是公平锁还是非公平锁
sync = fair ? new FairSync() : new NonfairSync();
//读锁
readerLock = new ReadLock(this);
//写锁
writerLock = new WriteLock(this);
}
//读锁
public static class ReadLock implements Lock, java.io.Serializable {
//锁对象 根据外部创建的锁对象来确定当前读锁是公平的还是非公平的
private final Sync sync;
/**
* 根据外部锁对象来构建读锁
* @param lock 外部锁对象 公平锁或非公平锁
*/
protected ReadLock(ReentrantReadWriteLock lock) {
//获取外部锁对象
sync = lock.sync;
}
}
//写锁
public static class WriteLock implements Lock, java.io.Serializable {
//锁对象 根据外部创建的锁对象来确定当前写锁是公平的还是非公平的
private final Sync sync;
/**
* 根据外部锁对象来构建写锁
* @param lock 外部锁对象 公平锁或非公平锁
*/
protected WriteLock(ReentrantReadWriteLock lock) {
//获取外部锁对象
sync = lock.sync;
}
}
在ReentrantReadWriteLock的构造方法中可以根据传递的参数来决定创建的是公平的读写锁还是非公平的读写锁,如果不传递参数则创建的是非公平的读写锁,ReadLock和WriteLock的构造方法中获取ReentrantReadWriteLock中的Sync对象,该对象则是上面创建的非公平锁或公平锁,ReadLock和WriteLock最终根据具体的锁来执行具体的方法。
Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
//使用锁状态的高低16位来区分读写锁 高16位为读锁 低16位为写锁
static final int SHARED_SHIFT = 16;
//65536 每加一次读锁将在state的基础上加上这个值
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//65535 加锁的最大次数
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//16进制的最大值 65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** 获取读锁的加锁次数 */
static int sharedCount(int c) {
//将加锁次数的高16位无符号右移16位获取到读锁的加锁次数
return c >>> SHARED_SHIFT;
}
/** 获取写锁的加锁次数 */
static int exclusiveCount(int c) {
//使用当前加锁的次数与65535的二进制进行与运算获取到写锁的加锁次数
/**
* 65535的二进制 1111 1111 1111 1111
* c = 1 0000 0000 0000 0001
* 重入次数 0000 0000 0000 0001 = 1
*/
return c & EXCLUSIVE_MASK;
}
static final class HoldCounter {
//加锁次数
int count = 0;
//线程id
final long tid = getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//当前线程持有的可重入读锁的数量对象
private transient ThreadLocalHoldCounter readHolds;
//线程加读锁时记录加锁次数和线程id的对象(最后一个线程加锁的次数和线程id)
private transient HoldCounter cachedHoldCounter;
//第一个加读锁的线程
private transient Thread firstReader = null;
//第一个加读锁的线程重入加读锁的次数
private transient int firstReaderHoldCount;
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState());
}
}
在看加锁的方法之前我们需要先了解一下Sync抽象类中的一些常量和方法,SHARED_SHIFT则是将锁状态分为32位,以16位来区分读写锁,高16位为读锁,低16位为写锁,重入写锁的时候只需要在state的基础上加1即可,那如果重入读锁的话该如何操作?此时就可以使用SHARED_UNIT常量,该值为65536,在16位中最大的值就是65535,在32位中想要在高16位上加1则需要65535+1即可,MAX_COUNT则是加锁的最大次数,在16位中最大的数是65535,所以加读锁或写锁的最大次数则是65535,EXCLUSIVE_MASK则是写锁的标识,sharedCount方法中将加锁的次数无符号右移16位获取到读锁的加锁次数,exclusiveCount方法则是将加锁次数与写锁标识进行与运算获取到写锁的加锁次数,HoldCount则是记录了线程以及线程加锁的次数,ThreadLocalHoldCount中的initialValue方法则是在线程获取计数器对象为空的时候后调用该方法来初始化计数器对象,firstReader和firstReaderHoldCount常量则是记录第一个加读锁的线程和加读锁的次数,Sync构造方法则是初始化ThreadLocalHoldCount和锁状态。
获取读锁
/**
* 加读锁
* 读锁又称共享锁
* 可以多个线程同时加读锁
* 加了读锁时,其它线程就不能加写锁
*/
public void lock() {
//获取共享锁
sync.acquireShared(1);
}
/**
* 获取共享锁
* @param arg
*/
public final void acquireShared(int arg) {
//1 加锁成功 -1 加锁失败
if (tryAcquireShared(arg) < 0)
//为当前线程创建共享模式下的节点并入队
//等待前面的线程获取了锁并释放锁之后当前线程再去尝试获取锁
doAcquireShared(arg);
}
先执行tryAcquireShared方法尝试加读锁,如果加读锁失败则执行doAcquireShared方法将当前线程封装成一个节点并将节点入队进行等待。
tryAcquireShared
protected final int tryAcquireShared(int unused) {
//获取当前线程
Thread current = Thread.currentThread();
//获取加锁状态
int c = getState();
//校验写锁的加锁次数,如果写锁的加锁次数不等于0则说明已经有线程加了写锁
//则校验加写锁的线程是否是当前线程,如果不是当前线程则返回-1
//返回-1后则会为当前线程创建节点并将节点添加到等待队列中
//等待前面的线程节点获取了锁并释放了锁之后再去获取锁
//情况1:写锁次数为0则可以尝试获取读锁
//情况2:写锁次数不为0并且加写锁的线程不是当前线程,此时当前线程加读锁则需要进入等待队列中等待
//情况3:写锁次数不为0并且加写锁的线程是当前线程,则需要走锁降级将写锁降级成读锁
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
//获取读锁的加锁次数
int r = sharedCount(c);
//readerShouldBlock 队列中的头节点的下一个节点是否是在独占模式下等待 true 独占模式 false 共享模式
//在非公平锁的模式下队列中的头节点的下一个节点不是在独占模式下等待
//并且读锁的加锁次数小于最大加锁次数并且当前线程通过CAS操作修改了state的值成功获取到了读锁
//在公平锁的模式下如果等待队列中有线程节点在等待则需要排队等待获取锁
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
//r等于0则说明当前线程是第一个加读锁的线程
//将当前线程设置为第一个加读锁的线程
firstReader = current;
//重入次数
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//第一个加读锁的线程是当前线程则将重入次数自增
firstReaderHoldCount++;
} else {
//线程加锁的计数器对象
HoldCounter rh = cachedHoldCounter;
//校验计数器对象是否为空或者上一次加锁的线程是否是当前线程
if (rh == null || rh.tid != getThreadId(current))
//如果计数器等于空或计数器对象中的线程不是当前线程
//则说明只有一个线程加了读锁之后后续没有线程加读锁
//或上一次加读锁的线程不是当前线程
//从当前线程中获取计数器对象,如果当前线程中没有计数器对象则会创建
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
//如果计数器对象不为空并且上一次加锁的线程是当前线程并且加锁次数为0
//有可能是加锁的线程进入了等待队列,线程还未获取到锁的时候被取消了加锁
readHolds.set(rh);
//线程加锁次数自增
rh.count++;
}
//加锁成功
return 1;
}
//加锁失败
return fullTryAcquireShared(current);
}
调用exclusiveCount方法来获取写锁的加锁次数,如果写锁的加锁次数不等于0则说明有线程已经加了写锁,此时就需要比较一下加线程的线程是否是当前线程,如果是当前线程则会走锁降级,如果不是当前线程加的写锁当前线程则需要入队进行等待,如果写锁的次数等于0则说明没有线程加写锁,此时就可以加读锁,调用sharedCount方法先获取读锁的加锁次数,然后再调用readerShouldBlock方法来确定是否需要入队进行等待,在公平锁的模式下如果等待队列中有线程节点在等待获取锁,并且当前线程不是第一个加读锁的线程,那么当前线程就需要进入等待队列中等待,如果是非公平锁的模式下会校验头节点的后续的一个线程节点是否是要加写锁的,如果是要加写锁的,并且当前线程不是第一个加读锁的线程,那么当前线程就会进入等待队列中等待。为什么在非公平锁的模式下如果头节点的后续一个节点是要加的写锁,那么其它新来的获取读锁的线程就要入队等待呢?因为在非公平锁的模式下,新来的获取锁的线程不管队列中是否有线程节点在等待,都会尝试获取锁,在读多写少的情况下,线程获取到了读锁,导致写锁入队进行等待,读读不互斥,这样其它线程就会一直获取读锁,导致获取写锁的线程一直在等待,这样就会造成锁饥饿的情况,为了避免锁饥饿的情况,如果等待队列中的头节点的下一个节点的线程是获取写锁的那么其它获取读锁的线程就要入队等待,优先让获取写锁的线程。如果readerShouldBlock返回false在公平锁模式下,等待队列中没有线程节点在等待,在非公平锁模式下,等待队列中的头节点的后续的一个线程节点不是获取写锁的,那就可以通过CAS操作修改锁的状态来获取锁。通过sharedCount方法获取到的读锁的加锁次数如果等于0则说明当前线程是第一个加读锁的线程,则将当前线程和加锁次数记录下来,如果当前线程并不是第一个加读锁的线程,那就获取上一个加读锁的线程的计数器对象,如果计数器对象为空则说明只有一个线程加了读锁,后续并没有其它线程加读锁,计数器对象为空或上一次加读锁的线程不是当前线程则从当前线程中获取计数器对象,如果计数器对象为空,ThreadLocal中会调用initialValue方法来初始化计数器对象,然后对计数器对象中的加锁次数count进行++操作,如果计数器对象不为空并且上一次加锁的线程是当前线程并且加锁次数为0,有可能是加锁的线程进入了等待队列中,线程还未获取到锁的时候被取消了加锁。
fullTryAcquireShared
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
//获取加锁状态
int c = getState();
//校验写锁的加锁次数是否不为0
if (exclusiveCount(c) != 0) {
//如果写锁的加锁次数不为0则校验加写锁的线程是否是当前线程
//已有线程加了写锁并且不是当前线程则返回-1进入等待队列中等待
if (getExclusiveOwnerThread() != current)
return -1;
//在非公平锁的模式下如果等待队列中的头节点的下一个节点是独占模式则当前线程进入等待队列
//在公平锁的模式下如果等待队列中有线程节点在等待则当前线程进入等待队列
//为什么在非公平锁的模式下等待队列中的头节点的下一个节点是独占模式则当前线程要进入等待队列呢?
//在读多写少的情况下大量的线程获取读锁,可能会导致写锁没有获取锁的权限,导致写锁饥饿
//在公平锁的模式下只要等待队列中有节点在等待获取锁,那就要依次排队获取锁
} else if (readerShouldBlock()) {
//第一次加读锁的线程是当前线程则可以重入加锁
if (firstReader == current) {
} else {
//当前线程不是第一次加读锁的线程
//校验线程加锁的计数器对象是否为空
//如果是一次循环的话计数器对象肯定是为空的
if (rh == null) {
//将最后一次加锁的计数器对象赋值给rh
rh = cachedHoldCounter;
//校验最后一次加锁的计数器对象是否为空或者最后一次加锁的对象不是当前线程
if (rh == null || rh.tid != getThreadId(current)) {
//如果计数器等于空或计数器对象中的线程不是当前线程
//则说明只有一个线程加了读锁之后后续没有线程加读锁
//或上一次加读锁的线程不是当前线程
//从当前线程中获取计数器对象,如果当前线程中没有计数器对象则会创建
rh = readHolds.get();
if (rh.count == 0)
//如果计数器对象中的加锁次数等于0则说明该线程之前没有加过读锁
//则将当前线程中的计数器对象删除
readHolds.remove();
}
}
//如果计数器对象中的加锁次数不等于0则说明当前线程之前加过锁则需要重入锁
if (rh.count == 0)
//如果计数器对象中的加锁次数等于0则让当前线程入队进行等待
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
//读锁的次数超出最大加锁次数
throw new Error("Maximum lock count exceeded");
//尝试加读锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
//如果当前线程是第一个加读锁的线程则将当前线程记录下来并记录加锁的次数
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//当前线程是第一个加读锁的线程则将重入次数自增
firstReaderHoldCount++;
} else {
if (rh == null)
//获取上一次加读锁的计数器对象
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//如果计数器等于空或计数器对象中的线程不是当前线程
//则说明只有一个线程加了读锁之后后续没有线程加读锁
//或上一次加读锁的线程不是当前线程
//从当前线程中获取计数器对象,如果当前线程中没有计数器对象则会创建
rh = readHolds.get();
else if (rh.count == 0)
//如果计数器对象不为空并且上一次加锁的线程是当前线程并且加锁次数为0
//有可能是加锁的线程进入了等待队列,线程还未获取到锁的时候被取消了加锁
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh;
}
return 1;
}
}
}
在tryAcquireShared方法中加锁失败或在公平锁的模式下等待队列中有节点在等待获取锁、在非公平锁的模式下队列中的头节点的后续一个节点的线程是获取写锁的就会进入到当前方法中,fullTryAcquireShared方法与tryAcquireShared方法大致相同。
doAcquireShared
private void doAcquireShared(int arg) {
//为当前线程创建共享模式的节点
final Node node = addWaiter(Node.SHARED);
//是否执行失败
boolean failed = true;
try {
//是否被中断
boolean interrupted = false;
for (;;) {
//获取当前节点的上一个节点
final Node p = node.predecessor();
if (p == head) {
//如果上一个节点是头节点则尝试获取读锁
int r = tryAcquireShared(arg);
if (r >= 0) {
//r大于0则说明获取锁成功
//更新头节点并唤醒后继节点为共享模式的节点
setHeadAndPropagate(node, r);
p.next = null;
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//修改节点中的等待状态并将线程挂起
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//出现异常的时候取消加锁
cancelAcquire(node);
}
}
先为当前线程创建一个共享模式的节点并入队,再获取当前线程节点的上一个节点判断是否是头节点,如果是头节点则尝试获取读锁,如果获取读锁成功则将当前线程节点设置为头节点并唤醒后续等待状态为SIGNAL的节点,如果获取锁失败则将前一个节点的等待状态设置为SIGNAL,前一个节点的等待状态为SIGNAL则说明后续有节点需要唤醒,将节点的等待状态设置为了SIGNAL后则将当前线程挂起,等待前一个线程来唤醒。
释放读锁
/**
* 释放读锁
*/
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
//校验当前线程加的读锁是否都释放了
if (tryReleaseShared(arg)) {
//如果当前线程加的读锁都释放了则唤醒后续节点中的线程
doReleaseShared();
return true;
}
return false;
}
先调用tryReleaseShared方法释放一次当前线程加的锁,如果当前线程多次加锁,并不会直接将当前线程加的所有的读锁都释放掉,如果当前线程加的锁都释放了则会调用doReleaseShared方法唤醒后续加锁的线程。
tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
//获取当前线程
Thread current = Thread.currentThread();
//校验当前线程是否是第一个加读锁的线程
if (firstReader == current) {
//当前线程是第一个加读锁的线程则校验当前线程加读锁的次数是否等于1
if (firstReaderHoldCount == 1)
//加读锁的次数等于1则将第一次加读锁的线程置为空
firstReader = null;
else
//读锁的次数自减
firstReaderHoldCount--;
} else {
//获取最后一个线程加锁的计数器对象
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//最后一个加锁的线程不是当前线程或计数器对象为空则从当前线程中获取计数器对象
rh = readHolds.get();
//获取加锁次数
int count = rh.count;
if (count <= 1) {
//从当前线程中删除计数器对象
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
//读锁的次数自减
--rh.count;
}
for (;;) {
//获取锁状态
int c = getState();
//减去一次读锁
int nextc = c - SHARED_UNIT;
//更新锁状态
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
整个if else则是对线程的计数器进行操作,然后通过for循环修改锁的状态,直到修改成功。
doReleaseShared
private void doReleaseShared() {
for (;;) {
//头节点
Node h = head;
//校验头节点是否不为空并且头节点不等于尾节点
if (h != null && h != tail) {
//获取头节点的等待状态
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//如果头节点的等待状态为SIGNAL则将等待状态修改为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//状态修改成功则唤醒下一个节点中的线程
unparkSuccessor(h);
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
线程释放了锁之后,如果后续还有节点在等待唤醒则会调用unparkSuccessor唤醒后续节点。
获取写锁
public void lock() {
sync.acquire(1);
}
/**
* 尝试获取锁
* 如果获取失败则将当前线程挂起并放入等待队列中
* 等待锁的持有者释放锁并唤醒队列中的头节点获取锁
* @param arg
*/
public final void acquire(int arg) {
/**
* 在ReentrantReadWriteLock中的返回的几种情况:
* false
* 1.有线程已经获取到了读锁,此时就不能加写锁,则需要入队等待
* 2.有线程已经获取到了写锁,此时就不能加写锁,则需要入队等待
* 3.锁状态为空闲状态,此时是公平锁,队列中有线程节点在等待,当前线程则需要入队等待
*
* true
* 1.有线程加了写锁并且加锁的线程是当前线程则重入加锁
* 2.锁状态为空闲状态,此时是公平锁并且队列中没有线程节点在等待则会尝试获取锁
* 3.锁状态为空闲状态,此时是非公平锁,当前线程会尝试加锁
*
* 在tryAcquire方法中尝试加锁失败时
* 则会调用addWaiter方法为当前线程创建一个独占模式的节点并将该节点添加到等待队列中
* 如果等待队列中为空则先创建一个头节点并将当前线程的节点设置为尾节点
*
*/
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
获取写锁的操作与ReentrantLock中获取锁的操作大致相同,不同的地方就是各自实现了tryAcquire方法,如果获取写锁失败则会为当前线程创建一个独占模式的节点入队并将线程挂起,等待唤醒。
tryAcquire
protected final boolean tryAcquire(int acquires) {
//获取当前线程
Thread current = Thread.currentThread();
//获取锁状态
int c = getState();
//获取写锁的加锁次数
int w = exclusiveCount(c);
//校验锁状态是否不为0
//如果锁状态不为0说明有线程已经加了锁
if (c != 0) {
//如果w等于0则有线程加的是读锁,当前线程则需要入队等待
//如果w不等于0则有线程加的是写锁此时就会校验加写锁的线程是否是当前线程
//如果是当前线程则重入,如果不是当前线程则需要入队等待
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//重入
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
//锁状态等于0说明还没有线程加锁
//writerShouldBlock 在公平锁的模式下会先校验等待队列中是否有线程节点在等待
//如果有线程节点在等待当前线程则需要入队等待
//在非公平锁的模式下不管等待队列中是否有线程在等待,都会尝试加锁
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
//将当前线程设置为持有锁的线程
setExclusiveOwnerThread(current);
return true;
}
先获取锁的状态,再获取写锁的加锁次数,如果锁的状态不为0则说明有线程已经加了锁,此时校验是否是加的写锁,如果不是加的写锁,当前线程则需要进入等待队列中等待,如果是加的写锁则需要校验是否是当前线程加的写锁,如果不是当前线程加的写锁也需要入队进行等待,如果是当前线程加的写锁那就更新锁状态。如果锁状态为0则说明还没有线程加锁,则会调用writerShouldBlock方法,如果在公平锁模式下该方法会校验等待队列中是否有线程节点在等待,如果有线程节点在等待,当前线程则需要入队等待,在非公平锁的模式下不管等待队列中是否有线程节点在等待,当前线程都会尝试加锁。
释放写锁
public void unlock() {
sync.release(1);
}
/**
* 尝试释放锁
* 如果释放锁成功并且锁的状态是空闲的
* 则会唤醒等待队列中当前节点的后续节点
* 如果后续节点的等待状态为1则会继续获取后续节点
* 直到后续节点为-1才进行唤醒
*/
public final boolean release(int arg) {
/**
* tryRelease 尝试释放锁,由调用方实现具体的逻辑
* 只有锁的持有者线程是当前线程才能释放锁
*/
if (tryRelease(arg)) {
//获取头节点
Node h = head;
//校验头节点是否为空并且头节点的等待状态是否不等于0
if (h != null && h.waitStatus != 0)
//如果头节点不为空并且头节点的等待状态不等于0则说明后续节点需要加锁
//调用unparkSuccessor方法唤醒下一个节点
//如果下一个节点中的等待状态为-1才进行唤醒
//如果等待状态为1则不会进行唤醒
unparkSuccessor(h);
return true;
}
return false;
}
/**
* 释放写锁
* @param releases
* @return
*/
protected final boolean tryRelease(int releases) {
//校验加锁的线程是否是当前线程
if (!isHeldExclusively())
//不是当前线程则抛出异常
throw new IllegalMonitorStateException();
//加锁次数减去释放锁次数获取到剩余加锁次数
int nextc = getState() - releases;
//校验剩余加锁次数是否为0
boolean free = exclusiveCount(nextc) == 0;
if (free)
//如果为0则将持有锁的线程设置为空
setExclusiveOwnerThread(null);
//更新锁状态
setState(nextc);
return free;
}
释放写锁的线程必须是加锁的线程才能释放,当将锁释放完毕之后则会唤醒后续节点中的线程。
转载自:https://juejin.cn/post/7172905552261939208