多线程间的通信其实就是多个线程都在处理同一个资源,但是处理的任务却不一样。比较经典的案例是生产者与消费者的问题了,下面通过代码进行演示。
说明:通过厨师长做菜,服务员端菜的例子模拟生产者与消费者模型,同时我们要求厨师长做菜与服务员端菜交替进行,即厨师长每做完一道菜,服务员就端一道菜。
public class Demo {
public static void main(String[] args) {
Food f = new Food();
//厨师长进程
new Thread(new Cook(f)).start();
//服务员进程
new Thread(new Waiter(f)).start();
}
//厨师长
static class Cook implements Runnable {
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
f.setNameAndTaste("小白生煎","甜口");
} else {
f.setNameAndTaste("煎饼果子","香辣");
}
}
}
}
//服务生
static class Waiter implements Runnable {
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i=0; i<100; i++) {
//控制输出间隔时间,同时让出错更明显
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food {
private String name;
private String taste;
public void setNameAndTaste(String name,String taste) {
this.name = name;
//控制输出间隔时间,同时让出错更明显
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
System.out.println("厨师长做菜:"+this.name+","+this.taste);
}
public void get() {
System.out.println("服务生端菜:"+this.name+","+this.taste);
}
}
}
运行结果:
能看出代码的几个问题:
- 存在乱序的情况,厨师和服务员的行为并不是交替进行的;
- 厨师连做两碗菜,服务员连端两碗菜;
- 菜的味道“变了”,小白生煎设置是甜口的,煎饼果子设置是香辣的,更离谱的是最开始的菜的味道值可能为null。
结果分析:
对于 问题1 和 问题2,我们知道 线程抢占资源的概率是相同的(不设置优先级的情况下),因此厨师长做菜和服务员端菜的顺序在上面的代码里完全控制不了;
对于 问题3 我们试想这种情况:厨师抢到资源刚设置菜的名字,还没来得及设置菜的味道(代码中通过sleep()实现),服务员就把菜端走了,而此时的菜的味道还是上一碗菜的味道,因此会出现菜的味道值为null的情况。
解决问题3,我们使用同步机制,把Food类中的setNameAndTaste()方法和get()方法设置成同步函数。同步方法使得厨师长进程在调用setNameAndTaste()时,服务员进程呈阻塞状态,不会执行get()方法。同理,当get()方法被调用时,setNameAndTaste()方法不会被调用。修改的代码如下:
public synchronized void setNameAndTaste(String name,String taste) {
this.name = name;
//控制输出间隔时间,同时让出错更明显
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
System.out.println("厨师长做菜:"+this.name+","+this.taste);
}
public synchronized void get() {
System.out.println("服务生端菜:"+this.name+","+this.taste);
}
解决问题1、2前,我们先搞清楚问题出在哪:当厨师长线程调用setNameAndTaste()方法后,一个“回首掏”,厨师长线程又抢到资源,接着调用setNameAndTaste()方法,相同的事情也会发生在服务员线程身上,因此会出现厨师长连续做几次菜,服务员连续端几次菜的打印。
根据问题描述我们假设这样一种场景:厨师长线程调用完set()方法后,让它“暂停一下”,并“通知”服务员线程,可以调用get()方法了;当服务员线程调用完get()方法后,同样“暂停一下”,并“通知”厨师长线程,可以调用set()方法了。如果两线程这样交替执行,就可以达到预期的结果。
Java为我们提供了这样一种实现多线程通信的机制——等待唤醒机制
常用方法(所属类:Object类):
- void notify() 唤醒正在此对象监视器上等待的单个线程。
- void notifyAll() 唤醒等待此对象监视器的所有线程。
- void wait() 导致当前线程等待它被唤醒,通常是 通知 或 中断 。
注意:
- 在线程中调用上述方法时要用synchronized锁住对象,确保代码段不会被多个线程调用。
- 如果没有synchronized加锁,那么当前的线程不是此对象监视器的所有者, 就会抛出 IllegalMonitorStateException 异常信息。
最后的代码如下:
public class Demo {
public static void main(String[] args) {
Food f = new Food();
//厨师长进程
new Thread(new Cook(f)).start();
//服务员进程
new Thread(new Waiter(f)).start();
}
//厨师长
static class Cook implements Runnable {
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
f.setNameAndTaste("小白生煎","甜口");
} else {
f.setNameAndTaste("煎饼果子","香辣");
}
}
}
}
//服务员
static class Waiter implements Runnable {
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++)
f.get();
}
}
//食物
static class Food {
private String name;
private String taste;
//标记,true表示厨师炒菜,false表示服务员上菜
private boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste) {
if (flag) {
this.name = name;
this.taste = taste;
System.out.println("厨师长做菜:"+this.name+","+this.taste);
flag = false;
this.notify(); //这里用notify()和notifyAll()都可以,注意在多个生产者和消费者的案例中notify()会引起异常!!!下同
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get() {
if (!flag) {
System.out.println("服务生端菜:"+this.name+","+this.taste);
flag = true;
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果: