一、什么是线程通信
不同种类的线程(消费类、生产类)在处理同一个资源时,需要线程之间进行配合, 避免对同一共享变量的争夺,来实现线程之间对同一个变量的使用或操作。
(可以简单理解为:不同种类的线程针对统一资源的操作)
于是,Java引出了等待唤醒机制(也称为线程间的通信):就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify)。
二、为什么需要线程通信
CPU的执行权是线程间随机抢占的,因此多个线程并发执行时, 当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行时,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
三、如何实现线程通信
Java为我们提供了以下方法来实现线程间的通信:
1 | void wait() |
线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。
注意:要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。
1 | void notify() |
notify()方法会唤醒在此对象监视器上等待的单个线程,即:会唤醒一个等待当前对象的锁的线程。
1 | void notifyAll() |
notifyAll()方法会唤醒在此对象监视器上等待的所有线程。
补充:细心的小伙伴可能已经发现了,这三个方法不就是Object里面的方法么。可是线程相关的方法不更应该在Thread类中吗?
怎么会定义在Object类中呢?
其实这是因为,所有的这些方法在使用时都是必须通过锁对象来调用的,但是我们在使用同步代码块时,是可以使用任意锁对象的。因此,只有将这些方法定义在Object类中才能满足方法呀!
四、实现线程通信的案例
我们可能都听说过,生产者-消费者模式是最好体现线程通信的例子。因此,接下来就让我们敲一个关于生产者和消费者的简单的小demo,来演示一下线程的通信:
(一)生产与消费面包(单生产、单消费)
资源类(面包类)
1 | public class Breads { |
注意:
如果不是使用同步方法,而是使用同步代码块时,加的锁一定要满足以下两点要求:
1)不同种类的线程都要加锁
2)不同种类的线程加的锁必须是同一把
加锁的目的是锁住共享资源,而不是锁住操作共享数据的代码块。
因为,锁住资源之后,只要有一个线程(不管是否为同一个类型)在操作数据,其他的线程就不能操作,从而实现线程安全。
生产者类
1 | public class producer extends Thread{ |
消费者类
1 | public class consume extends Thread{ |
测试类
1 | public class TestBreads { |
测试结果
1 | 生产者生产了一个编号为1的面包! |
结果分析
如上所示,案例表明生产者消费者之间就是运用了wait()和notify()这两个方法,通过线程的等待与唤醒机制来完成两个不同线程操作统一数据之间的通信。
当生产者生产出一个面包时,立即去唤醒消费者,就像是对他说,你去买吧,我生产好了,然后消费者就会屁颠屁颠的去买了那个面包,当他吃完那个面包后,立即唤醒生产者,就像是对他说,你再生产一个吧,我吃完了,然后生产者就… … 如此循环,周而复始,直到for循环结束为止。
当然,也有可能生产者生产一个之后,它通知了消费者,但是生产者再次抢到执行权,想要继续生产。于是它查看面包的数量,却发现还是1,也就是面包还没有被消费,此时生产者就只能等待消费者消费完之后再生产。
同理,当消费者消费完一个面包之后,它通知了生产者,但是消费者再次抢到执行权,想要继续消费。于是它查看面包的数量,却发现还是0,也就是面包还没有被生产,此时消费者就只能等待生产者生产完之后在消费。
(二)生产与消费面包(多生产、多消费)
当我们创建多个生产者和消费者时,上述的代码就会出现一个问题,就是他们无法直到到底要唤醒哪一个,所以这时候我们就用到了notifAll()方法。
篇幅原因,代码就不在重复放出。其实与上面的差不多,唯一不一样的就是在面包类里的notify()换成notifAll()。
以下是多个生产者和消费者的测试类:
1 | public class TestBreads { |
测试结果如下:
1 | 生产者1生产了一个编号为1的面包! |
如上所示,我们创建了三个生产者和三个消费者;
当进程开始后,会随机开启一个生产者线程,并在生产一个面包后,通知消费者,随后会唤醒所有消费者线程,但是只有一个消费者线程能抢到执行权,接着该消费掉刚刚生产的那个面包,然后通知生产者,随后会唤醒所有生产者线程,但是只有一个生产者线程能抢到执行权… 周而复始,直到结束。
五、状态转换图
可能有小伙伴会像我一样比较好奇,调用了wait()和notify()/notifyAll()方法后,线程的状态都会产生这样的变化。
因此,贴出一张线程的状态转换图:
六、总结
其实通过消费者-生产者模式来理解线程间的通信还是很简单的。
所以,好好弄懂这个案例吧!
本文大部分内容参考自线程之间的通信,并对部分内容进行了修改。
Java新手,若有错误,欢迎指正!