线程通信--java进阶day16
1.线程通信
确保线程能够按照我们指定的顺序执行

意义
之前我们玩的线程都是抢占式调度,很有可能一条线程完成了所有任务,就无法体现多线程,所以,使用线程通信让线程按照顺序执行

代码演示
如图,我们写了一个类,里面有两个方法,都是打印语句

接着,我们创建该类的对象作为资源对象,然后实现Runnable接口开启两条线程(共享该资源对象),两条线程分别打印print1和print2

为了保证线程安全,我们再加上同步技术

可以看见,上图中,控制台没有错乱的语句,但如果我们要让传智教育和黑马程序员一人一句的打印,该怎么做?
那就需要用到线程通信
如图,我们先定义一个变量作为标记,来决定该让哪条线程等待,哪条线程执行

在print01的开头就判断,如果flag不等于1,那么就让线程1等待,为1,就执行打印语句,之后再把flag赋值为2,并且唤醒线程2

print02开头也判断,如果flag不等于2,让线程2等待,为2,执行打印语句,之后再把flag赋值为1,唤醒线程1

所以,我们这两条线程该不该执行要根据标记来决定,当标记为1时,线程1执行,线程2等待。标记为2时,线程1等待,线程2执行。
2.等待唤醒机制
要想让线程等待和唤醒,就需要使用到下面的方法

这两个方法来自于Object类,但要使用锁对象进行调用,因为锁对象是任意的,为了确保任意的锁对象都可以调用,等待唤醒方法才在Object中

如图,我们使用锁对象调用等待和唤醒方法


因为我们在print方法里抛出了异常,所以在run方法里也报错了,但是系统提示我们只能try catch

之前说过,子类不能抛出父类没有的或者比父类更大的异常,我们现在在重写run方法,而Runnable接口里是没有异常的,所以我们只能try catch

通过线程通信,控制台打印的结果是一人一句

过程分析
1.假设线程1拿到执行权,走print01,flag初始为1,线程1打印传智教育

2.接着flag改为2,锁对象调用notify唤醒线程,notify支持空唤醒,就算没有等待的线程也只是喊一嗓子
3.执行结束,如果线程1又抢到了执行权,此时flag为2,满足if条件,锁对象调用wait方法,线程1就会进入等待

4.此时只剩线程2拥有执行权,线程2执行,打印黑马程序员,然后flag改为1,锁对象调用notify唤醒线程,线程1被唤醒

5.线程1被唤醒,但并不是直接切换到线程1,有可能线程2继续拥有执行权,
此时flag为1,满足if条件,锁对象调用wait,线程2休眠,只能切换到线程1,以此类推

刚才我们玩了两条线程的等待唤醒,现在我们玩一下三条线程的等待唤醒,如图



使用Runnable开启三条线程,右键运行,控制台并没有按照传智教育-黑马程序员-传智大学的顺序打印,有漏掉的地方

#原因分析
假设线程2先拿到执行权,flag初始为1,线程2等待,剩下线程1和线程3

假设线程3拿到执行权,flag还是1,线程3也等待,只能切换到线程1执行

线程1满足条件,打印语句后,flag改为2,然后唤醒线程,但要注意,notify 是随机唤醒,我们想让它唤醒线程2,但它可能唤醒的是线程3

而wait方法的特点:从哪里等待就从哪里唤醒,线程3是在wait方法这里等待的,唤醒后就从wait方法这里向下执行,就不会再判断if条件

所以,我们的打印顺序错乱,而且还极有可能丢掉打印数据
我们将if换成while,强迫线程进行标记判断,看是否能解决问题

上图,确实保证了一人一句,但是程序卡死了
#原因分析
假设线程2拿到执行权,flag初始为1,线程2等待,

然后线程3拿到执行权,也不满足标记,线程3等待,

只能切换到线程1,执行完逻辑后,flag变为2,notify随机唤醒,线程3被唤醒,

由于我们使用的是while循环,所以每次都会进行判断,flag为2,不满足标记,线程3又等待,

又只剩线程1,而此时flag为2,也不满足线程1的标记,三个线程全部等待,程序卡死
3.notifyAll
使用该方法唤醒所有的线程

使用该方法可以解决上述问题,每一次都可以唤醒所有线程,不存在卡死和随机的问题

但是该方法的弊端也很明显,效率太低了,每次唤醒都是所有线程,然后挨个判断
4.sleep和wait的区别
sleep是线程休眠,时间一到自动醒来,而且不会释放锁。而wait是线程等待,必须由notify才能唤醒,而且在等待期间会释放锁

如果wait不会释放锁,cpu切换不到其他线程,那就没有人去调用notify唤醒它
5.ReentrantLock等待唤醒机制
使用互斥锁调用等待唤醒方法不仅效率高,而且操作也简单

如下图,开启三条线程,可以看见没有使用同步代码块,因为一会要使用互斥锁

接着,在资源类中创建锁对象,进行上锁,三个方法都是一样的操作

然后通过锁对象调用newCondition获取线程状态对象(newCondition要在代码块或方法中才有提示),3条线程对应三个线程状态对象

接着,使用线程状态对象调用等待唤醒方法,开启线程通信,注意等待和唤醒的对象不要搞错!

三个方法的逻辑一样,右键运行,没有问题

当线程状态对象(c1,c2,c3)第一次调用await方法后,线程状态对象会和线程进行绑定,从而达到线程等待和唤醒操作
案例:生产者消费者模式

以吃包子和做包子为例,吃包子的是消费者,做包子的是生产者
当没有包子时,消费者要等待,生产者做包子,然后唤醒消费者吃包子

有包子时,生产者等待,消费者吃包子,然后唤醒生产者做包子

包子则作为标记,初始定为false(没有包子)

将消费者和生产者作为线程类创建,而包子则放在共享区域创建,我们选择互斥锁实现线程通信,所以互斥锁也要放在共享区

为了方便获取,我们将标记和互斥锁加上static修饰,直接类名调用
当标记为true时,走if;当标记为false时,走else

锁对象获取线程状态对象,线程状态对象也创建在共享区域,便于操作。接着进行线程等待和唤醒操作

最后在测试类中创建我们写的两个线程类(实现Runnable丢Thread对象里)

 
                    
                
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号