01-多线程(线程间通信-示例代码)
上一篇讲的卖票的例子,几个线程同时在卖票,它们执行的代码是相同的。
现在假如有一个资源,一个线程往里面存数据,一个线程往出取数据,一进一出,两个线程同时进行,这时它们运行的代码是不一致的。
一个简单的例子:
我们需要描述三部分内容:
第一部分,资源的内容,则里有name和sex。
第二部分,Input的方法。
第三部分,Output的方法。
线程间通讯:
其实就是多个线程在操作同一个资源,但是操作的动作不同。
将上面的例子用代码表示出来~
进一步细化:
输入:
输出:
在主函数中创建对象并调用:
运行结果:
本来,mike是man,丽丽是女女女女女,但是我们发现,运行出来,mike有时候也是女女女女女,丽丽有时候也是man。
为什么会这样呢?
分析:
02-多线程(线程间通信-解决安全问题)
现在我们来解决刚刚发生的安全问题。
我们用同步代码块synchronized将被操作的代码封装起来:
运行:
发现问题还在。
我们看看它是否满足同步的两个前提:
1,两个及两个以上线程?不满足,这里只是一个线程,是输入的那个线程,输出的在另一个类中。所以只是同步了一个线程。
怎么办~改呀~将另一个也同步起来:
运行,问题还是存在:
我们再看看同步的第二个前提它们是否满足:
2,用的是同一个锁? 不是的。
改起来,我们随便找一个对象,就都用Input吧:
编译运行:
问题解决啦。
那么我们这个程序中共有四个类,写Res.class、Output.class、InputOutputDemo当锁都OK。
其实还有一个对象是唯一的,就是主函数中创建的Res的对象r。
我们也可以这样写:
Output类也是这样写哦。代码略。这样也是完全OK的哦。
03-多线程(线程间通信-等待唤醒机制)
为什么会出现大片的mike man和丽丽 女女女女女呢?
分析:Input抢到执行权后,输入了mike man,输入完成后,接下来Input和Output都存在抢到执行权的可能性。并不是说这次是Input抢到了,下次就一定是Output抢到。
所以,如果一直是Input抢到执行权,每次输入的name和sex,都会将前一次的输入内容覆盖掉。忽然,某一个时刻,Output抢到了执行权。它会只输出一次吗?不是。CPU执行它的时候,它有可能会输出多次,所以会出现一打印一大片的情况。
而我们的需求是,我输入一个,你输出一个。
为了满足这个需求,我们加一个flag标记,表示里面有没有值。输入线程在输入数据的时候,先判断flag是否为false,若是false,则代表里面没有值,输入线程就在这里比如存一个mike nan。存完了以后,输入线程是不是还会持有执行权?它在存完数据之后,就应该做一件事情,那就是将flag标记改为true,代表里面有数据。这时如果输入线程再次拿到执行权,而flag为true,它就不能往里存了。
这个时候该怎么办呢?我们是不是应该让输入线程在这里等着不要动呀?那就sleep一下吧。sleep多长时间合适呢?不确定呀。什么时候应该醒呢?
最靠谱的应该是输出线程将数据取出后醒来。所以这个时候wait最合适啦!“你先等着别动喔,我叫你你再动喔。”所以输入线程就wait了,一wait就冻结辣。
冻结的特点是什么呢?放弃执行资格。
这时就只剩输出线程可以争夺执行权啦,所以它拿到了执行权,判断flag为true,代表里面有东西,于是拿出了数据,然后将flag改为false。这个时候就应该叫一下输入线程啦,“宝宝你该往里面存东西啦。”
于是输入线程和输出线程就这样交换着输入--输出,输入线程和输出线程也适时的等待对方存数据/取数据。
现在我们在讲的,就是今天的重点:等待唤醒机制。
这种情况非常常见~
程序怎么写呢?
如下:
给资源类中加一个flag标志:
Input类中,添加判断flag值的代码,若为true则wait,若为false则存值,存完值后将flag赋值为true,并唤醒另一个线程:
Output类中,添加判断flag值的代码,若为false,则wait,若为true,则输出数据,输出数据后将flag赋值为false,并唤醒另一个线程:
对啦,notify只能唤醒一个,如果想唤醒好多个,还有一个方法,叫notifyAll。
不说其它的啦,我们去Thread类中找一下我们需要用到的wait和notify方法,发现它木有这些方法呀。后来又看到介个:
原来它们竟然都是从object继承过来哒。
看一下wait:
我们发现它抛出异常啦。我们想使用wait的话,就只能try。
再看一下wait方法的描述:
因为只有同步的时候才需要锁,所以,wait、notify、notifyAll这几个方法,都是用在同步中哒~
而用在同步中,容易产生问题,什么问题呢?
你必须要标识出这个wait,它所操作的线程所属的锁。
这里的wait是指,持有r这个锁的线程。
为什么?
因为同步会出现嵌套。
是不是有两个锁呀?
而notify所notify的是r这个锁所在的线程。
所以这里也要标识r哦:
同理,Output方法也是:
可是,为什么wait、notify这种用于操作线程的方法却定义在了object当中呢?
我们回想一下,锁是不是可以是任意对象呀?
而任意对象可以调用的方法,是不是应该定义在我们的上帝类Object当中呢?
这下就全明白辣!
总结一下:
wait、notify、notifyAll都使用在同步中,因为要对持有监视器(锁)的线程操作。
所以要使用在同步中,因为只有同步才具有锁。
为什么这些操作线程的方法要定义在Object类中呢?
因为这些方法在操作同步线程时,都必须要标识它们所操作线程持有的锁。
只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒。
不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁。
而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。
好啦,说了这么多,我们编译运行一下:
OK~需求成功解决,耶٩(๑òωó๑)۶
04-多线程(线程间通信-代码优化)
刚刚程序写完了,但我们发现一个问题,就是它的代码没有进行优化。
哪里没有优化捏?
跟我来~
1,进行数据的私有化。
2,对数据私有化后,要对外提供公共的访问方法。
我们又发现,在set中,对name和sex进行赋值的时候,有可能出现安全问题,比如输完name在这里停住了,还没来得及输入sex就被输出线程抢走了cpu执行权,这样就会出问题哦。
所以需要将这两个语句同步,而这个方法中只有这两句话,所以我们将函数同步就OK啦:
而set方法同步了,也得把out方法也同步了,因为同步的前提是两个及以上的线程~
现在加入wait和notify:
OK~
下面我们将旧的代码中这一部分去掉,没用啦,Input类的run方法中,直接调用set就好:
Output类的run方法中,直接调用out就好:
主函数中这样写,也简化啦:
编译运行,OK哒:
05-多线程(线程间通信-生产者消费者)
这节课我们继续用上次的例子,做一点小小的修改即可。
在上次的例子中,我们的名字和性别都是固定的,而且没有编号。
这节课,我们打算给每个输入的数据都带上编号,每个输出的也显示输出数据的编号,生产一个,消费一个,生产一个,消费一个。就像生产者和消费者一样,所以这节课我们就来生产产品、消费产品啦。
资源Resource类:
生产者Producer类:
消费者Consumer类:
主函数:
编译运行:
OK的哦。每生产一个,就会消费一个。
我们的生产者和消费者可不止一个呢,再加一个生产者和一个消费者:
编译运行:
我们发现,有的时候生产了一个商品,却被消费了两次。
还有时候会生产两次却消费一次:
为什么会出现这种现象呢?
我们先来分析,生产两次而消费一次是为什么。
假设生产者先获取到了cpu的执行权,而生产者有两个,t1、t2。假设t1获取到了cpu的执行权,这个过程有点小复杂喔:
因为篇幅原因,第(5)步之后我就不再画啦,改为文字陈述,不过理解起来问题应该也不大~
应该能够注意到,因为t1上次是因为判断flag不合格而在原地wait,所以(5)中t1被t3唤醒后,就跳过了判断flag的流程,直接生产(当然此时t3刚刚消费过,flag为false,也是合格的)。到这里还没出现问题。
出现问题的是下一步,t1生产完后,该唤醒下一个线程了,而此时等待唤醒的线程是t2,同样是生产者。和t1一样,它上次也是因为判断flag不合格而在原地wait了,此时它被唤醒后,也跳过了判断flag的流程,一路直下开始生产,但这时就出现问题了喔。t1刚刚生产过,这个产品还没有消费,flag的值也为true,但是因为它跳过了判断flag的步骤,所以造成了消费前的第二次生产。
t2生产完后,t1、t2、t3、t4都有可能获得执行权。假设t1、t2先获得执行权,但因为flag为true,它们终将wait。所以最后获得执行权的会是t3或者t4。此时会消费一次。
这时就发生了生产两次,消费一次的情况。
生产一次,消费两次的情况同理,不再赘述~
我们反思一下生产两次消费一次的错误所在,如果t2被唤醒后,能够再次判断一下flag的值,这个错误就不会发生了。对于t1、t3、t4也是同理,它们都会遇到相同的处境。
那么,怎么才能够让它们每次醒过来都能再判断一次呢?
if是不是只判断一次,而如果换成while,就会判断多次。所以我们将if换成while。
但是编译运行之后,我们会发现,锁死了,程序卡住了:
为什么呢?
这时全都等待了,全冻结了。(不太明白为什么全等待了?当flag为false时不就可以生产吗?)
t1 notify的时候,有t2、t3、t4在等待,t1将t2唤醒了,它将自己方(生产者)的唤醒了,而没有将对方(消费者)唤醒,但是,是不是应该把对方唤醒才靠谱呀。
而notify往往唤醒的是线程池中的第一个,会导致数据错乱,而加上while以后,会导致全部等待。(似乎有点明白刚刚的问题了,假设wait列表中为t1、t2,再假设t1被唤醒,则t1生产完后,会唤醒t2,而此时t2判断flag为true,会再次wait,假设t1再次抢到执行权,依然会判断flag为true,t1也被wait了。想到这里又不明贬了,为什么会被锁死呢?如果t1、t2此时又被wait了,那么t3、t4消费后,按理说它们还是会得到生产的机会呀?难道如果线程的等待列表中存在本方线程,会默认主动唤醒本方线程?这时就会陷入死循环,只有这个解释可以让我理解,不知道想的对不对。)
但是有一个notifyAll,不分本方它方,所有的线程都有机会被唤醒。
编译运行:
检查了一下,发现没有再出现问题喔。
对于多个生产者和消费者,为什么要定义while判断标记?
为了让被唤醒的线程再一次判断标记flag。
为什么定义notifyAll?
因为需要唤醒对方线程。只用notify,容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待。
总结一下:当生产者和消费者出现多个时,判断flag必须用while而不是之前的if,唤醒的时候必须用notifyAll(既唤醒本方,又唤醒对方)而不是notify。
06-多线程(线程间通信-生产者消费者JDK5.0升级版)
上节课我们说用notifyAll,原因是想唤醒对方线程,但是伴随着对方线程被唤醒,本方线程也会同时被唤醒,也跟对方线程抢cpu。
而我们希望的是,只唤醒对方线程,不唤醒本方线程。该怎么去做呢?
后来Java工程师说,我们升级了一下JDK,提供了专有的解决方法。
下面就讲一讲升级后的新特性~
升级后,在工具类中有一个java.util.concurrent.locks包:
这个包中给我们提供了一些常用的接口和类:
注意这里有一个Lock接口,就是锁的意思,它提供了什么东东呢?
看一下:
听它这么描述,意思应该是lock可以替代synchronized耶。
那替代之后,怎么使用lock呢?
我们来看一下它的方法:
之前synchronized加锁和解锁的过程我们都看不到,而用lock之后,这个过程就变显式啦,我们调用lock()来加锁,调用unlock()来解锁。
JDK升级的过程中,JDK1.5的升级绝对是里程碑的升级。早期n多年一直都在用JDK1.4,JDK1.5升级后,把标识号、版本号都给改啦,改成了JDK5.0,再往后就是JDK6.0、JDK7.0。
而这个工具是1.5才有的,1.4的时候都没见着这个呢,所以1.4的程序员多痛苦呀,都在while、notifyAll。
而现在搞成了lock,这就很爽~
那这里所说的,支持多个相关的Condition对象指的是什么呀?
那我们必须要用一下~
我们点击进去Condition接口中看一下:
看完之后,我们知道了,1.5之后,synchronized挂了,被lock替代了,Object、wait、notify、notifyAll方法也挂了,被Condition替代了。
Java可好啦,还给我们提供了示例:
主要关注一下红色框住的地方哦,我们也来这样写一下~
那我们首先是不是应该先搞一个锁呀?
而Lock是一个接口,它下面有很多实现类:
我们用ReentrantLock就好啦。后面什么读锁写锁的我们先不需要用~
那个示例中也有写到喔:
我们也建立一个锁:
对象里new有什么用呢?不用管~我们用的是外面的规则:Lock lock。
wait、notify方法都应该定义在同步语句块当中,同步语句块有锁,而每一个wait、notify都要标识自己所属的锁。而现在同步变成了lock,wait、notify变成了condition,而condition 怎么获取呢?是不是通过锁获取?
而Lock这个接口当中,就定义了一些方法,是不是它可以帮我们建立一个newCondition:
根据锁,建立一个具有wait、notify功能的对象,这个对象叫Condition。
Condition也来啦:
写上拿到锁和释放锁的方法:
我们把之前的同步语句就变成了这两个方法,把拿到锁、释放锁分成两个功能,这样写就更明显啦。
我们继续写~下一步是判断标记flag,如果为true的话,就要等待:
再继续~生产完商品后,将标记flag改为true,此时是不是应该唤醒啦:
注意看一下,我们的程序有个小问题喔。
拿到锁之后,如果在执行被锁的代码时抛出了异常,这个功能是不是就结束了呢?而此时还没有执行到unlock,所以这个锁还被拿着,还没有被释放,这就坏事啦。
所以,unlock这句话一定要执行,我们就要把它写进finally中。
示例中也已经写好啦:
我们也按示例中这样写:
生产者方法set写好啦,消费者方法out也是同理:
生产者类中调用生产者方法:
消费者类中调用消费者方法:
别忘了导入包包哦:
编译运行:
程序挂这儿了。
注意,这个时候又回到了上节课那个问题解决之前,就是线程都等着啦,程序卡死啦。
我们将signal都改成signalAll:
再编译运行,发现一切都OK啦。
但是现在还是会唤醒本方,我们希望它只唤醒对方,不唤醒本方。接下来就显示出新特性出现的好处啦:一个锁上可以有多个相关的Condition对象。
然后在生产者方法set中,就可以让生产者睡眠,后面唤醒消费者,都可以直接指定的哟:
消费者方法out中也是一个道理,让消费者睡眠,唤醒生产者:
condition_pro.await()只能被condition_pro.signal()唤醒,condition_con.await()只能被condition_con.signal()唤醒。
这就是JDK1.5中提供的多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。
将Object中的wait,notify,notifyAll,替换成了Condition对象。
该对象可以通过Lock锁进行获取。
在该实例中,实现了本方只唤醒对方的操作。
以后问生产者消费者有什么替代方案,就说1.5版本以后,它提供了显式的锁机制以及显式的锁对象等待唤醒操作机制。同时,它把等待唤醒机制封装了,封装完,一个锁对应多个condition。(之前一个锁只能对应一个wait/notify)
07-多线程(停止线程)
接下来说一下停止线程。
线程中一般都会写循环,如果不写循环就执行一句话也没有必要开多线程啦,单线程一样能搞定。所以呢,我们玩的就是线程的运行。
但是运行了半天,我们该怎么让线程停下来呢?
之前我们记得有stop方法。
我们去Thread类中找一下:
但是很遗憾,它已经过时了。
既然已经过时了,为什么不清楚掉它呢?因为老的程序中可能还会有。
现在不用它的原因是,这个方法它有一些bug,这个bug是,它是强制性的停止,不管什么时候都会强制停掉线程,这是不OK的。
同样过时的还有suspend方法:
它一挂起会发生死锁。
所以说它们都过时了。
那我们现在该怎样让线程停下来呢?
只有一种,run方法结束。(线程要运行的代码没有了,线程也就结束了。)
该怎么结束run方法呢?
开启多线程运行,运行代码通常是循环结构。
只要控制住循环,就可以让run方法结束,也就是线程结束。
试一下~
主函数中:
编译运行:
(不太懂这个结果)
只要能让循环结束,这个线程就能结束。
但是有一种特殊情况,这种特殊情况下,程序也停不下来:
编译运行:
程序没停下来。但它不是死循环,现在代码并没有消耗资源。
当主线程while(true)的时候,循环一直在转。num++==60后,st.changeFlag();。可是开启两个线程以后,这两个线程无论什么时候抢到cpu执行权,都会在这里面运行:
线程0一进来,拿到锁了,但是try之后就wait了,释放了资格。紧接着线程1进来也wait了,释放了资格。现在它们俩就挂在这里不动了。
主线程执行完了吗?执行完了。
我们在主线程中再加一个over做标记:
主线程也执行完了,现在还有两个线程存活。
这个就是问题。改变了标记,但是没有结束线程。
特殊情况:
当线程出狱了冻结状态,就不会读取到标记,那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结状态进行清除。强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。
Thread类提供该方法,叫interrupt()。
那该怎么解决问题呢?
像这种状况发生之后,我们可以强制解决问题。
在Thread类中给我们提供了一个方法:interrupt()。
点进去看一下:
解释一下哦,中断状态绝对不是停止线程,stop方法才是停止线程。
而中断线程的意思是,当进入到冻结状态时,相当被挂起了,动不了了,中断线程强制清除这个冻结状态,让它恢复到运行状态中来。
#Java小剧场
小楠wait了,有人用notify轻轻拍了一下小楠,小楠就醒了。
小楠sleep了,5分钟后自己醒来了(假设sleep时间设置为5分钟)。
小楠挂过去了,有人用板砖把小楠拍醒了,可是小楠受伤了,发生了受伤异常。
#
我们对t1下手了:
Thread0异常了:
Thread0抛出了中断异常,被catch捕获了。
冻结状态强制被清除,它就发生异常了。异常被catch住并解决了(打印...Exception),然后又回到while(flag),又被wait了。t2还是没有解决。
我们对t2也解决一下:
编译运行:
现在Thread0和Thread1都中砖头了,但是程序还是没有停下来,因为它们又回去等待去了。
但是想一想,能够让那个它们回到运行状态,是不是离结束就不远了?
怎么结束呢?
只要能发生异常,是不是代表着有人在强制清除冻结状态,目的就是想让它结束,所以我们在这里将flag设为flase:
编译运行:
程序结束啦。