• 第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,但是不会放弃暂用的锁,此时就造成了死锁

  • 相关阅读:
    Leetcode 12. Integer to Roman
    Leetcode 133. Clone Graph
    Leetcode 199. Binary Tree Right Side View
    Leetcode 200. Number of Islands
    React通过Ajax获取数据
    canvas鼠标点击划线
    制作图片墙
    CSS制作翻牌特效
    输入框制作方法
    初来咋到
  • 原文地址:https://www.cnblogs.com/cenyu/p/6149850.html
Copyright © 2020-2023  润新知