原标题:Java中的线程的等待和唤醒(线程间通信)--生产消费模型
为了更好的帮助大家便利的学习java这门编程语言,和更好的巩固java语言学习中的基础知识,我们特意为大家精心制作了java程序设计精编教程。
本教程精选java核心内容,结合实例,循序渐进的向大家介绍Java语言。以零基础讲解为基础,用实例引导大家学习,深入浅出的向大家介绍java的相关知识和实战技能。
Java入门(41)
实际生活中,需要操作共享的某个资源(水池),但是对这个共享资源的操作方式不同(部分是注水、部分是抽水)。把这种现象我们可以称为生产和消费模型。
生产:它可以采用部分线程进行模拟。多个线程同时给水池中注水。
消费:它可以采用部分线程进行模拟。多个线程同时从水池中抽水。
对资源的不同的操作方式,每种方式都可以让部分的线程去负责。多个不同的线程,他们对相同的资源(超市、水池等)操作方式不一致。
这个时候我们不能使用一个run方法对线程的任务进行封装。所以这里就需要定义不同的线程任务类,描述不同的线程的任务。
简单的实现生产消费模型
package com.click369.test1;
/**
* 被多个线程操作的共享数据的资源类
*
* @author Administrator
*
*/
public class Resource {
// 注水的方法
public void add {
}
// 抽水的方法
public void delete {
}
}
package com.click369.test1;
/**
* 生产者类【注水】
* @author Administrator
*
*/
public class Productor implements Runnable{
private Resource r;
public Productor(Resource r){
this.r=r;
}
@Override
public void run {
//调用注水的方法
r.add;
}
}
package com.click369.test1;
/**
* 消费者类【抽水】
* @author Administrator
*
*/
public class Consumer implements Runnable{
private Resource r;
public Consumer(Resource r){
this.r=r;
}
@Override
public void run {
//调用抽水的方法
r.delete;
}
}
package com.click369.test1;
public class TestMain {
public static void main(String[] args) {
//创建资源对象
Resource r=new Resource;
//创建生产者的目标对象
Productor protaget=new Productor(r);
//创建消费者的目标对象
Consumer contaget=new Consumer(r);
//创建生产者的线程对象
Thread prothread=new Thread(protaget);
//创建消费者的线程对象
Thread conthread=new Thread(contaget);
//开启注水线程【生产者线程】
prothread.start;
//开启抽水线程【消费者线程】
conthread.start;
}
}
资源类基本实现,修改Resource为add/delete添加具体实现动作。
package com.click369.test1;
/**
* 被多个线程操作的共享数据的资源类
*
* @author Administrator
*
*/
public class Resource {
//数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
private Object[] objs = new Object[1];
// 定义一个变量,充当计数器
private int num = 1;
// 注水的方法
public void add {
objs[0] = "水" + num;
System.out.println(Thread.currentThread.getName + "正要注进入的水是:" + objs[0]);
num++;
}
// 抽水的方法
public void delete {
System.out.println(Thread.currentThread.getName + "抽出的水是:" + objs[0]);
objs[0] = null;
}
}
修改Productor为注水方法添加死循环,以达到持续注水的目标。
package com.click369.test1;
/**
* 生产者类【注水】
* @author Administrator
*
*/
public class Productor implements Runnable{
private Resource r;
public Productor(Resource r){
this.r=r;
}
@Override
public void run {
//持续注水
while(true){
//调用注水的方法
r.add;
}
}
}
修改Consumer为抽水方法添加死循环,以达到持续抽水的目标。
package com.click369.test1;
/**
* 消费者类【抽水】
* @author Administrator
*
*/
public class Consumer implements Runnable{
private Resource r;
public Consumer(Resource r){
this.r=r;
}
@Override
public void run {
//持续抽水
while(true){
//调用抽水的方法
r.delete;
}
}
}
此时可能会出现生产者注水为null的结果:
有两个线程生产者负责注水、消费者负责抽水。假设CPU在消费者线程上,那么消费者正要打印了抽水为null的情况下,还没有将数组空间赋值为null之前,CPU切换到生产者,生产者将水注到数组空间中之后,还没有打印,CPU又切回到消费者线程上,消费者线程就会将数组空间立刻赋值为null。CPU如果再切回到生产者线程上,打印出来的注水就是null。
上面的问题就是线程操作共享数据,需要进行同步。
同步问题:保证注水的时候不能抽水,或者抽水的时候不能给当前这个空间注水。
修改Resource为注水和抽水方法添加同步代码快保证注水的时候不能抽水,或者抽水的时候不能给当前这个空间注水。
package com.click369.test1;
/**
* 被多个线程操作的共享数据的资源类
*
* @author Administrator
*
*/
public class Resource {
//数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
private Object[] objs = new Object[1];
// 定义一个变量,充当计数器
private int num = 1;
// 创建一个对象,作为同步的锁
private static final Object loc = new Object;
// 注水的方法
public void add {
synchronized( loc ){
objs[0] = "水" + num;
System.out.println(Thread.currentThread.getName + "正要注进入的水是:" + objs[0]);
num++;
}
}
// 抽水的方法
public void delete {
synchronized( loc ){
System.out.println(Thread.currentThread.getName + "抽出的水是:" + objs[0]);
objs[0] = null;
}
}
}
上面执行完成以后会出现多次注水没有抽水,或者多次抽水,没有注水的问题?
要解决上面这个多次操作的问题,首先需要先判断是否满足抽水或者注水的条件。
添加一些判断,当满足注水线程操作的时候,我们才让这个注水线程去操作,如果不满足那么注水的线程就不能操作。如果满足抽水的条件,才能抽水,否则也不能抽水。
什么时候抽水:当数组空间中不是null的时候可以进行抽水。
什么时候注水:数组空间为null的时候才能注水。
如果不满足注水的时候,但是当前正好CPU在注水的线程上,这时就必须让这个注水的线程等待,等到可以注水的时候将本次注水的动作做完。
如果不满足抽水的时候,但是当前正好CPU在抽水的线程上,必须让抽水的线程等待,等到数组有水的时候将本次的抽水的动作做完。
需要使用Java中线程的等待和唤醒机制(线程间的通信):
等待:如果判断发现不满足,这个线程就要等待。等待到满足操作的时候,才能继续进行执行。
举例:注水或抽水进行分析:
注水:当前空间中有水,就需要等待,没水就可以注水。
抽水:当前空间中没水,就需要等待,有水就可以抽水。
注水线程注水结束之后,应该告诉抽水线程可以抽水。同样道理,抽水线程抽完水之后,应该告诉注水线程可以注水了。
两个线程之间就进行通信了。
唤醒:当某个一方操作完成之后,需要将处于另外一方操作的等待的线程等待的状态恢复到可以操作的状态(把一方通知另外一方的这个操作称为线程的唤醒)。
例如:注水的线程注水结束,可以唤醒抽水的线程。或者抽水的线程抽完之后,唤醒注水的线程。
在Java提供两个不同的方法分别代表等待和唤醒:
等待和唤醒的方法没有定义在Thread类中,而是定义在Object类中(因为只有同步的锁才能让线程等待或者将等待的线程唤醒,而同步的锁是任意对象,等待和唤醒的方法只能定义在Object类中)
void wait 在其他线程调用此对象的 notify 方法或 notifyAll 方法前,导致当前线程等待
void notify 唤醒在此对象监视器上等待的单个线程。
void notifyAll 唤醒在此对象监视器上等待的所有线程。
注意:等待和唤醒(线程通信)必须位于同步中。因为等待和唤醒必须使用当前的锁才能完成。
package com.click369.test1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 被多个线程操作的共享数据的资源类
*
* @author Administrator
*
*/
public class Resource {
//数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
private Object[] objs = new Object[1];
// 创建一个对象,作为同步的锁
private static final Object loc = new Object;
// 定义一个变量,充当计数器
private int num = 1;
// 注水的方法
public void add throws InterruptedException{
// t-0
synchronized( loc ){
// 进入同步之后必须先判断
if( objs[0] != null ){
// 判断成立说明数组中有水的,注水的线程需要等待
loc.wait;
}
objs[0] = "水" + num;
System.out.println(Thread.currentThread.getName + "正要注进入的水是:" + objs[0]);
num++;
// 上面的三行代码执行完,就说明注水结束了,可以抽水了,需要唤醒抽水的线程
loc.notify;
}
}
// 抽水的方法
public void delete throws InterruptedException{
// t-1
synchronized( loc ){
// 进入同步之后必须先判断
if( objs[0] == null ){
// 判断成立说明数组中没水的,抽水的线程需要等待
loc.wait;
}
System.out.println(Thread.currentThread.getName + "抽出的水是:" + objs[0]);
objs[0] = null;
// 抽水线程执行结束了,需要唤醒的注水的线程
loc.notify;
}
}
}
修改为多个注水和抽水
package com.click369.test1;
public class TestMain {
public static void main(String[] args) {
//创建资源对象
Resource r=new Resource;
//创建生产者的目标对象
Productor protaget=new Productor(r);
//创建消费者的目标对象
Consumer contaget=new Consumer(r);
//创建生产者的线程对象
Thread prothread1=new Thread(protaget);
Thread prothread2=new Thread(protaget);
//创建消费者的线程对象
Thread conthread1=new Thread(contaget);
Thread conthread2=new Thread(contaget);
prothread1.setName("生产者1");
prothread2.setName("生产者2");
conthread1.setName("消费者1");
conthread2.setName("消费者2");
//开启注水线程【生产者线程】
prothread1.start;
prothread2.start;
//开启抽水线程【消费者线程】
conthread1.start;
conthread2.start;
}
}
将单注水和单抽水修改为两个注水和两个抽水,结果程序中又出现了多次注水,或者多次抽水的现象吧。
发生这个原因:是因为在唤醒的时候,抽水的线程将另外一个抽水的线程唤醒了。或者注水的线程将另外一个注水的线程唤醒了。只要自己同伴线程将自己唤醒之后,这时被唤醒的线程就可以继续操作。导致问题发生。
解决上面的问题:将判断有没有水的if修改为while即可。唤醒之后可以继续判断。
package com.click369.test1;
/**
* 被多个线程操作的共享数据的资源类
*
* @author Administrator
*
*/
public class Resource {
//数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
private Object[] objs = new Object[1];
// 定义一个变量,充当计数器
private int num = 1;
// 创建一个对象,作为同步的锁
private static final Object loc = new Object;
// 注水的方法
public void addthrows InterruptedException{
synchronized( loc ){
// 进入同步之后必须先判断
while( objs[0] != null ){
// 判断成立说明数组中有水的,注水的线程需要等待
loc.wait;
}
objs[0] = "水" + num;
System.out.println(Thread.currentThread.getName + "正要注进入的水是:" + objs[0]);
num++;
// 上面的三行代码执行完,就说明注水结束了,可以抽水了,需要唤醒抽水的线程
loc.notify;
}
}
// 抽水的方法
public void deletethrows InterruptedException {
synchronized( loc ){
// 进入同步之后必须先判断
while( objs[0] == null ){
// 判断成立说明数组中没水的,抽水的线程需要等待
loc.wait;
}
System.out.println(Thread.currentThread.getName + "抽出的水是:" + objs[0]);
objs[0] = null;
// 抽水线程执行结束了,需要唤醒的注水的线程
loc.notify;
}
}
}
修改为while之后,程序又出现了新的问题:死锁(所有的线程都处于等待状态了。外面没有可以执行的线程了)。
线程唤醒的时候,注水的线程将注水的另外一个线程唤醒之后,判断完成直接等待。或者抽水的线程唤醒另外一个抽水的线程,唤醒之后也需要等待。
这样就导致会出现所有线程全部等待,而没有存活的可以执行的线程,程序就卡主不执行。
解决方案:只能使用notifyAll唤醒所有线程。每次在唤醒的时候都是唤醒所有线程,即使唤醒了自己的同伴,也无所谓,因为还要继续判断,这样一定还会等待,但是唤醒所有线程中一定有另外一方的线程,它们肯定不会等待。它们不等待,就会去操作,它们操作完成也唤醒所有。
上面的问题的解决方案:将notify换成notifyAll方法。
package com.click369.test1;
/**
* 被多个线程操作的共享数据的资源类
*
* @author Administrator
*
*/
public class Resource {
//数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
private Object[] objs = new Object[1];
// 定义一个变量,充当计数器
private int num = 1;
// 创建一个对象,作为同步的锁
private static final Object loc = new Object;
// 注水的方法
public void addthrows InterruptedException{
synchronized( loc ){
// 进入同步之后必须先判断
while( objs[0] != null ){
// 判断成立说明数组中有水的,注水的线程需要等待
loc.wait;
}
objs[0] = "水" + num;
System.out.println(Thread.currentThread.getName + "正要注进入的水是:" + objs[0]);
num++;
// 上面的三行代码执行完,就说明注水结束了,可以抽水了,需要唤醒抽水的线程
loc.notifyAll;
}
}
// 抽水的方法
public void deletethrows InterruptedException {
synchronized( loc ){
// 进入同步之后必须先判断
while( objs[0] == null ){
// 判断成立说明数组中没水的,抽水的线程需要等待
loc.wait;
}
System.out.println(Thread.currentThread.getName + "抽出的水是:" + objs[0]);
objs[0] = null;
// 抽水线程执行结束了,需要唤醒的注水的线程
loc.notifyAll;
}
}
}
JDK5中的Condition接口
多生产多消费的程序中,为了保证不出现全部线程被wait的情况,只能在唤醒的时候使用notifyAll将所有处于等待的线程唤醒。这样每次都可以保证一定会有存活的线程。但是这种唤醒效率太低了,经常会发生生产方唤醒自己的同伴线程,或者是消费方唤醒自己的同伴线程。
在JDK5中提供Condition接口。它用来代替等待和唤醒机制。
java.util.concurrent.locks接口 Condition
public interface Condition
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
在JDK5之前,一个同步的锁下面的等待和唤醒无法辨别当前让等待或唤醒的线程到底属于生产还是属于消费。而Condition接口,它可以创建出不同的等待和唤醒的对象,然后可以用在不同的场景下:
可以创建一个Condition对象,专门负责生产。
可以创建一个Condition对象,专门负责消费。
可以通过负责生产的Condition对象专门监视负责生产的线程。通过负责消费的Condition监视消费的线程。等待和唤醒的时候,可以使用各自的Condition对象。
void await造成当前线程在接到信号或被中断之前一直处于等待状态。
void signal唤醒一个等待线程。
void signalAll 唤醒所有等待线程。
注意:如果要想使用Condition接口,同步必须使用Lock接口。
如果程序中同步使用的同步代码块,等待和唤醒只能使用Object中的wait、notify、notifyAll方法。
只有同步使用的Lock接口,等待和唤醒才能使用Condition接口。
package com.click369.test1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 被多个线程操作的共享数据的资源类
*
* @author Administrator
*
*/
public class Resource {
//数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
private Object[] objs = new Object[1];
// 定义一个变量,充当计数器
private int num = 1;
// 创建一个对象,作为同步的锁
//private static final Object loc = new Object;
// 创建Lock接口,作为同步的锁
private Lock lock = new ReentrantLock;
// 负责监视注水的线程
private Condition proCon = lock.newCondition;
// 负责监视抽水的线程
private Condition conCon = lock.newCondition;
// 注水的方法
public void add{
try{
//获取锁
lock.lock;
// 进入同步之后必须先判断
while( objs[0] != null ){
// 判断成立说明数组中有水的,注水的线程需要等待
proCon.await;
}
objs[0] = "水" + num;
System.out.println(Thread.currentThread.getName + "正要注进入的水是:" + objs[0]);
num++;
// 上面的三行代码执行完,就说明注水结束了,可以抽水了,需要唤醒抽水的线程
//loc.notify;
//loc.notifyAll;
conCon.signal;
}catch(Exception e){
e.printStackTrace;
}finally {
//释放锁
lock.unlock;
}
}
// 抽水的方法
public void delete{
try{
//获取锁
lock.lock;
// 进入同步之后必须先判断
while( objs[0] == null ){
// 判断成立说明数组中没水的,抽水的线程需要等待
conCon.await;
}
System.out.println(Thread.currentThread.getName + "抽出的水是:" + objs[0]);
objs[0] = null;
// 抽水线程执行结束了,需要唤醒的注水的线程
//loc.notify;
//loc.notifyAll;
proCon.signal;
}catch(Exception e){
e.printStackTrace;
}finally {
//释放锁
lock.unlock;
}
}
责任编辑: