⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 cnblogs.com/flashsun/p/11017431.html 「闪客sun」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

最近在给别人讲解 Java 并发编程面试考点时,为了解释锁对象这个概念,想了一个形象的故事。

后来慢慢发现这个故事似乎能讲解 Java 并发编程中好多核心概念,于是完善起来形成了这篇文章。

大家先忘记并发编程,只听我给你讲个故事:

故事可能比较奇怪,有这么一个学校,里面有好多好多人,我们简单分成学生、老师、以及宿管阿姨。

学校中间还有一个很奇葩的水果超市,里面有个仓库放着苹果、西瓜、橘子。来这个超市的人,一方面可以拿走水果吃掉,另一方面也可以送来水果还钱。

不过超市还有一个很奇葩的规则,就是学生只能去吃或者送苹果,老师则只能是吃或者送西瓜,宿管阿姨只能是吃或者送橘子。

这个超市的进出也很有规矩,来这个超市的人,必须持有相应的证件,学生则需要持有学生证,老师需要持有教师证,宿管阿姨需要持有阿姨证。

这三个证每个都分别只有一个,保管在超市门口的一个领证处,人们进入这个超市之前,必须先去取证处那里领取相应的证件才能进入。

如果证件暂时被别人取走了拿不到,则需要到后方的等待区里面排队等证。那这个等待区也有三个,分别是学生证等待区,教师证等待区,阿姨证等待区。

进入超市里面就更加奇葩了,不论是要从这个超市拿走水果,还是要送来水果,都需要通过一个操作台来控制,而这个操作台,同一时刻只能有一个人进行操作。

这个操作台为了防止有人霸占操作台过长时间,只允许一个人持续操作 10s,10s 之后会在屏幕上显示一个 ID,只有这个 ID 的人才能来操作。

至于选择什么号码,老师学生或是宿管阿姨都无法决定和干预,只能任凭这个操作台来决策。

但好在,每个人在操作台上都有自己的账号,操作一半被中断的数据并不会丢失。

这个故事的背景就介绍完了,下面这个学校就发生了各种各样的事。

首先我们假设,进这所学校的人,都是为了去超市做事情。某一时刻,操作台上显示了一个号码 2 号,这个号码通过各种学校大屏幕通知给所有的人。

于是 ID 为 2 号的学生小明看到了自己的号码,得知自己获得了进入超市操作控制台的权利,于是出发前往超市。

小明首先到超市门口,问领证处的管理人员,“给我一张学生证!”。

管理人员找了找发现有一张学生证,于是便给了小明。小明拿到了学生证,顺利进入超市,并坐在了操作台前,登录了自己的账号系统。

小明此行的目的是为了拿走一个苹果,于是他点击了苹果商品的图标,系统显示苹果还有 4 个。

于是小明顺利地拿走了苹果,系统将苹果数量 -1,将新的苹果数量 3 记录到总系统库中。

接着小明走出超市,将学生证交还给了领证处,走出了校园,消失在外面的人海中。

接着操作台上显示了3号,同样通过学校大屏幕通知给了所有人。ID 为 3 号的学生小张看到了自己的号码,得知自己获得了进入超市操作控制台的权利,于是出发前往超市。

小张和小明做着完全相同的操作,但小张操作太慢了,刚刚点击完了苹果商品的图标,系统就显示了下一个人的号码 5 号。此时小明只能被迫终止自己的操作,让出操作台的权利。

ID 为 5 号的学生小王接到通知,兴冲冲地前往超市,并在领证处问管理人员,“给我一张学生证!”

管理人员找了找,发现学生证已经被小张取走了,只能告诉小王,“抱歉,学生证暂时没有,请到后面的学生证等待区排队吧!”。小王没办法,只能乖乖去排队了。

这时操作台再次显示了 3 号,也就是刚刚操作到一半的小张。小张此时还在超市里,并不需要重新进入,于是小张赶紧到操作台前继续着刚刚的操作,取走了一个苹果,离开了超市,交还了学生证。

此时领证处的管理人员收到了学生证,对着后面的学生证排队区喊,“学生证有啦,排队的人过来取吧!”

正在排队等证的 5 号小王听到后,从排队的队列里出来,准备领证并进入超市。但此时操作台上显示的号是另一个学生 10 号,10 号学生拿走了学生证,进入超市开始操作。

操作到一半,操作台时间限制又到了,显示了小王的 ID 5 号。小王刚从等待领证的队列里出来,终于获得了进行下一步行动的准许,于是走向了领证处,“给我一张学生证!”

由于学生证已经被 10 号拿走,管理人员只能说,“抱歉,学生证暂时没有,请到后面的学生证等待区排队吧!”。

小王一看等了那么久居然又被别人抢先了一步,刚想爆粗口,想到了这个学校的名言,“这个世界是不公平的”,于是又乖乖走向了学生证等待区,继续排队。

等 10 号操作完出来了,还了学生证,小王又被领证处管理员喊话,“学生证有啦,排队的人过来取吧!”。

小王走出排队区,而此时操作台终于显示了小王的号码 5 号。小王这次顺利领取了学生证,进入了超市,坐在了操作台上,登录了自己的系统。

小王想买苹果,于是点击了苹果商品的按钮,但系统显示苹果数量为 0!

小王此时想了想,有了个接下来的计划:

  • 继续呆在超市里,得空就去操作台上查询一下苹果的数量,直到有苹果为止。但继续呆在超市里,可能导致想向超市送苹果的学生拿不到学生证,而自己也就永远无法得到苹果了,显然不妥。
  • 所以小王的另一个想法是,走出超市,交还学生证,等下次有机会再进入超市查看苹果数量,直到有苹果为止。这样虽然有机会得到苹果,但太累了,假如这期间根本没人往超市送苹果,那这一趟趟其实是白费事的。
  • 于是小王想出了一个聪明的方案,我可以走出超市,到一个地方等待,在这里不会收到操作台的通知。但如果有人向超市送苹果了,那这个等待区里会发一个信号,这时超市才有可能是有苹果的,这时我从等待区里出来,等待叫号的机会。虽然苹果有可能被其他吃苹果的学生抢没,但这样起码不会浪费太多时间。

刚刚好超市旁边为每一种水果准备了好多等待区,一共有六个,分别是:苹果没了等待区,西瓜没了等待区,橘子没了等待区。苹果满了等待区,西瓜满了等待区,橘子满了等待区。

小王很聪明,去了苹果没了等待区,等待着有人往里送苹果的信号。这时小孙走进了超市,给超市添置了 5 个苹果,并换来了零花钱。

之后他立刻通知苹果没了等待区,给了个信号“超市有苹果啦!”,但此时小孙还没有走出超市呢。

小王在等待区里收到信号,立刻走出了等待区,等待被叫号,以完成自己吃苹果的任务。

但很不幸,在小王得到叫号机会之前,苹果又被其他几个学生抢光了,这时才轮到小王。

小王也很聪明,他考虑到了这种情况,没有直接取苹果,而是重新查询了一遍苹果数量,发现苹果数量为 0,于是重复之前的步骤,小王再次回到了苹果没了等待区。

接下来的时间里,小王不断在苹果没了等待区和学生证等待区移动,小王发现为了吃一个苹果太难了,必须同时满足:苹果没了等待区发来了“超市有苹果了”的信号,领证区此时有学生证,并且在操作台上查询出的苹果数量不为 0。

终于有一次,小王成功满足了这三个条件,在操作台上看到苹果的数量为 1!小王正激动地准备按下购买按钮,可此时操作台一闪,突然出现了别人的号码。

这个人是超市管理员,拿着一张特殊的超市管理员证顺利进入了超市,将苹果拿走,此时苹果数量又变成了 0。

之后又轮到小王操作,但小王并不知道之前发生的一切,他眼中明明看到苹果数量是 1。

小王为了保险起见,又多次查询了苹果数量,发现仍然是 1,于是兴奋地点下了购买按钮!

于是,操作台对根本没有苹果的储藏区发出了取苹果的指令,该系统根本没有想到会有这种事情发生,于是机器炸了,整个学校夷为平地。

数年后,学校慢慢被重新建立了起来,之前做操作台的人已经被枪毙了,高薪聘请了一位高人来建造,解决了之前的那个问题。

超市又顺利运转起来,有时超市只有一个人,有时超市会有三个人,分别是学生、老师、宿管阿姨,他们仨人互不影响,相安无事。学校的生活再次丰富了起来。

----------------------华丽的分割线-----------------------

这个故事包含了 Java 多线程的大部分核心问题,下面我把故事重新讲一遍。

有这么一个学校**(Java 虚拟机),里面有好多好多人(线程)**,我们简单分成学生、老师、以及宿管阿姨。

学校中间还有一个很奇葩的水果超市**(临界区),里面有个仓库放着苹果、西瓜、橘子(临界区里的受保护资源)**。

来这个超市的人,一方面可以拿走水果吃掉,另一方面也可以送来水果还钱。不过超市还有一个很奇葩的规则,就是学生只能去吃或者送苹果,老师则只能吃或者送西瓜,宿管阿姨只能吃或者送橘子。

这个超市的进出也很有规矩,来这个超市的人,必须持有相应的证件**(锁对象),学生则需要持有学生证,老师需要持有教师证,宿管阿姨需要持有阿姨证(不同的锁对象)**。

这三个证每个都分别只有一个,保管在超市门口的一个领证处**(获取锁的地方,可以说是堆吧),人们进入这个超市之前,必须先去取证处那里领取相应的证件(获取锁)**才能进入。

如果证件暂时被别人取走了拿不到**(获取锁失败),则需要到后方的等待区(同步队列 SychronizedQueue)**里面排队等证。

那这个等待区也有三个,分别是学生证等待区,教师证等待区,阿姨证等待区**(每个锁对象对应一个同步队列)**。

进入超市里面就更加奇葩了,不论是要从这个超市拿走水果,还是要送来水果,都需要通过一个操作台**(单核 CPU)**来控制,而这个操作台,同一时刻只能有一个人进行操作。

这个操作台为了防止有人霸占操作台过长时间,只允许一个人持续操作 10s**(CPU 时间片),10s 之后会在屏幕上显示一个 ID,只有这个 ID 的人才能来操作(线程切换)**。

至于选择什么号码,老师学生或是宿管阿姨都无法决定和干预,只能任凭这个操作台来决策**(操作系统决定线程的切换和时间的分配)**。

但好在,每个人在操作台上都有自己的账号**(线程的工作内存)**,操作一半被中断的数据并不会丢失。

这个故事的背景就介绍完了,下面这个学校就发生了各种各样的事。

首先我们假设,进这所学校的人,都是为了去超市做事情。首先人出现在学校外**(线程状态 NEW),人进入学校(线程状态 RUNNABLE)**。

某一时刻,操作台上显示了一个号码 2 号,这个号码通过各种学校大屏幕通知给所有的人。

于是 ID 为 2 号的学生小明看到了自己的号码,得知自己获得了进入超市操作控制台的权利**(获得 CPU 执行权)**,于是出发前往超市。

小明首先到超市门口,问领证处的管理人员,“给我一张学生证!”(获取锁)。管理人员找了找发现有一张学生证,于是便给了小明。

小明拿到了学生证,顺利进入超市**(获取锁成功,进入临界区),并坐上了操作台前,登录了自己的账号系统(准备好工作内存,开始执行临界区代码)**。

小明此行的目的是为了拿走一个苹果,于是他点击了苹果商品的图标,系统显示苹果还有 4 个。

于是小明顺利地拿走了苹果,系统将苹果数量 -1,将新的苹果数量 3 记录到总系统库中**(代码)**。

接着小明走出超市**(代码执行完毕出临界区),将学生证交还给了领证处(释放锁),走出了校园(线程状态 TERMINAL)**,消失在外面的人海中。

接着操作台上显示了 3 号,同样通过学校大屏幕通知给了所有人。ID 为 3 号的学生小张看到了自己的号码,得知自己获得了进入超市操作控制台的权利,于是出发前往超市。

小张和小明做着完全相同的操作,但小张操作太慢了,刚刚点击完了苹果商品的图标,系统就显示了下一个人的号码 5 号。此时小张只能被迫终止自己的操作,让出操作台的权利**(线程切换)**。

ID 为 5 号的学生小王接到通知,兴冲冲地前往超市,并在领证处问管理人员,“给我一张学生证!”。

管理人员找了找,发现学生证已经被小明取走了,只能告诉小王,“抱歉,学生证暂时没有,请到后面的学生证等待区**(同步队列 WaitQueue)排队吧!”(获取锁失败)**。

小王没办法,只能乖乖去排队了**(线程状态 BLOCKING)**。这时操作台再次显示了 3 号,也就是刚刚操作到一半的小张。

小张此时还在超市里**(不释放锁),并不需要重新进入,于是他赶紧到操作台前继续着刚刚的操作(线程切换,继续执行中断的代码),取走了一个苹果,离开了超市,交还了学生证(释放锁)**。

此时领证处的管理人员收到了学生证,对着后面的学生证排队区喊,“学生证有啦,排队的人过来取吧!”(通知同步队列出队)

正在排队等证的 5 号小王听到后,从排队的队列里出来,准备领证并进入超市。但此时操作台上显示的号是另一个学生 10 号,10 号学生拿走了学生证,进入超市开始操作。

操作到一半,操作台时间限制又到了,显示了小王的 ID 5 号。小王刚从等待领证的队列里出来,终于获得了进行下一步行动的准许,于是走向了领证处,“给我一张学生证!”。

由于学生证已经被 10 号拿走,管理人员只能说,“抱歉,学生证暂时没有,请到后面的学生证等待区排队吧!”

小王一看等了那么久居然又被别人抢先了一步,刚想爆粗口,想到了这个学校的名言,“这个世界是不公平的”,于是又乖乖走向了学生证等待区,继续排队。(非公平锁,并不是谁等的时间最长谁就获取锁)

等 10 号操作完出来了,还了学生证,小王又被领证处管理员喊话,“学生证有啦,排队的人过来取吧!”。

小王走出排队区,而此时操作台终于显示了小王的号码 5 号。小王这次顺利领取了学生证,进入了超市,坐在了操作台上,登录了自己的系统。

小王想买苹果,于是点击了苹果商品的按钮,但系统显示苹果数量为 0!

小王此时想了想,有了个接下来的计划:

  • 继续呆在超市里,得空就去操作台上查询一下苹果的数量,直到有苹果为止。但继续呆在超市里,可能导致想向超市送苹果的学生拿不到学生证,而自己也就永远无法得到苹果了,显然不妥。(Sychronized 代码块里循环等待)
  • 所以小王的另一个想法是,走出超市,交还学生证,等下次有机会再进入超市查看苹果数量,直到有苹果为止。这样虽然有机会得到苹果,但太累了,假如这期间根本没人往超市送苹果,那这一趟趟其实是白费事的。(Sychronized 代码块外循环等待)
  • 于是小王想出了一个聪明的方案,我可以走出超市,到一个地方等待**(Wait),在这里不会收到操作台的通知。如果有人向超市送苹果了,那这个等待区里会发一个信号(Notify)**,这时超市才有可能是有苹果的,这时我从等待区里出来,等待叫号的机会。

虽然苹果有可能被其他吃苹果的学生抢没,但这样起码不会浪费太多时间。(等待通知机制)

刚刚好超市旁边为每一种水果准备了好多等待区**(等待队列 WaitQueue),一共有六个,分别是:苹果没了等待区,西瓜没了等待区,橘子没了等待区,苹果满了等待区,西瓜满了等待区,橘子满了等待区(条件变量 Condition)**。

小王很聪明,走出超市交还学生证**(Wait 会释放锁),去了苹果没了等待区(Wait)**,等待着有人往里送苹果的信号(同步信号-唤醒)。

这时小孙走进了超市,给超市添置了 5 个苹果,并换来了零花钱。之后他立刻通知苹果没了等待区,给了个信号“超市有苹果啦!(AppleNotEmpty.notifyAll)”,但此时小孙还没有走出超市呢**(Notify 不释放锁)**。

小王在等待区里收到信号,立刻走出了等待区,等待被叫号,以完成自己吃苹果的任务。

但很不幸,在小王得到叫号机会之前,苹果又被其他几个学生抢光了,这时才轮到小王。

小王也很聪明,他考虑到了这种情况,没有直接取苹果,而是重新查询了一遍苹果数量**(Wait 一般配合 While 条件)**,发现苹果数量为 0,于是重复之前的步骤,小王再次回到了苹果没了等待区。

接下来的时间里,小王不断在苹果没了等待区和学生证等待区移动,小王发现为了吃一个苹果太难了,必须同时满足,苹果没了等待区发来了“超市有苹果了”的信号,领证区此时有学生证,并且在操作台上查询出的苹果数量不为 0。

终于有一次。小王成功满足了这三个条件,在操作台上看到苹果的数量为 1!小王正激动地准备按下购买按钮,可此时操作台一闪,突然出现了别人的号码。

这个人是超市管理员,拿着一张特殊的超市管理员证顺利进入了超市,将苹果拿走,此时苹果数量又变成了 0。

之后又轮到小王操作,但小王并不知道之前发生的一切,他眼中明明看到苹果数量是 1。

小王为了保险起见,又多次查询了苹果数量,发现仍然是 1**(非 Volatile 修饰的变量不保证线程之间的可见性)**,于是兴奋地点下了购买按钮!

于是,操作台对根本没有苹果的储藏区发出了取苹果的指令,该系统根本没有想到会有这种事情发生,于是机器炸了,小王牺牲**(抛出运行时异常,线程释放锁并终止)**。

数年后,之前做操作台的人已经被枪毙了,学校又高薪聘请了一位高人来建造,解决了之前的那个问题**(Volatile)**。

超市又顺利运转起来,有时超市只有一个人**(不同线程进入锁对象相同的临界区会互斥,只有一个线程可以进入),有时超市会有三个人(不同锁对象的临界区不互斥)**,分别是学生、老师、宿管阿姨,他们仨人互不影响,相安无事。学校的生活再次丰富了起来。

故事讲完了,虽然不能解释全部并发编程的内容,也不能处处都很恰当地说明细节,但却是一个很有趣的思考过程。

希望大家也能积极讨论下故事中的错误和不完善的地方,一起将故事讲的更好。

下面整理一下故事中出现的东西和寓意:

东西 寓意
线程
通行证 锁对象
水果超市 临界区代码
水果 受保护资源
操作台 CPU
叫号 时间片分配
领证处 获取锁
等待区 等待队列
领证排队区 同步队列
水果储藏区 主内存
每个人的账号系统 工作内存
文章目录