实际生活中,需要操作共享的某个资源(水池),但是对这个共享资源的操作方式不同(部分是注水[生产]、部分是抽水[消费])。把这种现象我们可以称为生产和消费模型。
生产:它可以采用部分线程进行模拟。多个线程同时给水池中注水。
消费:它可以采用部分线程进行模拟。多个线程同时从水池中抽水。
对资源的不同的操作方式,每种方式都可以让部分的线程去负责。多个不同的线程,他们对相同的资源(超市、水池等)操作方式不一致。
这个时候我们不能使用一个run方法对线程的任务进行封装。所以这里就需要定义不同的线程任务类,描述不同的线程的任务。
通过不同的线程操作,来控制同一个资源,这种现象就属于生产消费模型
例:
1.创建公共资源类
package com.weiwei.test;
/**
* 公共资源类
*
*/
public class Resource {
/**
* 生产资源方法
*/
public void add(){
}
/**
* 消费资源方法
*/
public void delete(){
}
}
2.创建生产任务类和消费任务类
/**
* 生产任务类线程
*
*/
public class ShengChanThread implements Runnable{
//定义被控制的资源
private Resource resource=null;
//构造方法初始化
public ShengChanThread(Resource resource){
this.resource= resource;
}
public void run(){
for(int i=1;i<=20;i++){
resource.add();
}
}
}
/**
* 消费任务类线程
*
*/
public class XiaoFeiThread implements Runnable{
//定义被控制的资源
private Resource resource=null;
//构造方法初始化
public XiaoFeiThread(Resource resource){
this.resource= resource;
}
public void run(){
for(int i=1;i<=20;i++){
resource.delete();
}
}
}
3.给资源类添加方法
/**
* 公共资源类
*
*/
public class Resource {
//使用一个对象数组充当水池
private Object obj[]=new Object[1];//生产一个消费后,才能再生产
//记录生产和消费的次数
private int num=1;
/**
* 生产资源方法
*/
public void add(){
obj[0]="水资源=="+num;
System.out.println(Thread.currentThread().getName()+"生产的资源是:"+obj[0]);
num++;
}
/**
* 消费资源方法
*/
public void delete(){
System.out.println(Thread.currentThread().getName()+"消费的资源是:"+obj[0]);
obj[0]=null;
}
}
测试类:
public class Main1 {
public static void main(String[] args) {
Resource r = new Resource();
ShengChanThread sc = new ShengChanThread(r);
Thread scth1 = new Thread(sc);
//Thread scth2 = new Thread(sc);
scth1.setName("生产者1");
scth1.start();
XiaoFeiThread xf = new XiaoFeiThread(r);
Thread xfth1 = new Thread(xf);
//Thread xfth2 = new Thread(xf);
xfth1.setName("消费者1");
xfth1.start();
}
}
有时会出现消费者消费为null情况:
以上图为例,过程是生产线程执行到4的时候,切换给消费线程,还没来得急打印输出“消费者生产的资源是”,线程又切换给了生产线程,执行到6,切换到消费线程,继续执行没执行完的那句话,输出消费资源4,还没执行对数组赋值null,又切换给了生产线程执行生产7,然后切换回消费线程,执行赋值null,然后继续执行消费线程,就会输出消费资源为null。
有时会出现生产者生产为null情况:
假设CPU在消费者线程上,那么消费者正要打印了消费为null的情况下,还没有将数组空间赋值为null之前,CPU切换到生产者,生产者将水注到数组空间中之后,还没有打印,CPU又切回到消费者线程上,消费者线程就会将数组空间立刻赋值为null。CPU如果再切回到生产者线程上,打印出来的消费就是null。
上面的这两个问题就是因为当前线程正在访问的共享资源的时候,其他的线程也可以访问共享资源所产生的。所以线程操作共享数据时,需要进行线程同步。
线程同步能够保证生产的时候不能消费,或者消费的时候不能生产。
4.使用synchronized同步代码块进行线程同步
//创建一个同步对象
private static final Object loc = new Object();
/**
* 生产资源方法
*/
public void add(){
synchronized (loc) {
obj[0]="水资源=="+num;
System.out.println(Thread.currentThread().getName()+"生产的资源是:"+obj[0]);
num++;
}
}
/**
* 消费资源方法
*/
public void delete(){
synchronized (loc) {
System.out.println(Thread.currentThread().getName()+"消费的资源是:"+obj[0]);
obj[0]=null;
}
上面执行完成以后发现存在新的问题,出现多次消费没有生产或者多次生产没有消费的问题,要解决这个问题,首先需要判断是否满足消费或者生产的条件。
什么时候消费:当数组空间中不是null的时候可以进行消费。
什么时候生产:当数组空间为null的时候才能生产。
如果不满足生产的时候,但是当前正好CPU在生产的线程上,这时必须要让注水的线程等待,等到可以注水的时候将本次注水的动作做完。
如果不满足消费的时候,但是当前正好CPU在消费的线程上,这时必须要让抽水的线程等待,等到可以抽水的时候将本次抽水的动作做完。
使用java中线程的等待和唤醒机制(线程间的通信)。
5.等待和唤醒机制----修改Resource为注水和抽水方法添加线程等待和唤醒操作
等待和唤醒的方法没有定义在Thread类中,而是定义在Object类中(因为只有同步的锁才能让线程等待或者将等待的线程唤醒,而同步的锁是任意对象,等待和唤醒的方法只能定义在Object类中)
void | wait() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待 |
void | notify() 唤醒在此对象监视器上等待的单个线程。 |
void | notifyAll() 唤醒在此对象监视器上等待的所有线程。 |
注意:等待和唤醒(线程通信)必须位于同步中。因为等待和唤醒必须使用当前的锁才完成。
/**
* 生产资源方法
*
*/
public void add() throws InterruptedException{
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{
synchronized (loc) {
//如果对象数组为空,不能消费
if(objs[0]==null){
//消费等待
loc.wait();//抛出异常
}
System.out.println(Thread.currentThread().getName()+"消费的资源是:"+objs[0]);
objs[0]=null;
//唤醒注水生产线程
loc.notify();
}
测试类:
上面程序处理好了单线程的注水和抽水动作,下面将程序修改为多线程注水和多线程抽水情况。
6.修改主类,增加注水和抽水线程---多线程生产和消费
public class Main1 {
public static void main(String[] args) {
Resource r = new Resource();
ShengChanThread sc = new ShengChanThread(r);
Thread scth1 = new Thread(sc);
Thread scth2 = new Thread(sc);
scth1.setName("生产者1");
scth2.setName("生产者2");
scth1.start();
scth2.start();
XiaoFeiThread xf = new XiaoFeiThread(r);
Thread xfth1 = new Thread(xf);
Thread xfth2 = new Thread(xf);
xfth1.setName("消费者1");
xfth2.setName("消费者2");
xfth1.start();
xfth2.start();
}
}
测试:
问题出现了:多次生产和多次消费!!
原因:假设生产线程1完成后,切换给其他线程,这个时候可以切换的线程有三个,生产线程1,消费线程1,2。因此会出现多次消费或者多次生产。
解决方式:将if语句改为while语句,如果消费线程中数组为null,则让他一直等待不往下进行消费,直到切换到生产线程进行生产,使得数组中不为null,此时如果切回到消费线程,则进行消费。生产同理。
7.将if语句改为while语句—解决多次生产和多次消费
/**
* 生产资源方法
*
*/
public void add() throws 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 delete() throws InterruptedException{
synchronized (loc) {
//如果对象数组为空,不能消费
while(objs[0]==null){
//消费等待
loc.wait();//抛出异常
}
System.out.println(Thread.currentThread().getName()+"消费的资源是:"+objs[0]);
objs[0]=null;
//唤醒注水生产线程
loc.notify();
}
}
此时经过测试发现新的问题,有一种情况。我设置的到20,此处到3就结束了。
所有的线程都处于等待了,外面没有可以执行的线程了,这个时候出现死锁。
死锁过程(以上图为例):起初数组为null,进入生产线程1,生产水资源1,然后切换给消费者2,进行消费后,数组为空了,切换给消费者1,此时消费者1处于等待,直到切换给生产者2,生产者生产后数组不为null,切换给了自己生产者2 ,此时生产者2处于等待,直到切换给消费者2,消费后数组为null,同样切换给自己,此时消费者2处于等待,直到切换给生产者1,生产后数组不为null后同样切换给自己生产者1,此时生产者1处于等待。到此四条线程全部处于等待状态。
解决方案:将notify()换成notifyAll(),唤醒所有处于等待的线程。每次在唤醒的时候都是唤醒所有线程,即使唤醒了自己的同伴,也无所谓,因为还要继续判断,这样一定还会等待,但是唤醒中一定有另外一方的线程,它们肯定不会等待。它们不等待,就会去操作,它们操作完成也唤醒所有。
8. 将notify()换成notifyAll()---解决死锁状态
/**
* 生产资源方法
*
*/
public void add() throws 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 delete() throws InterruptedException{
synchronized (loc) {
//如果对象数组为空,不能消费
while(objs[0]==null){
//消费等待
loc.wait();//抛出异常
}
System.out.println(Thread.currentThread().getName()+"消费的资源是:"+objs[0]);
objs[0]=null;
//唤醒注水生产线程
loc.notifyAll();
}
}
多生产多消费的程序中,为了保证不出现全部线程被wait的情况,只能在唤醒的时候使用notifyAll将所有处于等待的线程唤醒。这样每次都可以保证一定会有存活的线程。但是这种唤醒效率太低了,经常会发生生产方唤醒自己的同伴线程,或者是消费方唤醒自己的同伴线程。
在JDK5中提供Condition接口。它用来代替等待和唤醒机制。
9.Condition接口---解决唤醒效率低
在JDK5之前,一个同步的锁下面的等待和唤醒无法辨别当前让等待或唤醒的线程到底属于生产还是属于消费。而Condition接口,它可以创建出不同的等待和唤醒的对象,然后可以用在不同的场景下:
可以创建一个Condition对象,专门负责生产。
可以创建一个Condition对象,专门负责消费。
可以通过负责生产的Condition对象专门监视负责生产的线程。通过负责消费的Condition监视消费的线程。等待和唤醒的时候,可以使用各自的Condition对象。
void | |
void | signal()唤醒一个等待线程。 |
void | signalAll() 唤醒所有等待线程。 |
注意:
如果使用Condition接口,同步必须使用Lock接口。
如果程序中同步使用的同步代码块,等待和唤醒只能使用Object中的wait、notify、notifyAll方法。
只有同步使用的Lock接口,等待和唤醒才能使用Condition接口。
10.修改线程同步,使用Lock接口
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 公共资源类
*
*/
public class Resource {
// 使用一个对象数组充当水池
private Object objs[] = new Object[1];// 生产一个消费后,才能再生产
// 记录生产和消费的次数
private int num = 1;
// 创建Lock接口,作为同步的锁
private Lock lock = new ReentrantLock();
// 负责监视生产的线程
private Condition scCondition = lock.newCondition();
// 负责监视消费的线程
private Condition xfCondition = lock.newCondition();
/**
* 生产资源方法
*
*/
public void add() {
try {
// 锁住资源
lock.lock();
// 如果对象数组不为空,不能生产
while (objs[0] != null) {
// 生产等待
scCondition.await();
}
objs[0] = "水资源==" + num;
System.out.println(Thread.currentThread().getName() + "生产的资源是:" + objs[0]);
num++;
// 唤醒抽水消费线程
xfCondition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
/**
* 消费资源方法
*
*/
public void delete() {
try {
// 锁住资源
lock.lock();
// 如果对象数组为空,不能消费
while (objs[0] == null) {
// 消费等待
xfCondition.await();
}
System.out.println(Thread.currentThread().getName() + "消费的资源是:" + objs[0]);
objs[0] = null;
// 唤醒注水生产线程
scCondition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
}
有无异常都要释放锁,因此改为try…catch…finally
11.各种区别
11.1 等待和唤醒两种方式的区别
Object类提供的wait、notify、notifyAll方法 | Condition接口提供的await(),signal(),signalAll() |
同步代码【synchronized】实现线程同步 | Lock接口对象实现线程同步 |
效率低,会唤醒同类 | 效率高,不唤醒同类 |
11.2 notify和notifyAll区别
notify | notifyAll |
只随机唤醒一个wait线程 | 唤醒所有的wait线程 |
可能会导致死锁 | 不会导致死锁 |
唤醒等待的线程不分彼此 |
11.3 signal和signalAll区别
signal | signalAll |
只随机唤醒一个wait线程【同一类】 | 唤醒所有的wait线程【同一类】 |
11.4 sleep和wait区别
sleep | wait | |
Thread | Object | |
依赖于系统时钟和CPU调度机制 | 线程调用notify()或者notifyAll()方法 | |
不释放以获取的锁资源 | 释放以获取的锁资源 |