七、线程间通信
线程间通信,主要介绍问题的引出和如何解决等内容。
1、问题的引出
实例:
把一个数据存储空间划分为两个部分:一部分用于存储人的姓名,另一部分用于存储人的性别。这里包含两个线程:一个线程向数据存储空间添加数据(producer),另一个线程从数据存储空间去除数据(consumer)。这个应用会出现两种意外。
第一种意外:假设producer刚向数据存储空间添加了一个人的姓名,还没有加入这个人的性别,cpu就切换到consumer线程,
consumer
则把这个人的姓名和上一个人的性别联系到了一起。
第二种意外:producer放入了若干次数据,
consumer
才开始取数据,或者是,
consumer
取完一个数据后,还没等到producer放入新的数据,又重复取出已去过的数据。
2、问题如何解决
程序中的生产者线程和消费者线程运行的是不同的程序代码,因此编写包含run方法的两个类来完成这两个线程,一个是生产线程Producer,另一个是消费者线程Consumer。
class Producer implements Runnable {
public void run(){
//数据空间存放数据
}
}
class Consumer implements Runnable{
public void run(){
//从数据空间读取数据
}
}
还需要定义一个数据结构来存储数据
class P{
String name;
String sex;
}
范例1:
class P{
String name = "LiSi";
String sex = "Girl";
}
class Producer implements Runnable {
P q = null;
public Producer(P q){
this.q = q;
}
public void run(){
int i = 0;
while(true){
if(i==0){
q.name = "ZhangSan";
q.sex = "Boy";
}else{
q.name = "Lisi";
q.sex = "Girl";
}
i = (i+1)%2;
}
}
}
class Consumer implements Runnable{
P q = null;
public Consumer (P q){
this.q = q;
}
public void run(){
while(true){
System.out.println(q.name+"----->"+q.sex);
}
}
}
public class Test {
public static void main(String args[]){
P q = new P();
Thread pro = new Thread(new Producer(q));
Thread con = new Thread(new Consumer(q));
pro.start();
con.start();
}
}
运行结果片段:
......
Lisi----->Girl
ZhangSan----->Boy
ZhangSan----->Boy
Lisi----->Girl
Lisi----->Boy
ZhangSan----->Girl
ZhangSan----->Boy
ZhangSan----->Girl
ZhangSan----->Boy
Lisi----->Boy
Lisi----->Boy
ZhangSan----->Boy
ZhangSan----->Girl
......
从运行结果来看,打印出现了混乱的情况,这是什么原因?在程序中,Producer类和Consumer类都操纵了同一个P类,这就是有可能出现Producer类还未操纵玩P类,Consumer类就已经将P类中的内容取走了,这就是资源不同步的原因。为此,可在P类中增加两个同步方法:set()和get()。
范例2:
class P{
String name = "LiSi";
String sex = "Girl";
public synchronized void set(String name,String sex){
this.name = name;
this.sex = sex;
}
public synchronized void get(){
System.out.println(this.name+"----->"+this.sex);
}
}
class Producer implements Runnable {
P q = null;
public Producer(P q){
this.q = q;
}
public void run(){
int i = 0;
while(true){
if(i==0){
q.set("ZhangSan","BOy");
}else{
q.set("LiSi","Girl");
}
i = (i+1)%2;
}
}
}
class Consumer implements Runnable{
P q = null;
public Consumer (P q){
this.q = q;
}
public void run(){
while(true){
q.get();
}
}
}
public class Test {
public static void main(String args[]){
P q = new P();
Thread pro = new Thread(new Producer(q));
Thread con = new Thread(new Consumer(q));
pro.start();
con.start();
}
}
运行结果:
........
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
LiSi----->Girl
.......
这个输出结果顺序上没有任何问题,但又有新的问题产生。Consumer线程对Producer线程放入的一次数据连续的读取了多次,这并不符合实际的要求。实际要求的结果是,Producer方一次数据,Consumer就取一次;反之,Producher也必须等到Consumer取完后才能放入新的数据,而这一问题的解决就需要使用下面讲到的线程间的通信。
Java是通过Object类的wait、notify、notifyAll这几个方法来实现线程间的通信的,又因为所有的类都是从Object继承的,所以任何类都可以直接使用这些方法。
wait:告诉当前线程放弃监视器并进入睡眠状态,直到其他线程进入同一监视器并调用notify为止;
nofity:唤醒同一对象监视器中调用wait的第一个线程。这类似于排队买票,一个人买完之后,后面的人才可以继续买;
notifyAll:
唤醒同一对象监视器中调用wait的所有线程,具有最高优先级的线程首先被唤醒并执行。
我们现在将P类修改如下:
class P{
String name = "LiSi";
String sex = "Girl";
boolean bFull = false;
public synchronized void set(String name,String sex){
if(bFull){
try{
wait();
}catch(InterruptedException e){}
}
this.name = name;
try{
Thread.sleep(10);
}catch(Exception e){}
this.sex = sex;
bFull = true;
notify();
}
public synchronized void get(){
if(!bFull){
try{
wait();
}catch(InterruptedException e){}
}
System.out.println(this.name+"----->"+this.sex);
bFull = false;
notify();
}
}
当Consumer线程取走数据后,
bFull值为false,当Producer线程放入数据后,
bFull值为true。只有
bFull为true时,Consumer线程才能取走数据,否则就必须等待Producer线程放入新的数据后的通知;反之,只有
bFull为false,Producer线程才能放入新的数据,否则就必须等待Consumer线程取走数据后的通知。
运行结果如下:
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
ZhangSan----->BOy
LiSi----->Girl
wait、notify、notifyAll这3个方法只能在synchronized方法中调用,即无论线程调用一个对象的wait还是notify方法,该线程必须先得到该对象的锁标记。这样,notify就只能唤醒同一对象监视器中调用wait的线程。而使用多个对象监视器,就可以分别有多个wait、notify的情况,同组里的wait只能被同组的notify唤醒。