• 3、生产者和消费者问题


    这是面试高频:还有其它的:单例模式、8大排序算法、死锁;

    synchronized版

    两个线程的情况

    package com.zxh.demo01;
    
    /**
     * 线程之间的通信问题:也就是生产者和消费者问题!
     * 如何做到通信:等待唤醒,通知唤醒
     * 模拟多个线程操作一个变量:对 num 进行加1、减1操作
     * A num + 1
     * B num - 1
     */
    
    public class A {
        public static void main(String[] args) {
            Data data = new Data();
    
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "B").start();
    
        }
    }
    // 如何实现通信,分三步:1、判断等待,2、业务操作,3、通知唤醒
    class Data{ //资源类,数字,实现低耦合
    
        private int number = 0;
    
        // +1
        public synchronized void increment() throws InterruptedException {
            if(number != 0){
                this.wait();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + " => " + number);
            // 通知其他线程,我执行 +1 完毕
            this.notifyAll();
        }
    
        // -1
        public synchronized void decrement() throws InterruptedException {
            if(number == 0){
                this.wait();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + " => " + number);
            // 通知其他线程,我执行 -1 完毕
            this.notifyAll();
        }
    
    
    }

    顺利执行:

    存在虚假唤醒问题

    更多的线程参与,A和C进行+1操作,B和D进行-1操作,就会导致问题

    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            try {
                data.increment();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "A").start();
    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            try {
                data.decrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "B").start();
    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            try {
                data.increment();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "C").start();
    new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            try {
                data.decrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "D").start();

    解决虚假唤醒问题

    问题:就是存在虚假唤醒

    什么是虚假唤醒?

    不需要唤醒的线程也别唤醒了,比如,B执行后,A和C都被唤醒。那么会发生什么呢?接下来看:

    • 现在 A和C 进行的是 +1 操作,B和D 进行的是 -1 操作

    假设程序运行的过程:

    1. A 进行了 +1 操作后等待,此时进入C 等待,再进入B进行了 -1 操作后唤醒所有线程并等待

    2. 这个时候A 和 C 都在这个位置

    3. 在这个位置被唤醒,那么假设A先拿到锁,进行了 +1 操作,并唤醒其他线程,判断 number != 0,A线程等待,释放了+1方法的锁。
    4. 虽然唤醒了BD线程,但是线程C并没有在等待,并且到了+1方法的锁,如果CPU调度到它,那么又会进行一次 +1 操作,结果就会有出现2。

    如何解决呢?

    在第4步的时候,C紧接着A后面被调度到,此时的number = 1,显然不能再进行 +1 操作了,但是 if 语句不能再次进行判断,所以就可以使用while语句再次进行判断,是否可以操作即可。

     

    在jdk8的源码中,wait()方法中,就有如下说明:

     修改代码,将if判断改为while循环,进行多次判断,这样就解决了虚假唤醒

    /**
     * 线程之间的通信问题:也就是生产者和消费者问题!
     * 如何做到通信:等待唤醒,通知唤醒
     * 模拟多个线程操作一个变量:对 num 进行加1、减1操作
     * A num + 1
     * B num - 1
     */
    public class A {
        public static void main(String[] args) {
            Data data = new Data();
    
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "B").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "C").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "D").start();
    
        }
    }
    // 如何实现通信,分三步:1、判断等待,2、业务操作,3、通知唤醒
    class Data{ //资源类,数字,实现低耦合
    
        private int number = 0;
    
        // +1
        public synchronized void increment() throws InterruptedException {
            while(number != 0){
                this.wait();    //A C
            }
            number++;
            System.out.println(Thread.currentThread().getName() + " => " + number);
            // 通知其他线程,我执行 +1 完毕
            this.notifyAll();
        }
    
        // -1
        public synchronized void decrement() throws InterruptedException {
            while(number == 0){
                this.wait();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + " => " + number);
            // 通知其他线程,我执行 -1 完毕
            this.notifyAll();
        }
    
    
    }

    JUC版

    • 使用的是Lock锁

    Lock用什么替换sync版

    使用JUC版的话,需要知道原来的3剑客用什么替换了?

    • 文档中Lock接口中提到一个方法,是获取一个Condition接口的实现类ConditionObject对象

    • 在源码中可以看到

    • 该方法具体的说明,也可以进行等待和唤醒操作

    • 点进去这个Condition接口,可以发现是Locks包下的

    • 具体的说明如下:该接口取代了对象监视器方法的使用

    • 文档里页举例该对象的使用方法,await()等待、signal()唤醒

    代码实现

    package com.zxh.pc;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 线程之间的通信问题:也就是生产者和消费者问题!
     * 如何做到通信:等待唤醒,通知唤醒
     * 模拟多个线程操作一个变量:对 num 进行加1、减1操作
     * A num + 1
     * B num - 1
     */
    
    public class B {
        public static void main(String[] args) {
            Data2 data = new Data2();
    
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "B").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "C").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "D").start();
    
        }
    }
    
    // 如何实现通信,分三步:1、判断等待,2、业务操作,3、通知唤醒
    class Data2{ //资源类,数字,实现低耦合
    
        private int number = 0;
    
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
    
        //condition.await();  //等待
        //condition.signalAll();  //唤醒所有
    
        // +1
        public void increment() throws InterruptedException {
            lock.lock();    // 加锁
            try {
                // 业务
                while(number != 0){
                    //等待
                    condition.await();
                }
                number++;
                System.out.println(Thread.currentThread().getName() + " => " + number);
                // 通知其他线程,我执行 +1 完毕
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();  // 解锁
            }
        }
    
        // -1
        public void decrement() throws InterruptedException {
            lock.lock();    // 加锁
            try {
                // 业务
                while(number == 0){
                    //等待
                    condition.await();
                }
                number--;
                System.out.println(Thread.currentThread().getName() + " => " + number);
                // 通知其他线程,我执行 -1 完毕
                condition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();  // 解锁
            }
        }
    
    
    }

    存在问题

    任何一个新的技术,绝不仅仅是覆盖了原来的技术,肯定会有它的优势和补充!

    解决问题

    那么要如何解决呢?

    Condition 精准通知和唤醒线程

    举例

    有3个线程ABC,现在想让这3个线程的执行顺序是固定的:A -> B -> C,依次循环。

    • 以之前所掌握的知识,没有办法解决的。

    • JUC中 Condition 接口,相当于一个对象资源监视器,要想做到精准唤醒,使用多个监视器进行控制即可!

    • 比如:在购物中,用户下单 -> 支付 -> 商品数量 - 1,每个步骤都有一个资源监视器,执行过程中,只需要唤醒对应的监视器就可以了。

     

    测试代码

    package com.zxh.pc;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    // 3个线程ABC,现在想让这3个线程的执行顺序是固定的:A -> B -> C,依次循环。
    public class C {
        public static void main(String[] args) {
            Data3 data = new Data3();
    
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    data.printA();
                }
            }, "A").start();
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    data.printB();
                }
            }, "B").start();
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    data.printC();
                }
            }, "C").start();
    
        }
    }
    class Data3{    // 资源类
    
        private int number = 1; // 用于控制的变量
    
        private Lock lock = new ReentrantLock();
        //资源监视器,现有3个线程,创建3个对应的资源监视器
        private Condition condition1 = lock.newCondition();
        private Condition condition2 = lock.newCondition();
        private Condition condition3 = lock.newCondition();
    
        public void printA() {
            lock.lock();    //加锁
            try {
                while(number != 1){
                    condition1.await(); // 等待
                }
                // 业务
                number = 2;
                System.out.println(Thread.currentThread().getName() + "=> AAAAA");
                // 唤醒,需要唤醒的是指定的线程 B 的监视器
                condition2.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();  // 解锁
            }
        }
    
        public void printB(){
            lock.lock();    // 加锁
            try {
                while(number != 2){
                    condition2.await(); // 等待
                }
                // 业务
                number = 3;
                System.out.println(Thread.currentThread().getName() + "=> BBBBBB");
                //唤醒,需要唤醒指定线程 C 的监视器
                condition3.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();  // 解锁
            }
        }
    
        public void printC(){
            lock.lock();    // 加锁
            try {
                while(number != 3){
                    condition3.await(); // 等待
                }
                // 业务
                number = 1;
                System.out.println(Thread.currentThread().getName() + "=> CCCCCCC");
                //唤醒,需要唤醒指定线程 A 的监视器
                condition1.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();  // 解锁
            }
        }
    
    }

    成功做到精准通知

    致力于记录学习过程中的笔记,希望大家有所帮助(*^▽^*)!
  • 相关阅读:
    高并发第八弹:J.U.C起航(java.util.concurrent)
    高并发第七弹:不可变对象及常用的工具类
    高并发第六弹:线程封闭(ThreadLocal)
    分布式爬取链家网二手房信息
    scrapy CrawlSpider爬取猎云网文章数据
    selenium实现12306网站自动抢票
    王者荣耀官方壁纸爬取
    使用 vsftpd 服务传输文件
    使用 Apache 服务部署静态网站
    iptables 与 firewalld 防火墙
  • 原文地址:https://www.cnblogs.com/zxhbk/p/12950855.html
Copyright © 2020-2023  润新知