Fork me on GitHub

第23章 java线程通信——生产者/消费者模型案例

第23章 java线程通信——生产者/消费者模型案例

1.案例:

package com.rocco;

/**
 * 生产者消费者问题,涉及到几个类
 * 第一,这个问题本身就是一个类,即主类
 *  第二,既然是生产者、消费者,那么生产者类和消费者类就是必须的
 * 第三,生产什么,消费什么,所以物品类是必须的,这里是馒头类
 *  第四,既然是线程,那么就不是一对一的,也就是说不是生产一个消费一个,既然这样,多生产的往哪里放
 *  现实中就是筐了,在计算机中也就是数据结构,筐在数据结构中最形象的就是栈了,因此还要一个栈类
 */



public class ProduceConsume {
    public static void main(String[] args) {
        SyncStack ss = new SyncStack();//建造一个装馒头的框
        Producer p = new Producer(ss);//新建一个生产者,并把已经创建好框传给它,使其使用这个框
        Consume c = new Consume(ss);//新建一个消费者,并把已经创建好框传给它,使其使用这个框
        Thread tp = new Thread(p);//接口方法创建一个生产者线程
        Thread tc = new Thread(c);//接口方法创建一个消费者线程
        tp.start();//开启生产者线程
        tc.start();//开启消费者线程
    }
}


//馒头类
class SteamBread{
    int id;
    SteamBread(int id){
        this.id = id;
    }

    @Override
    public String toString() { //重写馒头返回的方法
        return "SteamBread:" + id;
    }
}


//装馒头的框,栈结构
class SyncStack{
    int index = 0;
    SteamBread[] stb = new SteamBread[6]; //构造一个装馒头的数组,容量是6


    //放入框中,相当于入栈
    public synchronized void push(SteamBread sb){
        while (index==stb.length){  //筐满了,即栈满,
            try {
                this.wait();//让当前线程等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();//唤醒在此对象监视器上等待的单个线程,即消费者线程
        stb[index]=sb;
        this.index++;
    }

    //从框中拿出,相当于出栈
    public synchronized SteamBread pop(){
        while (index==0){//筐空了,即栈空
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
        this.index--;//push第n个之后,this.index++,使栈顶为n+1,故return之前要减一
        return stb[index];
    }
}





//生产者类,实现了Runnable接口,以便于构造生产者线程
class Producer implements Runnable{
    SyncStack ss = null;
    Producer(SyncStack ss){
        this.ss = ss;
    }

    @Override
    public void run() {
        // 开始生产馒头
        for (int i = 0; i < 20; i++) {
            SteamBread stb = new SteamBread(i);//生产一个新的包子
            ss.push(stb);//把包子存到框里面
            System.out.println("生产了"+stb);//打印出我们生产了一个包子,注意此处stb本身是有一个返回值的,这这里将被调用
            try {
                Thread.sleep(10);//每生产一个馒头,睡觉10毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}



//消费者类,实现了Runnable接口,以便于构造消费者线程
class Consume implements Runnable{
    SyncStack ss  = null;
    public Consume(SyncStack ss){
        super();
        this.ss = ss;
    }

    @Override
    public void run() {
        //开始消费馒头
        for (int i = 0; i < 20; i++) {
            SteamBread stb = ss.pop();
            System.out.println("消费了"+stb);
            try {
                Thread.sleep(10);//每消费一个馒头,睡觉100毫秒。即生产多个,消费一个
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}



代码版权是thinkpadshi博主的,写的挺好的。

2.知识点讲解

线程通信-wait()和notify()方法介绍:
java.lang.bjec类提供了这两类方法用于线程通信
wait():执行该方法的线程对象释放同步锁,JVM把该线程存放到等待池中,等待其他线程唤醒该线程
notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待
notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待
注意:上述方法只能被同步监听锁对象那个来调用,否则报错:

建设A线程和B线程共同操作一个X对象。A,B线程可以通过X对象的wait()和notify()方法来进行通信,流程如下:
1.当A线程执行X对象的同步方法时,A线程持有X对象的锁,B线程在X对象的锁池中等待
2.A线程在同步方法中执行X.wait()方法时,A线程释放X对象的锁,并进入X对象的等待池
3.在X对象的锁池中等待的B线程获取X兑现的锁,执行X的另一个同步方法
4.B线程在同步方法中执行X.notify()方法时,JVM把A线程从X对象的等待池中移动到X对象的锁池中,等待获取锁
5.B线程执行完同步方法,释放锁A线程获得锁,继续执行同步方法

解释一下:
等待池指的是线程现在还没有能力去抢锁,所以被放在一边被等待赋予抢锁的能力。

有点像你投简历找工作需要经历简历和面试两个关卡,简历刚投到一家公司的时候,你现在还不具备去面试的资格,这个时候你的简历被放在一堆不被面试的文件夹里,这个文件夹叫做等待池文件夹

锁池就是线程具备了抢锁的能力,但是同时有多个线程来抢,这个时候线程就处在锁池里,然后等待谁抢到锁,谁就执行锁里面的的代码块。

同样,就像你的简历通过的筛选,通知你参加面试了,这个时候你就具备了争夺这个工作的机会,但是这个时候还是有很多的人跟你一样有面试机会,但最终只能录取一个人,至于录用谁就要看这个人的能力了。这个时候你的简历从等待池里面拿出来了,放在锁池里面。

为什么wait(),notify()都是Object类的方法:

多个线程只有使用相同的一个对象的时候,多线程之间才有互斥效果
我们把这个用来做互斥的对象称之为:同步监听对象/同步锁

同步锁可以选择任意类型的对象即可,只需要保证多个线程使用的是相同锁对象即可
因为,只有同步监听对象才能调用wait和notify方法,所以wait和notify方法应该存在与Object类中,而不是Thread类中

3.使用Lock(锁)方法

上面是使用普通的synchronized修饰符,多线程的同步操作有三种方式,现在看看Lock方法如何来写
使用Lock和Condition接口:
wait()和notify()方法,只能被同步监听锁对象来调用,否则报错IllegalMontiorStateException
那么,现在有一个问题,Lock机制是不需要同步锁的,他自己就是一个锁,这个时候当然也是没有自动获取锁和自动释放锁的概念的。
因为没有同步锁,所以,我们就不能使用wait()和notify()方法
那么** 解决方法是:**
java5中提供了Lock机制的同时提供了处理Lock机制的通信控制的Condition接口
** 具体就是:**
1.使用Lock机制取代synchronized代码块和synchronized方法
2.使用Condition接口对象的await,signal,signalAll方法取代Object类中的wait,notify,notifyAll方法
** 解释一下:**
await()和wait()方法作用是相同相同的
signal()和notify()方法的作用是完全相同的
signalAll()和notifyAll()方法的作用是完全相同的
具体使用方法:
Condition本身是Lock的一个内部类,实例实质上被绑定到一个锁上,要为特定的锁Lock实例获得Condition实例,请使用newCondition()方法
生产者/消费者模型里面有两个类,所以我们要创建两个锁,分别用在不同的额

代码示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 生产者消费者问题,涉及到几个类
 * 第一,这个问题本身就是一个类,即主类
 *  第二,既然是生产者、消费者,那么生产者类和消费者类就是必须的
 * 第三,生产什么,消费什么,所以物品类是必须的,这里是馒头类
 *  第四,既然是线程,那么就不是一对一的,也就是说不是生产一个消费一个,既然这样,多生产的往哪里放
 *  现实中就是筐了,在计算机中也就是数据结构,筐在数据结构中最形象的就是栈了,因此还要一个栈类
 */



public class ProduceConsume {
    public static void main(String[] args) {
        SyncStack ss = new SyncStack();//建造一个装馒头的框
        Producer p = new Producer(ss);//新建一个生产者,并把已经创建好框传给它,使其使用这个框
        Consume c = new Consume(ss);//新建一个消费者,并把已经创建好框传给它,使其使用这个框
        Thread tp = new Thread(p);//接口方法创建一个生产者线程
        Thread tc = new Thread(c);//接口方法创建一个消费者线程
        tp.start();//开启生产者线程
        tc.start();//开启消费者线程
    }
}


//馒头类
class SteamBread{
    int id;
    SteamBread(int id){
        this.id = id;
    }

    @Override
    public String toString() { //重写馒头返回的方法
        return "SteamBread:" + id;
    }
}


//装馒头的框,栈结构
class SyncStack{
    int index = 0;
    SteamBread[] stb = new SteamBread[6]; //构造一个装馒头的数组,容量是6
    final Lock lock = new ReentrantLock(); //创建一个锁
    final Condition condition = lock.newCondition(); //为lock锁创建一个Conditon实例


    //放入框中,相当于入栈
    public void push(SteamBread sb){
        lock.lock();//获取锁
            try {
                while (index==stb.length) { //筐满了,即栈满,
                    condition.await();//当前线程等待
                }
                condition.signal();//唤醒另外一个线程
                stb[index]=sb;
                this.index++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();//释放锁
            }


    }

    //从框中拿出,相当于出栈
    public SteamBread pop(){
        lock.lock();//获取锁
        try {
            while (index==0) {//筐空了,即栈空
                condition.await();//当前线程等待
            }
            this.index--;//push第n个之后,this.index++,使栈顶为n+1,故return之前要减一
            condition.signal();//唤醒另外一个线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();//释放锁
        }
    return stb[index];
    }
}


4.死锁

多线程通信的时候是非常容易造成死锁的,死锁是无法解决的,只能避免:
当A线程等待B线程所持有的锁,而B线程也在等在A线程所持有的锁时,这个收会发生死锁现象,对于死锁JVM是不检测的
由于死锁不会被检测出来,所以只能由程序员来保证线程不会导致死锁
最有名的死锁问题就是:哲学家吃面条的问题 **
避免死锁法则:当多个线程都要访问共享的资源A,B,C时,保证每一个线程都按照相同的顺序去访问他们,比如都都先访问A,接着B,最后C
** Thread类中一些过时的用法

suspend();使正在运行的线程放弃CPU,暂停运行
resume():是暂停的线程恢复运行
过时的用法是非常容易导致死锁的,所以不可再用

** 死锁情况:**
A线程获得对象锁,正在执行一个同步方法,如果B线程调用A线程的suspend()方法,此时A线程暂停运行,此时A线程放弃CPU,但是不会放弃暂用的锁,此时就造成了死锁

posted @ 2016-12-09 15:53  洋葱源码  阅读(592)  评论(0编辑  收藏  举报