最近在给别人讲解Java并发编程面试考点时,为了解释锁对象这个概念,想了一个形象的故事。后来慢慢发现这个故事似乎能讲解Java并发编程中好多核心概念,于是完善起来形成了了这篇文章。大家先忘记并发编程,只听我给你讲个故事。

  故事可能比较奇怪。有这么一个学校,里面有好多好多人,我们简单分成学生老师、以及宿管阿姨。学校中间还有一个很奇葩的水果超市,里面有个仓库放着苹果西瓜橘子。来这个超市的人,一方面可以拿走水果吃掉,另一方面也可以送来水果还钱。不过超市还有一个很奇葩的规则,就是学生只能去吃或者送苹果,老师则只能西瓜,宿管阿姨只能橘子。

  这个超市的进出也很有规矩,来这个超市的人,必须持有相应的证件,学生则需要持有学生证,老师需要持有教师证,宿管阿姨需要持有阿姨证。这三个证每个都分别只有一个,保管在超市门口的一个领证处,人们进入这个超市之前,必须先去取证处那里领取相应的证件才能进入。如果证件暂时被别人取走了拿不到,则需要到后方的等待区里面排队等证。那这个等待区也有三个,分别是学生证等待区,教师证等待区,阿姨证等待区。

  进入超市里面就更加奇葩了,不论是要从这个超市拿走水果,还是要送来水果,都需要通过一个操作台来控制,而这个操作台,同一时刻只能有一个人进行操作。这个操作台为了防止有人霸占操作台过长时间,只允许一个人持续操作10s钟,10s之后会在屏幕上显示一个ID,只有这个ID的人才能来操作,至于选择什么号码,老师学生或是宿管阿姨都无法决定和干预,只能任凭这个操作台来决策。但好在,每个人在操作台上都有自己的账号,操作一半被中断的数据并不会丢失。

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

  首先我们假设,进这所学校的人,都是为了去超市做事情。某一时刻,操作台上显示了一个号码2号,这个号码通过各种学校大屏幕通知给所有的人。于是ID为2号的学生小明看到了自己的号码,得知自己获得了进入超市操作控制台的权利,于是出发前往超市。小明首先到超市门口,问领证处的管理人员,“给我一张学生证!”。管理人员找了找发现有一张学生证,于是便给了小明。小明拿到了学生证,顺利进入超市,并坐上了操作台前,登录了自己的账号系统。小明此行的目的是为了拿走一个苹果,于是他点击了苹果商品的图标,系统显示苹果还有4个。于是小明顺利地拿走了苹果,系统将苹果数量-1,将新的苹果数量3记录到总系统库中。接着小明走出超市,将学生证交还给了领证处,走出了校园,消失在外面的人海中。

  接着操作台上显示了3号,同样通过学校大屏幕通知给了所有人。ID为3号的学生小张看到了自己的号码,得知自己获得了进入超市操作控制台的权利,于是出发前往超市。小张和小明做着完全相同的操作,但小张操作太慢了,刚刚点击完了苹果商品的图标,系统就显示了下一个人的号码5号。此时小明只能被迫终止自己的操作,让出操作台的权利。ID为5号的学生小王接到通知,兴冲冲地前往超市,并在领证处问管理人员,“给我一张学生证!”。管理人员找了找,发现学生证已经被小明取走了,只能告诉小王,“抱歉,学生证暂时没有,请到后面的学生证等待区排队吧!”。小王没办法,只能乖乖去排队了。

  这是操作台再次显示了2号,也就是刚刚操作到一半的小明。小明此时还在超市里,并不需要重新进入,于是小明赶紧到操作台前继续着刚刚的操作,取走了一个苹果,离开了超市,交还了学生证。此时领证处的管理人员收到了学生证,对着后面的学生证排队区喊,“学生证有啦,排队的人过来取吧!”。正在排队等证的5号小王听到后,从排队的队列里出来,准备领证并进入超市。但此时操作台上显示的号是另一个学生10号,10号学生拿走了学生证,进入超市开始操作。操作到一半,操作台时间限制又到了,显示了小王的ID5号。小王刚从等待领证的队列里出来,终于获得了进行下一步行动的准许,于是走向了领证处,“给我一张学生证!”,由于学生证已经被10号拿走,管理人员只能说,“抱歉,学生证暂时没有,请到后面的学生证等待区排队吧!”。小王一看等了那么久居然又被别人抢先了一步,刚想爆粗口,想到了这个学校的名言,“这个世界是不公平的”,于是又乖乖走向了学生证等待区,继续排队。

  等10号操作完出来了,还了学生证,小王又被领证处管理员喊话,“学生证有啦,排队的人过来取吧!”。小王走出排队区,而此时操作台终于显示了小王的号码5号。小王这次顺利领取了学生证,进入了超市,坐在了操作台上,登录了自己的系统。小王想买苹果,于是点击了苹果商品的按钮,但系统显示苹果数量为0!小王此时想了想,有了个接下来的计划:

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

  刚刚好超市旁边为每一种水果准备了好多等待区,一共有六个,分别是:苹果没了等待区,西瓜没了等待区,橘子没了等待区。苹果满了等待区,西瓜满了等待区,橘子满了等待区。小王很聪明,去了苹果没了等待区,等待着有人往里送苹果的信号。

  这时小孙走进了超市,给超市添置了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)。

  这是操作台再次显示了2号,也就是刚刚操作到一半的小明。小明此时还在超市里(不释放锁),并不需要重新进入,于是小明赶紧到操作台前继续着刚刚的操作(线程切换,继续执行中断的代码),取走了一个苹果,离开了超市,交还了学生证(释放锁)。此时领证处的管理人员收到了学生证,对着后面的学生证排队区喊,“学生证有啦,排队的人过来取吧!”(通知同步队列出队)。正在排队等证的5号小王听到后,从排队的队列里出来,准备领证并进入超市。但此时操作台上显示的号是另一个学生10号,10号学生拿走了学生证,进入超市开始操作。操作到一半,操作台时间限制又到了,显示了小王的ID5号。小王刚从等待领证的队列里出来,终于获得了进行下一步行动的准许,于是走向了领证处,“给我一张学生证!”,由于学生证已经被10号拿走,管理人员只能说,“抱歉,学生证暂时没有,请到后面的学生证等待区排队吧!”。小王一看等了那么久居然又被别人抢先了一步,刚想爆粗口,想到了这个学校的名言,“这个世界是不公平的”,于是又乖乖走向了学生证等待区,继续排队。(非公平锁,并不是谁等的时间最长谁就获取锁)

  等10号操作完出来了,还了学生证,小王又被领证处管理员喊话,“学生证有啦,排队的人过来取吧!”。小王走出排队区,而此时操作台终于显示了小王的号码5号。小王这次顺利领取了学生证,进入了超市,坐在了操作台上,登录了自己的系统。小王想买苹果,于是点击了苹果商品的按钮,但系统显示苹果数量为0!小王此时想了想,有了个接下来的计划:

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

  刚刚好超市旁边为每一种水果准备了好多等待区(等待队列WaitQueue),一共有六个,分别是:苹果没了等待区,西瓜没了等待区,橘子没了等待区。苹果满了等待区,西瓜满了等待区,橘子满了等待区(条件变量Condition)。小王很聪明,走出超市交还学生证(wait会释放锁),去了苹果没了等待区(wait),等待着有人往里送苹果的信号(同步信号-唤醒)。

  这时小孙走进了超市,给超市添置了5个苹果,并换来了零花钱。之后他立刻通知苹果没了等待区,给了个信号“超市有苹果啦!(AppleNotEmpty.notifyAll)”,但此时小孙还没有走出超市呢(notify不释放锁)。小王在等待区里收到信号,立刻走出了等待区,等待被叫号,以完成自己吃苹果的任务。但很不幸,在小王得到叫号机会之前,苹果又被其他几个学生抢光了,这时才轮到小王。小王也很聪明,他考虑到了这种情况,没有直接取苹果,而是重新查询了一变苹果数量(wait一般配合while条件),发现苹果数量为0,于是重复之前的步骤,小王再次回到了苹果没了等待区。

  接下来的时间里,小王不断在苹果没了等待区和学生证等待区移动,小王发现为了吃一个苹果太难了,必须同时满足,苹果没了等待区发来了“超市有苹果了”的信号,领证区此时有学生证,并且在操作台上查询出的苹果数量不为0。终于有一次。小王成功满足了这三个条件,在操作台上看到苹果的数量为1!小王正激动地准备按下购买按钮,可此时操作台一闪,突然出现了别人的号码。这个人是超市管理员,拿着一张特殊的超市管理员证顺利进入了超市,将苹果拿走,此时苹果数量又变成了0。之后又轮到小王操作,但小王并不知道之前发生的一切,他眼中明明看到苹果数量是1。小王为了保险起见,又多次查询了苹果数量,发现仍然是1(非volatile修饰的变量不保证线程之间的可见性),于是兴奋地点下了购买按钮!于是,操作台对根本没有苹果的储藏区发出了取苹果的指令,该系统根本没有想到会有这种事情发生,于是机器炸了,小王牺牲(抛出运行时异常,线程释放锁并终止)。

  数年后,之前做操作台的人已经被枪毙了,学校又高薪聘请了一位高人来建造,解决了之前的那个问题(volatile)。超市又顺利运转起来,有时超市只有一个人(不同线程进入锁对象相同的临界区会互斥,只有一个线程可以进入),有时超市会有三个人(不同锁对象的临界区不互斥),分别是学生、老师、宿管阿姨,他们仨人互不影响,相安无事。学校的生活再次丰富了起来。

  故事讲完了,虽然不能解释全部并发编程的内容,也不能处处都很恰当地说明细节,但确是一个很有趣的思考过程,希望大家也能积极讨论下故事中的错误和不完善的地方,一起将故事讲的更好。下面整理一下故事中出现的东西和寓意。

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

一个故事搞懂Java并发编程的更多相关文章

  1. ERP 到底是什么? 一则故事搞懂ERP

    你知道什么是ERP? ERP是什么? 你知道什么是ERP吗? (通俗易懂版) 一个故事搞懂“ERP” 一天中午,丈夫在外给家里打电话:“亲爱的老婆,晚上我想带几个同事回家吃饭可以吗?”(订货意向) 妻 ...

  2. 一个例子搞清楚Java程序执行顺序

    当我们new一个GirlFriend时,我们都做了什么? 一个例子搞懂Java程序运行顺序 public class Girl { Person person = new Person("G ...

  3. Java并发编程-看懂AQS的前世今生

    在具备了volatile.CAS和模板方法设计模式的知识之后,我们可以来深入学习下AbstractQueuedSynchronizer(AQS),本文主要想从AQS的产生背景.设计和结构.源代码实现及 ...

  4. 干货:Java并发编程必懂知识点解析

    本文大纲 并发编程三要素 原子性 原子,即一个不可再被分割的颗粒.在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败. 有序性 程序执行的顺序按照代码的先后顺序执行.(处理器可能会 ...

  5. Java并发编程中的设计模式解析(二)一个单例的七种写法

    Java单例模式是最常见的设计模式之一,广泛应用于各种框架.中间件和应用开发中.单例模式实现起来比较简单,基本是每个Java工程师都能信手拈来的,本文将结合多线程.类的加载等知识,系统地介绍一下单例模 ...

  6. Java并发编程原理与实战十五:手动实现一个可重入锁

     package com.roocon.thread.ta1; public class Sequence { private MyLock lock = new MyLock(); private ...

  7. java 并发编程——Thread 源码重新学习

    Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

  8. Java 并发编程(一):摩拳擦掌

    这篇文章的标题原本叫做——Java 并发编程(一):简介,作者名叫小二.但我在接到投稿时觉得这标题不够新颖,不够吸引读者的眼球,就在发文的时候强行修改了标题(也不咋滴). 小二是一名 Java 程序员 ...

  9. 【搞定 Java 并发面试】面试最常问的 Java 并发进阶常见面试题总结!

    本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.觉得内容不错 ...

随机推荐

  1. Android利用碎片fragment实现底部标题栏(Github模板开源)

    在安卓开发当中,一个十分重要的布局则是底部标题栏了,拥有了底部标题栏,我们就拥有了整个软件UI开发的框架,一般而言,整个软件的布局首先就是从底部标题栏开始构建,然后再开始其他模块的编写,组成一个完善的 ...

  2. Java8 日期时间API

    一.转换 1.与字符串 //LocalDateTime 转 字符串 String str = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss ...

  3. 【Android - IPC】之AIDL简介

    参考资料: 1.<Android开发艺术探索>第二章2.4.4 2.Android AIDL Binder框架解析:http://blog.csdn.net/lmj623565791/ar ...

  4. 关于JAVA,特点,历史,编译式的语言&解释式的语言,什么是java?JDK?DOS?一次编译到处运行原理。

    1.java语言的特点: 简单的:面向对象的:跨平台(操作系统)的(一次编译,到处运行):高性能的: 2.类名的首字母大写,方法小写: 3.历史: java2(即java),为什么加个2呢?1998年 ...

  5. day26

    绑定方法 分为对象绑定方法和类的绑定方法 绑定方法的特殊之处 绑定给谁就是谁来调用 类的绑定方法 绑定给类,类来调用,会把类自身传过来 不需要通过对象 ,只需要通过类就能获取到一些东西的时候,用类的绑 ...

  6. Linux基础命令小技巧

    总结 CentOS(Community Enterprise Operating System,中文意思是:社区企业操作系统)是Linux发行版之一,它是来自于Red Hat Enterprise L ...

  7. Frida用法之函数操作

    Frida接口功能介绍   Frida是个so级别的hook框架,它可以帮助开发.安全人员对指定的进程的so模块进行分析.它主要提供了功能简单的Python接口和功能丰富的JS接口,使得hook函数和 ...

  8. C 基础数据类型 性能测试

    简单测试了C语言中分别使用16位整数和32位整数实现的定点数和内建浮点数的乘除性能: 在release 下 循环 1 0000 0000 * 20次 的时间: CPU:7700K/4.2Ghz 定点数 ...

  9. KVM http网络加载镜像报错(mount: wrong fs type, bad option, bad superblock on /dev/loop0)

    curl: (23) Failed writing body (7818 != 16384)loop: module loadeddracut-initqueue[579]: mount: wrong ...

  10. oracle监听查看、启动和停止

    oracle监听查看.启动和停止 查看监听lsnrctl status 停止监听lsnrctl stop 启动监听lsnrctl start