黑马程序员——生产者消费者问题之线程间通信

------- android培训java培训、期待与您交流! ----------

本篇博客将通过代码,分析线程通信的原理。线程通信指的是:一个线程存入数据,另一个线程取出数据。当然,实际中可能出现多个线程存入数据,多个线程读取数据。这里只讨论单个线程写入数据,单个线程读取数据。下面以“生产者消费者”问题为例,分析线程间通信的过程。

为了分析线程通信过程中中会遇到的各种问题,这里循序渐进,先分析线程通信、再分析线程同步、最后才分析线程间通信的生产者消费者问题。

1. 线程通信

线程通信指的是:多个线程操作同一个资源,但是操作动作不同。例如一个线程存储入数据,另一个线程去出数据。

1.1线程通信例子——存储与读取不同步问题

下面的例子演示线程通信的过程,有一个类Resource用于产生资源,Input线程用于创建资源,Output出现用于读取Input线程创建的资源。该程序会产生一个问题:我们希望的数据是姓名为"Mike"对应的性别是"man",姓名为"罗林"对应的性别是"女",但是控制台中输出的结果有:Mike——女,罗林——man,这是不安全的。原因是输入数据时,还没有完整输入,输出数据就执行了。也就是说,输入线程还没有执行完run方法时,输出线程就执行run方法读取数据了,所以导致数据不一致的问题。下面一节(1.2)将通过线程同步解决该问题。

package com.itheima.entranceExam.blog;

//资源类
class Resource {
	String name;
	String sex;
}

//输入线程,该线程用于创建Resource对象的数据
class Input implements Runnable{
	private Resource r;
	public Input(Resource r) {
		this.r = r;
	}
	public void run() {
		int x = 0;
		//不停的该Resource对象赋值
		while(true) {
			if(x == 0) {
				r.name = "Mike";
				r.sex = "man";
			}
			else {
				r.name = "罗琳";
				r.sex = "女";
			}
			x = (x+1)%2;
		}
	}
}

//输出线程,该线程用于读取Resource对象的数据
class Output implements Runnable{
	private Resource r;
	public Output(Resource r) {
		this.r = r;
	}
	public void run() {
		//不停地读取Resource对象的成员变量值
		while(true) {
			System.out.println(r.name+"——"+r.sex);
		}
	}
}

public class ThreadsCommunication {
	public static void main(String[] args) {
		Resource r = new Resource();
		Input in = new Input(r);
		Output out = new Output(r);
		//创建两个线程
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		//启动两个线程
		t1.start();
		t2.start();
	}
}

1.2 线程同步解决问题

现在我们通过线程同步,解决上面一节(1.1)中出现的问题。我们知道,上面的例子中,当输入线程执行run方法时,正在循环创建数据:Resource.name="Mike"、Resource.sex="man",Resource.name="罗琳"、Resource.sex="女",在此期间输出线程有可能读取这些数据。我们希望只有当输入线程执行完毕这几行创建数据的代码后,输出线程才能读取数据,所以,需要将这几行创建数据的代码放到同步代码块中,输出线程中的输出数据的代码也需要使用同步代码块。这样,我们就可以使用同步代码块来实现两个线程的线程同步,代码如下(为了简便,这里只显示修改部分的代码,Resource类代码和主函数不显示):

//输入线程,该线程用于创建Resource对象的数据
class Input implements Runnable{
	private Resource r;
	public Input(Resource r) {
		this.r = r;
	}
	public void run() {
		int x = 0;
		//不停的给Resource对象赋值
		while(true) {
			//同步代码块
			synchronized (r) {
				if(x == 0) {
					r.name = "Mike";
					r.sex = "man";
				}
				else {
					r.name = "罗琳";
					r.sex = "女";
				}
				x = (x+1)%2;
			}
		}
	}
}

//输出线程,该线程用于读取Resource对象的数据
class Output implements Runnable{
	private Resource r;
	public Output(Resource r) {
		this.r = r;
	}
	public void run() {
		//不停地读取Resource对象的成员变量值
		while(true) {
			//同步代码块
			synchronized (r) {
				System.out.println(r.name+"——"+r.sex);
			}
		}
	}
}

1.3 线程间通信-等待唤醒机制

1.2节中的例子,还是存在问题:因为当输入线程获得cup执行权后,就不断地输入数据,而输出线程却无法输出这些及时的数据。当输出线程获得cup执行权后,就不断地输出数据,造成输出的数据相同。下面的图片就说明该问题:


为了解决这些问题,本例将通过wait()和notify()方法对线程进行等待和唤醒。当输入线程输入数据后,输入线程等待,并将输出线程唤醒,让输出线程输出数据。当输出线程输出数据后,将输入线程唤醒,让输入线程输入数据。下面的例子通过标记flag来标记输入和输出。当flag=false时,表示执行输入线程,当flag=true时,表示执行输出线程。

代码如下(为了简便,这里不显示主函数,主函数代码请看1.1节中的内容):

package com.itheima.entranceExam.blog;

//资源类
class Resource {
	String name;
	String sex;
	//标记:当为false时执行输入线程,当为true时执行输出线程
	boolean flag = false;
}

//输入线程,该线程用于创建Resource对象的数据
class Input implements Runnable{
	private Resource r;
	public Input(Resource r) {
		this.r = r;
	}
	public void run() {
		int x = 0;
		//不停的给Resource对象赋值
		while(true) {
			//同步代码块
			synchronized (r) {
				//第一次时,flag为false,所以执行输入线程创建Resource对象的数据
				//之后输入线程和输出线程交替执行,输入线程执行完创建数据后,输出线程马上执行输出数据
				if(r.flag)
					try {
						r.wait();//线程等待
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				if(x == 0) {
					r.name = "Mike";
					r.sex = "man";
				}
				else {
					r.name = "罗琳";
					r.sex = "女";
				}
				x = (x+1)%2;
				//改变标记,让输出线程执行
				r.flag = true;
				r.notify();//唤醒线程
			}
		}
	}
}

//输出线程,该线程用于读取Resource对象的数据
class Output implements Runnable{
	private Resource r;
	public Output(Resource r) {
		this.r = r;
	}
	public void run() {
		//不停地读取Resource对象的成员变量值
		while(true) {
			//同步代码块
			synchronized (r) {
				if(!r.flag)
					try {
						r.wait();//线程等待
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				System.out.println(r.name+"——"+r.sex);
				//改变标记,让输入线程执行
				r.flag = false;
				r.notify();//线程唤醒
			}
		}
	}
}

1.4 优化代码

下面我们优化1.3节中的代码,使其更简单。将1.3节中的同步代码块改写为同步函数,我们不能将run方法改写为同步函数,因为run方法是Runnable类的方法,继承该类的线程类只能重载run方法中的内容,而不能重写。所以,我们可以将输入线程中run方法中的,设置Resource类对象成员变量值的代码,抽取出来放到Resource类的set方法中,并将set方法定义为同步函数。同理,将输出线程中的输出数据的代码抽取出来,放到Resource类的out方法中,并将out方法定义为同步函数。这样,我们就将1.3节中的代码优化了,如下:

package com.itheima.entranceExam.blog;

//资源类
class Resource {
	private String name;
	private String sex;
	private boolean flag = false;
	
	//同步函数:设置Resource类成员变量的值
	public synchronized void set (String name, String sex) {
		if(flag)
			try {
				this.wait();//线程等待
			} catch (Exception e) {}
		this.name = name;
		this.sex = sex;
		flag = true;
		this.notify();//唤醒线程
	}
	
	//同步函数:输出Resource类成员变量的值
	public synchronized void out() {
		if(!flag) {
			try {
				this.wait();//线程等待
			} catch (Exception e) {}
		}
		System.out.println(name+"——"+sex);
		flag = false;
		this.notify();//唤醒线程
	}
}

//输入线程
class Input implements Runnable{
	private Resource r;
	public Input(Resource r) {
		this.r = r;
	}
	public void run() {
		int x = 0;
		while(true) {
			if(x == 0)
				r.set("Mike", "man");
			else 
				r.set("罗琳", "女");
			x = (x+1)%2;
		}
	}
}

//输出线程
class Output implements Runnable{
	private Resource r;
	public Output(Resource r) {
		this.r = r;
	}
	public void run() {
		while(true) {
			r.out();
		}
	}
}

2. 线程通信实现生产者消费者问题

通过上面的第1节的学习,我们初步掌握了线程通信的机制,以及如何实现线程同步。现在我们就应用这些知识点来实现“生产者消费者”的问题。

生产者消费者问题分析:生产者是输入线程,消费者是输出线程。当生产者生产商品时(也就是输入线程创建数据),消费者等待。当生产者生产完毕一件商品后,生产者线程等待,消费者线程执行并消费该商品(输出商品)。就只要,生产者和消费者交替执行,只有生产者生产一件视商品,消费者马上消费该商品。为了便于说明,我们该每个商品都编号,每个商品只对应唯一的一个编号,消费者只能消费一次某一个编号的商品,代码如下:

package com.itheima.entranceExam.blog;

//资源类
class Resource {
	private String name;
	//计数器,用来给商品编号
	private int count = 1;
	private boolean flag = false;
	
	//同步函数:设置Resource对象的成员变量数据
	public synchronized void set(String name) {
		if(flag)
			try {
				this.wait();//线程等待
			} catch (Exception e) {}
		this.name = name+":"+count++;
		//Thread.currentThread().getName()表示获取当前线程的名称,下同
		System.out.println(Thread.currentThread().getName()+"  生产者  "+this.name);
		flag = true;
		this.notify();//唤醒线程
	}
	
	//同步函数:输出Resource类对象的成员变量数据
	public synchronized void out() {
		if(!flag)
			try {
				this.wait();//线程等待
			} catch (Exception e) {}
		System.out.println(Thread.currentThread().getName()+"  消费者  "+this.name);
		flag = false;
		this.notify();//唤醒线程
	}
}

//生产者
class Producer implements Runnable {
	private Resource res;
	
	public Producer(Resource res) {
		this.res = res;
	}
	
	public void run() {
		while(true) {
			res.set("商品");
		}
	}
}

//消费者
class Consumer implements Runnable {
	private Resource res;
	
	public Consumer(Resource res) {
		this.res = res;
	}
	
	public void run() {
		while(true) {
			res.out();
		}
	}
}

public class ProducerConsumer {
	public static void main(String[] args) {
		Resource r = new Resource();
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(con);
		t1.start();
		t2.start();
	}
}







  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值