zoukankan      html  css  js  c++  java
  • 四、生产者和消费者

    我们这里的生产者和消费者模型为:

        生产者Producer 生产某个对象(共享资源),放在缓冲池中,然后消费者从缓冲池中取出这个对象。也就是生产者生产一个,消费者取出一个。这样进行循环。

      第一步:我们先创建共享资源的类 Person,它有两个方法,一个生产对象,一个消费对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class Person {
        private String name;
        private int age;
         
        /**
         * 生产数据
         * @param name
         * @param age
         */
        public void push(String name,int age){
            this.name = name;
            this.age = age;
        }
        /**
         * 取数据,消费数据
         * @return
         */
        public void pop(){
            System.out.println(this.name+"---"+this.age);
        }
    }

      第二步:创建生产者线程,并在 run() 方法中生产50个对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Producer implements Runnable{
        //共享资源对象
        Person p = null;
        public Producer(Person p){
            this.p = p;
        }
        @Override
        public void run() {
            //生产对象
            for(int i = 0 ; i < 50 ; i++){
                //如果是偶数,那么生产对象 Tom--11;如果是奇数,则生产对象 Marry--21
                if(i%2==0){
                    p.push("Tom"11);
                }else{
                    p.push("Marry"21);
                }
            }
        }
    }

      第三步:创建消费者线程,并在 run() 方法中消费50个对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Consumer implements Runnable{
        //共享资源对象
        Person p = null;
        public Consumer(Person p) {
            this.p = p;
        }
         
        @Override
        public void run() {
            for(int i = 0 ; i < 50 ; i++){
                //消费对象
                p.pop();
            }
        }
    }

      由于我们的模型是生产一个,马上消费一个,那期望的结果便是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次

    但是结果却是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Marry---21
    Marry---21
    Marry---21
    Marry---21
    Marry---21
    ......
    Marry---21
    Marry---21
    Marry---21
    Marry---21
    Marry---21

    为了让结果产生的更加明显,我们在共享资源的 pop() 和 push() 方法中添加一段延时代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /**
         * 生产数据
         * @param name
         * @param age
         */
        public void push(String name,int age){
            this.name = name;
            try {
                //这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
                Thread.sleep(10);
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.age = age;
        }
        /**
         * 取数据,消费数据
         * @return
         */
        public void pop(){
            try {
                Thread.sleep(10);
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.name+"---"+this.age);
        }  

      

    这个时候,结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Marry---11
    Tom---21
    Marry---11
    Tom---21
    Marry---11
    Tom---21
    Marry---11
    Tom---21
    ......
    Tom---11
    Tom---21
    Marry---11
    Tom---21
    Marry---11
    Marry---21

      

    结果分析:这时候我们发现结果全乱套了,Marry--21是固定的,Tom--11是固定的,但是上面的结果全部乱了,那这又是为什么呢?而且有很多重复的数据连续出现,那这又是为什么呢?

    原因1:出现错乱数据,是因为先生产出Tom--11,但是消费者没有消费,然后生产者继续生产出name为Marry,但是age还没有生产,而消费者这个时候拿去消费了,那么便出现 Marry--11。同理也会出现 Tom--21

    原因2:出现重复数据,是因为生产者生产一份数据了,消费者拿去消费了,但是第二次生产者生产数据了,但是消费者没有去消费;而第三次生产者继续生产数据,消费者才开始消费,这便会产生重复

    解决办法1:生产者生产name和age必须要是一个整体一起完成,即同步。生产的中间不能让消费者来消费即可。便不会产生错乱的数据。如何同步可以参考:

            Java 多线程详解(三)------线程的同步:http://www.cnblogs.com/ysocean/p/6883729.html

         这里我们选择同步方法(在方法前面加上 synchronized)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class Person {
        private String name;
        private int age;
         
        /**
         * 生产数据
         * @param name
         * @param age
         */
        public synchronized void push(String name,int age){
            this.name = name;
            try {
                //这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
                Thread.sleep(10);
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.age = age;
        }
        /**
         * 取数据,消费数据
         * @return
         */
        public synchronized void pop(){
            try {
                Thread.sleep(10);
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.name+"---"+this.age);
        }
    }

      结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Marry---21
    Marry---21
    Marry---21
    Marry---21
    Marry---21
    Tom---11
    Tom---11
    ......
    Tom---11
    Tom---11
    Tom---11
    Tom---11
    Tom---11

    问题:还是没有解决上面的问题2,出现重复的问题。期望的结果是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次。那如何解决呢?

    解决办法:生产者生产一次数据了,就暂停生产者线程,等待消费者消费;消费者消费完了,消费者线程暂停,等待生产者生产数据,这样来进行。

    这里我们介绍一个同步锁池的概念:

      同步锁池:同步锁必须选择多个线程共同的资源对象,而一个线程获得锁的时候,别的线程都在同步锁池等待获取锁;当那个线程释放同步锁了,其他线程便开始由CPU调度分配锁

    关于让线程等待和唤醒线程的方法,如下:(这是 Object 类中的方法)

      

      

    wait():执行该方法的线程对象,释放同步锁,JVM会把该线程放到等待池中,等待其他线程唤醒该线程

    notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待(注意锁池和等待池的区别)

    notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待。

    注意:上述方法只能被同步监听锁对象来调用,这也是为啥wait() 和 notify()方法都在 Object 对象中,因为同步监听锁可以是任意对象,只不过必须是需要同步线程的共同对象即可,否则别的对象调用会报错:        java.lang.IllegalMonitorStateException

    假设 A 线程和 B 线程同时操作一个 X 对象,A,B 线程可以通过 X 对象的 wait() 和 notify() 方法来进行通信,流程如下:

    ①、当线程 A 执行 X 对象的同步方法时,A 线程持有 X 对象的 锁,B线程在 X 对象的锁池中等待

    ②、A线程在同步方法中执行 X.wait() 方法时,A线程释放 X 对象的锁,进入 X 对象的等待池中

    ③、在 X 对象的锁池中等待锁的 B 线程获得 X 对象的锁,执行 X 的另一个同步方法

    ④、B 线程在同步方法中执行 X.notify() 方法,JVM 把 A 线程从等待池中移动到 X 对象的锁池中,等待获取锁

    ⑤、B 线程执行完同步方法,释放锁,等待获取锁的 A 线程获得锁,继续执行同步方法

    那么为了解决上面重复的问题,修改代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public class Person {
        private String name;
        private int age;
         
        //表示共享资源对象是否为空,如果为 true,表示需要生产,如果为 false,则有数据了,不要生产
        private boolean isEmpty = true;
        /**
         * 生产数据
         * @param name
         * @param age
         */
        public synchronized void push(String name,int age){
            try {
                //不能用 if,因为可能有多个线程
                while(!isEmpty){//进入到while语句内,说明 isEmpty==false,那么表示有数据了,不能生产,必须要等待消费者消费
                    this.wait();//导致当前线程等待,进入等待池中,只能被其他线程唤醒
                }
                 
                //-------生产数据开始-------
                this.name = name;
                //延时代码
                Thread.sleep(10);
                this.age = age;
                //-------生产数据结束-------
                isEmpty = false;//设置 isEmpty 为 false,表示已经有数据了
                this.notifyAll();//生产完毕,唤醒所有消费者
            catch (Exception e) {
                e.printStackTrace();
            }
             
        }
        /**
         * 取数据,消费数据
         * @return
         */
        public synchronized void pop(){
            try {
                //不能用 if,因为可能有多个线程
                while(isEmpty){//进入 while 代码块,表示 isEmpty==true,表示为空,等待生产者生产数据,消费者要进入等待池中
                    this.wait();//消费者线程等待
                }
                //-------消费开始-------
                Thread.sleep(10);
                System.out.println(this.name+"---"+this.age);
                //-------消费结束------
                isEmpty = true;//设置 isEmpty为true,表示需要生产者生产对象
                this.notifyAll();//消费完毕,唤醒所有生产者
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

      

    结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Tom---11
    Marry---21
    Tom---11
    Marry---21
    Tom---11
    Marry---21
    Tom---11
    ......
    Marry---21
    Tom---11
    Marry---21
    Tom---11
    Marry---21
    Tom---11
    Marry---21  

    那么这便是我们期待的结果,交替出现。

    死锁:

    ①、多线程通信的时候,很容易造成死锁,死锁无法解决,只能避免

    ②、当 A 线程等待由 B 线程持有的锁,而 B 线程正在等待由 A 线程持有的锁时发生死锁现象(比如A拿着铅笔,B拿着圆珠笔,A说你先给我圆珠笔,我就把铅笔给你,而B说你先给我铅笔,我就把圆珠笔给你,这就造成了死锁,A和B永远不能进行交换)

    ③、JVM 既不检测也不避免这种现象,所以程序员必须保证不能出现这样的情况

    Thread 类中容易造成死锁的方法(这两个方法都已经过时了,不建议使用):

    suspend():使正在运行的线程放弃 CPU,暂停运行(不释放锁)

    resume():使暂停的线程恢复运行

    情景:A 线程获得对象锁,正在执行一个同步方法,如果 B线程调用 A 线程的 suspend() 方法,此时A 暂停运行,放弃 CPU 资源,但是不放弃同步锁,那么B也不能获得锁,A又暂停,那么便造成死锁。

    解决死锁法则:当多个线程需要访问 共同的资源A,B,C时,必须保证每一个线程按照一定的顺序去访问,比如都先访问A,然后B,最后C。就像我们这里的生产者---消费者模型,制定了必须生产者先生产一个对象,然后消费者去消费,消费完毕,生产者才能在开始生产,然后消费者在消费。这样的顺序便不会造成死锁。

  • 相关阅读:
    Ubuntu通过ADB连接手机
    MyRolan (快速启动小工具)
    关闭QQ右下角弹窗小程序
    day23作业-韩明琰
    day18-20作业-韩明琰
    day14-16作业-韩明琰
    java中对于多态的理解
    day11作业-韩明琰
    day10作业-韩明琰
    day09_作业
  • 原文地址:https://www.cnblogs.com/zhoanghua/p/9292151.html
Copyright © 2011-2022 走看看