再谈Java多线程编程中的虚假唤醒

虚假唤醒和你写的程序逻辑无关!

这是偏于底层的锅,是否看到有些博客写的虚假唤醒难以理解?各种逻辑看似合理,但却给出了虚假唤醒的错误解释?那么来看这篇文章吧,可以满足你的需求。
首先,先看一波《Java并发编程的艺术》中关于等待/通知的经典范式的描述:

等待方遵循如下原则:

  1. 获取对象的锁。
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
  3. 条件满足则执行对应的逻辑。

对应的伪代码如下

synchronized(对象) { 
	while(条件不满足) { //(注意此处呼应第二条,使用while)
		对象.wait(); 
	}对应的处理逻辑 
}

通知方遵循如下原则:

  1. 获得对象的锁。
  2. 改变条件。
  3. 通知所有等待在对象上的线程。

对应的伪代码如下

synchronized(对象) { 
	改变条件 
	对象.notifyAll(); 
}

上面为什么唤醒后要再进行判断?书上并没有解释,但是经过仔细分析,我认为这是防止虚假唤醒,很多文章关于虚假唤醒的解释说有其他线程影响,导致条件再次改变,解释还说的通,但是这并不是虚假唤醒。

虚假唤醒是什么?来看看维基百科的解释:
Spurious wakeup
虚假唤醒描述了使用条件变量的复杂性,这些条件变量是由某些多线程API(如POSIX线程和Windows API)提供的。
即使从等待线程的角度来看,条件变量似乎已经发出了信号,但是等待的条件仍然可能是假的。其中一个原因是虚假的唤醒;也就是说,即使没有线程发出条件变量的信号,线程也可能从其等待状态中被唤醒。为了确保正确性,有必要在线程完成等待后验证条件是否确实为真。因为虚假的唤醒可以重复发生,这是通过在循环中等待来实现的,当条件为真时,该循环终止,例如:

true, for example:

/* In any waiting thread: */
while(!buf->full)
	wait(&buf->cond, &buf->lock);

/* In any other thread: */
if(buf->n >= buf->size){
	buf->full = 1;
	signal(&buf->cond);
}

在本例中,我们知道另一个线程将在发送buf->cond(同步两个线程的方法)之前将buf->full(等待的实际条件)设置。等待线程在从等待返回时将始终验证实际情况的真实性,以确保在出现虚假唤醒时正确的行为。
根据David R. Butenhof的POSIX线程编程ISBN 0-201-63392-2:
这意味着当您等待一个条件变量时,当没有线程特别广播或发出条件变量的信号时,等待可能(偶尔)返回。虚假的唤醒可能听起来很奇怪,但是在一些多处理器系统中,使条件唤醒完全可预测可能会大大降低所有条件变量操作的速度。造成虚假觉醒的竞争条件应该被认为是罕见的。”

知乎上有这样一个回答:
为什么条件锁会产生虚假唤醒现象(spurious wakeup)?
回答原文如下:

pthread 的条件变量等待 pthread_cond_wait 是使用阻塞的系统调用实现的(比如 Linux 上的
futex),这些阻塞的系统调用在进程被信号中断后,通常会中止阻塞、直接返回 EINTR 错误。同样是阻塞系统调用,你从 read 拿到
EINTR 错误后可以直接决定重试,因为这通常不影响它本身的语义。而条件变量等待则不能,因为本线程拿到 EINTR 错误和重新调用
futex 等待之间,可能别的线程已经通过 pthread_cond_signal 或者
pthread_cond_broadcast发过通知了。
所以,虚假唤醒的一个可能性是条件变量的等待被信号中断。不过,把等待放到循环里的另一个原因是还可能有这样的情况(有人觉得它是虚假唤醒的一种,有人觉得不是):明明有对应的唤醒,但条件不成立。这是因为可能由于线程调度的原因,被条件变量唤醒的线程在本线程内真正执行「加锁并返回」前,另一个线程插了进来,完整地进行了一套「拿锁、改条件、还锁」的操作。比如
Windows 下 SleepConditionVariableCS 的文档中就明确指出了可能出现这种情况。(pthread
里的情况则有点区别,人家的 pthread_cond_signal 本来就可能唤醒多个正在等待的线程。)

结论:

虚假唤醒与操作系统底层函数有关,而和逻辑无关,但是很多人分析的逻辑是这样:
明明有对应的唤醒,但条件不成立。这是因为可能由于线程调度的原因,被条件变量唤醒的线程在本线程内真正执行「加锁并返回」前,另一个线程插了进来,完整地进行了一套「拿锁、改条件、还锁」的操作。
个人认为,这不算虚假唤醒的解释,只能算是逻辑问题。

综上,关于虚假唤醒的浅解,希望能帮助到大家!

全部评论

相关推荐

2025-12-13 21:01
已编辑
百度_meg_前端开发(实习员工)
走到这一步,确实有些意外。先简单说说我的情况,我是双非本,大一那年对后端兴趣特别浓,学了快一年半。但不知为什么越往后学兴趣越淡——大概到分布式那块,比如nacos、卡夫卡这些,感觉越来越吃力。再加上看到师兄师姐在后端方向上的碰壁(现在是大go时代),在和师兄师姐商量后我在今年一月左右转前端了或许是因为有java的基础,对项目开发流程有些概念,前端三件套我过得比较快。之后学了Vue,动手做了自己的博客,这大概也是我转前端的一个重要原因吧,一直很想拥有一个属于自己的个人博客,能按自己的想法去设计、实现,并长期迭代完善,这种成就感真的很棒。之前拿过别人的开源项目来更改 但是自己修改的就是一坨,那个时候缺少对前端代码的理解 就算借助ai做出来的效果也是一坨就这样到了大二暑假,我觉得该找份实习,丰富一下简历了。我自认不是很有创造力的人,平时少有自发的项目灵感,所以更希望通过实习开阔眼界、提升能力。一开始投递和面试的过程挺煎熬的,或许是因为目标多是中小厂,很多hr已读不回,或是直接砍半薪资问我接不接受。面试时也常觉得像在走流程,问的都是八股文,有的面试官还会边看题边问,甚至有一次十分钟就结束了,好在最后钛动给了我机会。实习期间我学到了很多,虽然也常被拷打,还好ld会帮我收拾烂摊子。从钛动离职回校后,我半推半就地背八股、学新技术,无聊时就刷里扣、看看牛客和biss。原本以为双非bg很会被hr速度筛掉所以就尝试性的投了纷享销客和百度的日常实习,没想到最后两家都oc了,雷姆了家人们,双非鼠鼠居然圆了大厂梦yysy,这一路其实冒了不小的风险。毕竟学了那么久的后端,大学四年时间有限,突然转前端,意味着很多积累的知识可能用不上了。但我很庆幸当时有放下的勇气。无论过去做了什么选择,我都想感谢当时的自己,因为那份勇气,才走到了今天。同时也很感谢这一路师兄师姐的帮忙,师兄帮忙模拟面试,提供资料,师姐教我如何选择岗位,如何处理实习带来的问题马上就要北漂了,对未来是充满了期待也存在着恐惧,南方人头一次去这么远的地方,每天都能看到雪,可以跟实力强劲的同事合作,想想都很兴奋,但是也害怕自己不能胜任这份工作会被压力到爆,但是不管怎么样大家一起互勉吧,呆在舒适区只会停滞不前,压力才能带来成长
牛马人的牛马人生:勇敢追梦
2025年终总结
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
01-07 00:20
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务