JAVA-初步认识-第十四章-线程间通信-多生产者多消费者机制
一.
接下来,我们将等待唤醒机制深化。前面讲述的时候并没有体现出等待唤醒机制的精髓,下面的生产者消费者才是经典的案例。
生产,和消费是同时进行的。消费者消费的时候,看到仓库里东西没有了,就提醒生产者。生产者生产满仓库的话,就暂不生产了,直到消费者消费完了,来提醒它,再生产。
生产多少有一个数量的问题,消费品要有所属的数组来表征生产的数目,这样比较麻烦,有数组指针。现在简单地讲解,只定义一个变量。对消费品进行编号,每消耗完一个,再进行下一个的生产(编号这个东西,不采用数组,是要采用循环么?)。这就需要等待唤醒机制。
这个例子和上个例子是一样的道理,不再赘述。
二.
现在修改一下,之前是生产一个消费一个,现在是多生产,多消费。(多生产和多生产者不一样)
现在有四个线程,两个负责生产,两个负责消费。每次都生产一只,你生产一次,我生产一次。
DOS出现了问题,生产者生产了一次408,消费者消费了两次408。还有一种情况是,生产了两次,消费了一次,比如生产408和409,但是只消费了409,以及下面更为极端的情况。
现在的情况是同步了,但是不安全。为什么会出现这样的问题呢?
二.
现在来思考为什么出现这样的问题,先从代码中走一遍,再画一张图。
为什么多生产多消费出现问题,而单生产单消费不出现问题。这在等待唤醒机制中是最为常见的现象
假设生产者得到了cpu的执行权,读到set方法就拿到执行权了,就拿到锁了。如果flag是假,不用wait,this.name就是烤鸭1,count变为2,把标号往后撤一下顺便生产第二次烤鸭。接着,输出语句打印了烤鸭1,意味着我们生产了一次。接着就是flag为true,且notify唤醒了。这个notify没什么作用,因为没有所在线程。生产者执行完set方法后,出来后释放了锁。但是它还是可能获取cpu执行权,因为cpu执行set函数不会执行一次(有循环语句么,为什么能重复执行?),所以这哥们又进入了set函数,有相当大的可能这种情况。接着判断flag时,它已经是true了,就进入了异常处理等待过程,线程t0进入等待。
在set函数那儿,有t0和t1在待命,现在t0等待了,剩下的就是t1。out函数那有t2和t3线程,所以总的剩下处于阻塞状态的线程就是t1,t2和t3。在这种情况下,三个线程将有一个会被执行到,而且是随机的。
假如t1被执行到,t1线程就进来了,flag还是true,这样一来,t1也就等待了。还有两个线程t1和t2,这两个线程来争夺cpu执行权。假设t2拿到了执行权,!flag为false,不用执行异常处理。接下来的输出语句是消费了烤鸭1。然后flag置为false,接着notify,现在线程池里有两个线程,t0和t1。这样就唤醒了同锁两个线程中的一个。假设t0被唤醒了,立即获得执行权吗?不一定,我们只能说t0活了。但是t2还握有执行权,t2在执行完notify后,握有执行权,又进入out方法。这时,!flag为true,这样t2就进入wait等待。这时,活着的线程还剩t0和t3,假设t3抢到执行权,进入out方法后,!flag为true,这样t3就进入wait等待。这时,只剩下了t0线程活着了。关键点到了,t0活了以后,不在判断if小括号中的flag标记了,继续往下走。这时,this.name为烤鸭2, count为3。输出生产烤鸭2。
接着flag为true,而且notify唤醒,这一唤醒,池子中有三个线程。三个线程中会有任意一个线程醒过来。如果活的是t2或者t3,这事儿就和谐了。如果活的是t1,不一定立即运行。t0还拿到执行权,flag还为ture,t0再次进入set方法,就等待了。这时就剩下t1或者了,t1不判断flag标记了,直接往下走。烤鸭3就出来了,count为4。接着输出生产烤鸭3。
这时就产生了安全问题,烤鸭生产了3次,消费了一次。
不要以为加了同步,就没问题了。这里面还有等待和唤醒机制。这时需要重新思考问题,烤鸭2没被消费到,什么原因造成的?原因在这,有人说是t1被唤醒了造成的,我觉着,t1是被唤醒了,最终t1也得被唤醒,它不醒还干不干活。它还得被唤醒,问题在这,t0本身生产的烤鸭2, t1那边醒了之后,它连标记都没判断,就继续生产了,但凡t1醒了之后,判断flag标记为true了,它就不生产。
现在,我们怎么让t1在处理异常语句时,醒过来时回过来判断flag标记呢?写个while就可以,执行完处理异常语句后,还要回来判断while的条件。
if和while有啥区别?if判断一次,while判断多次,就是在活了生产之前,再判断是否里面还有烤鸭,之前活了是直接生产。下面消费者那也要改成while。
改完后,运行时卡在那儿了,以前我们讲述过,这就是死锁。
为什么会锁着呢?
不再从头开始赘述,直接从关键点开始讲。假设t0和t1被wait了,t3就进来了,t2就消费了一次,正常消费。它notify一次,唤醒了一个线程,比如说t0活了。t2就wait了,t3呢也wait了,因为标记问题。现在就t0活着,就从这开始。t0回过来判断标记,t2已经把标记改为false了,t0判断时,不满足继续往下走。它就生产了一只烤鸭,生产几号都不用再管了。生产完以后,notigy一次,这时有t1,t2和t3三种线程。如果唤醒了t1,t0握有执行权,回过来判断,flag=true满足了,t0等着了。现在活着t1,t1活着,回来判断flag标记,还是true,t1也就等在这儿了。这样一来,四个线程全部都在等待。这就是死锁,大家谁都动不了。这是死锁的另外一种情况。
总结:在这的讲解中,过于随意,没那么多确定好的规则,或者说情况很多,只能讲述其中一种。