虚假唤醒详解
接触过多线程编程的朋友们或多或少都听说过虚假唤醒这一术语,我在百度,B站上看了很多讲解,都没说的太清楚,在这里写一下,说一下我对它的理解。
虚假唤醒是什么
首先我们来谈谈虚假唤醒到底是什么。在这里我给出一个自己的定义,用来理解虚假唤醒。
虚假唤醒是一种现象,它只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,但由于多个线程执行的顺序不同,后面竞争到锁、获得运行权的线程在运行时条件已经不再满足,线程应该睡眠但是却继续往下运行的一种现象。
上面是比较书面化的定义,我们用人能听懂的话来介绍一下虚假唤醒。
多线程环境的编程中,我们经常遇到让多个线程等待在一个条件上,等到这个条件成立的时候我们再去唤醒这些线程,让它们接着往下执行代码的场景。假如某一时刻条件成立,所有的线程都被唤醒了,然后去竞争锁,因为同一时刻只会有一个线程能拿到锁,其他的线程都会阻塞到锁上无法往下执行,等到成功争抢到锁的线程消费完条件,释放了锁,后面的线程继续运行,拿到锁时这个条件很可能已经不满足了,这个时候线程应该继续在这个条件上阻塞下去,而不应该继续执行,如果继续执行了,就说发生了虚假唤醒。
代码示例
Linus说show me the code,我们也写点代码说明一下,首先写一个会发生虚假唤醒的情况:
class SpuriousWakeup {
private static ReentrantLock lock = new ReentrantLock();
private static Condition hasApple = lock.newCondition();
private static volatile int nApple;
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
if (nApple == 0) {
log.debug("没苹果,我先休息会儿,苹果来了我再醒...");
hasApple.await();
}
nApple -= 1;
log.debug("哇,苹果来了,我吃掉了...");
log.debug("现在苹果还有 " + nApple + " 个...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "萧炎").start();
new Thread(() -> {
lock.lock();
try {
if (nApple == 0) {
log.debug("没苹果,我先休息会儿,苹果来了我再醒...");
hasApple.await();
}
nApple -= 1;
log.debug("哇,苹果来了,我吃掉了...");
log.debug("现在苹果还有 " + nApple + " 个...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "唐三").start();
SleepUtils.sleep(1);
new Thread(() -> {
lock.lock();
try {
log.debug("我来送苹果了,但只有一个哦...");
nApple = 1;
hasApple.signalAll();
} finally {
lock.unlock();
}
}, "萧薰儿").start();
}
}
运行结果:
21:09:27.968 [萧炎] - 没苹果,我先休息会儿,苹果来了我再醒...
21:09:27.997 [唐三] - 没苹果,我先休息会儿,苹果来了我再醒...
21:09:28.927 [萧薰儿] - 我来送苹果了,但只有一个哦...
21:09:28.927 [萧炎] - 哇,苹果来了,我吃掉了...
21:09:28.928 [萧炎] - 现在苹果还有 0 个...
21:09:28.928 [唐三] - 哇,苹果来了,我吃掉了...
21:09:28.928 [唐三] - 现在苹果还有 -1 个...
在这个例子里,我们写了三个线程,萧炎和唐三想吃一个苹果,刚运行发现没有苹果,于是睡在了Condition上;萧薰儿拿来了一个苹果,叫醒了两个人,两个人都醒了,但是只有一个苹果,根据结果可以看出来,萧炎先抢到了这个苹果,他吃掉了,但是唐三也被叫醒了,他吃了一个寂寞。
可以看出来,等到唐三醒了的时候苹果是没有了的,这时候他应该继续睡在Condition上。但是他没有,他醒了,他还继续运行了,所以他吃了个寂寞。发生了虚假唤醒。
下面是一个不会发生虚假唤醒的情况:
class NonSpuriousWakeup {
private static ReentrantLock lock = new ReentrantLock();
private static Condition hasApple = lock.newCondition();
private static volatile int nApple;
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
while (nApple == 0) {
log.debug("没苹果,我先休息会儿,苹果来了我再醒...");
hasApple.await();
}
nApple -= 1;
log.debug("哇,苹果来了,我吃掉了...");
log.debug("现在苹果还有 " + nApple + " 个...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "萧炎").start();
new Thread(() -> {
lock.lock();
try {
while (nApple == 0) {
log.debug("没苹果,我先休息会儿,苹果来了我再醒...");
hasApple.await();
}
nApple -= 1;
log.debug("哇,苹果来了,我吃掉了...");
log.debug("现在苹果还有 " + nApple + " 个...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "唐三").start();
SleepUtils.sleep(1);
new Thread(() -> {
lock.lock();
try {
log.debug("我来送苹果了,但只有一个哦...");
nApple = 1;
hasApple.signalAll();
} finally {
lock.unlock();
}
}, "萧薰儿").start();
}
}
运行结果:
21:26:25.980 [萧炎] - 没苹果,我先休息会儿,苹果来了我再醒...
21:26:25.995 [唐三] - 没苹果,我先休息会儿,苹果来了我再醒...
21:26:26.953 [萧薰儿] - 我来送苹果了,但只有一个哦...
21:26:26.954 [萧炎] - 哇,苹果来了,我吃掉了...
21:26:26.954 [萧炎] - 现在苹果还有 0 个...
21:26:26.955 [唐三] - 没苹果,我先休息会儿,苹果来了我再醒...
这个例子与上面的代码几乎没有差别,只是把if判断换成了while判断,所以每次萧炎和唐三醒过来之后都会再判断一下有没有苹果(唤醒自己的条件是否满足),如果不满足,就会继续睡下去,不会接着往下运行,从而避免了虚假唤醒。
总结:
等待在一个条件上的线程被全部唤醒后会去竞争锁,所以这些线程会一个一个地去消费这个条件,等到后面的线程去消费这个条件时,条件可能已经不满足了,所以每个被唤醒的线程都需要再检查一次条件是否满足。如果不满足,应该继续睡下去;只有满足了才能往下执行。
其他的拓展
其实虚假唤醒是因为多个线程被同时唤醒然后消费条件,导致后面拿到锁去运行的线程在条件不再满足的情况下继续往下执行了。理论上来说,如果场景允许,比如萧薰儿只拿来一个苹果的话,是可以调signal的,这样每次只会唤醒一个线程,拿走这一个苹果,其他的线程不会被唤醒,也就不会发生虚假唤醒。
Condition类的精确等待与精确唤醒只是相对于Java提供的wait/notify、wait/notifyAll的机制的,在原先的机制下,所有的的线程只能等在一个地方,无论他们等待的条件是什么,只要被notifyAll了,就都会被唤醒,醒来都会检查自己等待的条件是否被满足。而每一个Condition变量就是一个条件,线程可以准确地睡在自己需要的条件上,这样每次被唤醒的线程都是等待的条件被满足的线程,这样便可以减少唤醒的线程的数量,降低系统负担,提高效率。
顺便贴一下自己写的一个线程安全的小工具,用来打印log挺不错。
@ThreadSafe
public class LogHelper {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
private static final int POSSIBLE_MAX_THREAD_NAME_LENGTH = 12;
private static final int DEFAULT_FIXED_PREFIX_LENGTH = 18;
private static final int DEFAULT_PREFIX_LENGTH = DEFAULT_FIXED_PREFIX_LENGTH + POSSIBLE_MAX_THREAD_NAME_LENGTH;
/* 使用单例模式,在类加载过程中保证单例 */
public static LogHelper log = new LogHelper();
private LogHelper() {}
/**
* 使用线程封闭保证线程安全
* @param pattern C语言格式下的模式串,也可以用Java的+++
* @param args 参数列表,可以为空
*/
public void debug(String pattern, Object... args) {
String str = buildPrefix() + pattern + "\n";
System.out.printf(str, args);
}
private String buildPrefix() {
LocalTime now = LocalTime.now();
StringBuilder stringBuilder = new StringBuilder(DEFAULT_PREFIX_LENGTH);
stringBuilder.append(now.format(formatter));
stringBuilder.append(" [" + Thread.currentThread().getName() + "] - ");
return stringBuilder.toString();
}
}