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();  // 解锁
        }
    }

}

成功做到精准通知

posted @ 2020-05-24 14:06  忘忧山的兰木  阅读(235)  评论(0编辑  收藏  举报
她只是想吃这个而已啊……这一定是她非常爱吃的,我居然连如此细微的幸福也夺走了……
Hide
Switch
Save