写在前面:对于多线程的使用可以说是对于程序员能力评判的重要指标之一了,在面试中可以说是一定会问到多线程相关的知识点了,但是,由于多线程相关的知识点实在太多了,要往深了扩展又更是能扩展出一大堆东西,所以我们只能一个一个知识点来逐步的掌握多线程。现在,我们就从比较经典的生产者消费者问题来了解多线程吧。有关于张三的背景介绍参考: 张三背景介绍
张三又开始了新的一轮面试。
面试官:先来个自我介绍吧。
张三:!@#¥%……&*。
前面的一阵交谈此处省略
面试官:线程的状态有几个?分别介绍一下呗。
张三心想,好家伙,一来就问这个,还好我刚准备过,不然有可能真回答不上来。
张三:线程的状态有5种,分别是新建状态、就绪状态、运行状态、阻塞状态、死亡状态。新建状态指的是,我们刚创建出线程对象的时候的线程状态。而当我们调用了对象的start方法后,线程就进入了就绪状态,但此时线程并不一定立即运行,只有当它获得了CPU时间片时它才能进入到运行状态。处于运行状态时,线程可以被sleep、wait等方法影响而进入阻塞状态,当然阻塞状态也可以通过notify等方法再次回到运行状态。而死亡状态指的是线程执行完毕或者异常退出的状态。
面试官:既然你刚刚提到了sleep和wait,说说他们两的区别。
张三:sleep是Thread类中的方法,而wait是Object中的方法。sleep调用时会不会释放锁,而wait会释放锁。sleep是到设置的时间后唤醒,而wait需要通过notify()或者notifyAll()方法唤醒。我了解的区别就这些了。
面试官:写一个生产者消费者的案例,写的出来吗?
张三:以前写过,现在突然让我写的话应该还是没办法一次写对的。
面试官:没事,创建线程有几种方法?
张三:继承Thread类...
后面的省略,咱们这次的知识点只与生产者消费者有关。
面试官:这次的面试就先到这里,后面会有我们的HR联系你。
张三:好的……
-------------------------------------------------------------------------------------------
其实本次的面试中,涉及到的知识点很少很少,与庞大的多线程体系来比可能就像是足球场上的足球那样渺小。但这些知识点能很好的区分开发人员究竟有没有真正入门并开始掌握多线程,所以我们还是要把这个知识点透彻的弄清楚啊。
先来看看线程有哪些状态吧,打开百度一搜,约有百分之80的博客告诉我们多线程有5种状态,又有百分之15的人告诉我们是6种,剩下的还有告诉我们是7种的,究竟状态有几种,我们该如何靠自己来分辨清楚呢?
口说无凭,我们想弄清楚,一定是要通过源码来确定,所以我们想要看状态有几个,首先应该清楚状态指的是谁的状态。既然是线程的状态,那说明我们可以去线程类中找到对应的状态有哪些。所以我们跟进代码Thread类,看看能发现什么。
于是我们在Thread类中找到了一个枚举类叫State,再仔细一看,它定义了6个枚举值,分别为NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
那么是不是以为着线程有六种状态呢?我们再仔细看一下注释说到是什么。
注释中简单明了的介绍了这些状态,我们只需要拥有一般的词汇量就能够清楚明白这些状态是什么含义。NEW、RUNNABLE、BLOCKED和TERMINATED状态都基本是见名知意了,通过它的名字以及简短的介绍我们就能够理解什么是新建状态、运行状态、阻塞状态以及死亡状态。关键就在于WAITING和TIMED_WAITING好像名字很接近,这两种状态究竟有什么区别,为什么要定义两种枚举类呢?
再看看对这两种状态详细的注释吧:
原来,当调用wait() 、join()、park()等方法没有设等待时间时,线程进入的就是WAITING状态,直到调用了notify()或者notifyAll()方法为止。而如果调用的是sleep、wait(long)、join(long)等方法时,就会进入TIMED_WAITING状态了。简单来说,WAITING状态是等待别人唤醒,而TIMED_WAITING是自己到时间就能够唤醒,本质上应该归于等待就绪状态还是可运行状态看个人的理解了,总而言之处于这两种状态的时候线程并没有在执行,但也完全可以转换为运行态。
所以我们在回答wait和sleep的区别的时候,如果能结合着源码来分析一番,应该会很加分的把。
线程的状态我们了解完后先放一边,这个线程的生产着消费者我们能否带着理解彻底弄明白呢?
为了能讲清楚知识点,我们来一边带着疑问一边编码来实现多线程的生产者消费者。
首先我们需要先确定一下场景,既然是要实现多线程的生产者与消费者的代码,先需要理解多线程的作用以及可能会发生的问题。
我们使用多线程来实现生产者消费者模型,自然是希望生产和消费能够同时进行,假如说我们想要卖出100个产品,不会希望说等100产品都生产完了才开始卖,更希望的是能一边生产一边就在卖了才是最有效率的。那如果说是一边生产一边卖,那又要考虑到生产如果比卖的快,那也不能一直生产,库房放不下。那如果卖的比生产快,还没生产出来卖东西也是不符合业务逻辑的。所以在我们实现生产者消费者的代码时,这些是需要我们考虑的。
那么我们说干就干,首先我们定义一个店员类Clerk,毕竟不管我们生产东西或者是买东西都需要经过店员才能够做得到,在我们的代码中由他来管理我们的产品数量。
在店员类中我们定义一个进货的方法来增加产品的数量。为了防止多个线程修改同一个属性,我们对方法添加一个synchronized锁(有关于synchronized我们以后详细分析,当前先理解它能保证线程安全就行了)。于是关键的地方来了,假设我们最多只允许库房里具有的产品数量为3个,怎么办到呢?我们只需要加一个while判断条件,当产品数量大于等于3的时候,我们调用wait方法,使这个线程进入就绪状态就行了。注意这里我们不能够使用if判断,因为在其他线程调用notifyAll()时,if条件下wait住的线程有可能又在不符合条件的情况下继续执行,以至于出现问题,所以我们的进货的方法如下:
同理,我们也需要有方法来卖货,卖的时候库房里没有或也不合适吧,所以我们也要控制当产品为0的时候要记得wait住。由于前面进货时如果库房满了会做wait操作,所以记得在卖完后要调用notifyAll方法通知生产者可以继续执行了啊,卖货的方法如下:
店员是个模型方法类,要执行多线还是得通过那几种创建线程的方法,这里我们就简单的使用实现Runnable接口来创建线程了,等以后开始讲线程池时再使用更为推荐的做法。于是我们这个生产者假设要生产20件产品,生产的速度为100ms一件,代码如下:
同理消费者的代码也类似,但是这里我们假设消费的会慢一些,200ms才能卖出一件,代码如下:
然后就要到激动人心的执行环节了,假设我们有两个线程来生产,又有两个线程来消费,但是前面我们也说了,消费的会慢一点,执行的效果会是怎么样呢?
由于输出打了很多,我们就看最下面的返回结果,首先消费的会比生产的慢一些,所以我们能看到打印了很多此产品已满,而只有当消费者消费了之后,生产者才能够继续生产。而在最后,消费者也是顺利的把所有的产品都消费完了。
由于这个代码已经算是执行的没有问题的代码了,假设我们在之前说到的while那里使用了if,结果会是怎么样呢?
答案是线程并没有按想象中的控制住,生产者有可能会生产出超出库存容量的产品。有兴趣的同学可以试一试。
那假设我们消费的速度比生产的速度快呢?我们只需要把两边sleep的时间互换一下就能实现效果。
所以我们看到消费者必须等生产者生产完了才能够进行消费,否则会提升缺货。
这一次我们的生产者消费者的代码到这里就实现完成了,总结起来也还是比较简单的,也就是生产达到一定数量需要调用wait方法来进行等待,消费到一定数量也需要等待,生产消费结束后需要通知其他线程继续执行就行了。唯一需要注意的坑就是判断数量要使用while来判断就好了。
如果大家能把代码都敲一遍,相信对生产者消费者的代码会有一定的了解了,对多线程的认识更进一步。那么假设你们这个知识点明白了,那么另一个经典问题,多个线程循环打印ABC,清楚该如何实现吗?这个问题留作大家思考,涉及到多线程的另一个知识点哦。
张三在这次的面试中收获颇丰,我们下周见。