Java基础day12笔记:线程间通信|线程间通信安全问题|等待唤醒机制|生产者消费者|停止线程

    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:

        编译运行:

        程序结束啦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容