JUC编程:生产者消费者问题

1 什么是生产者消费者问题

举例:有一个盘子,你女朋友洗小番茄,他洗好一个放到盘子里,你从盘子里取拿来塞到自己嘴里,如果她不洗,那你就干巴巴的等着,这是一个很经典的问题,也是面试最经常问的也是最容易面试让手写的问题,接下里我们用传统的synchronized的写法和使用lock的写法进行实现,并且对比两种方式的差异。

2 使用 synchronized实现

使用实际工作中常用的方式书写

package productconsumer;

/**
 * synchronized
 */
public class Demo1 {

    public static void main(String[] args) {

        Data data = new Data();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.product();
            }
        }, "girl").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.consumer();
            }
        }, "boy").start();

    }
}

/**
 * 资源类
 */
class Data {

    private int num = 0;

    /**
     * 生产
     */
    public synchronized void product() {
        //假设盘子里最大只能装5个番茄
        if(num>=5){
            try {
                //线程等待,不能再洗了
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //业务操作
        num++;
        System.out.println(Thread.currentThread().getName() + ",洗了一个番茄,盘子里剩余:" + num);
        //通知你能吃了
        this.notifyAll();
    }

    /**
     * 消费者
     */
    public synchronized void consumer() {
        if (num <= 0) {
            try {
                //盘子里的吃完了,等着你女朋友去洗
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num--;
        this.notifyAll();
        System.out.println(Thread.currentThread().getName() + ",吃了一个番茄,盘子里剩余:" + num);
    }
}

打印结果:

girl,洗了一个番茄,盘子里剩余:1
girl,洗了一个番茄,盘子里剩余:2
boy,吃了一个番茄,盘子里剩余:1
boy,吃了一个番茄,盘子里剩余:0
girl,洗了一个番茄,盘子里剩余:1
girl,洗了一个番茄,盘子里剩余:2
girl,洗了一个番茄,盘子里剩余:3
girl,洗了一个番茄,盘子里剩余:4
girl,洗了一个番茄,盘子里剩余:5
boy,吃了一个番茄,盘子里剩余:4
boy,吃了一个番茄,盘子里剩余:3
boy,吃了一个番茄,盘子里剩余:2
boy,吃了一个番茄,盘子里剩余:1
boy,吃了一个番茄,盘子里剩余:0
girl,洗了一个番茄,盘子里剩余:1
girl,洗了一个番茄,盘子里剩余:2
girl,洗了一个番茄,盘子里剩余:3
boy,吃了一个番茄,盘子里剩余:2
boy,吃了一个番茄,盘子里剩余:1
boy,吃了一个番茄,盘子里剩余:0

进程完成,退出码 0

很明显,没有任何问题,一个洗,一个吃,但是 当生产者和消费者出现一对多或者多对多的时候,就会出现问题,咱们把上面的main方法改一下,Data资源类不用动

    public static void main(String[] args) {

        Data data = new Data();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.product();
            }
        }, "girl1").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.consumer();
            }
        }, "boy1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.product();
            }
        }, "girl2").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.consumer();
            }
        }, "boy2").start();
    }

两个女孩洗,两个男孩吃,看打印结果

girl1,洗了一个番茄,盘子里剩余:1
girl1,洗了一个番茄,盘子里剩余:2
girl1,洗了一个番茄,盘子里剩余:3
girl1,洗了一个番茄,盘子里剩余:4
boy1,吃了一个番茄,盘子里剩余:3
boy1,吃了一个番茄,盘子里剩余:2
boy1,吃了一个番茄,盘子里剩余:1
boy1,吃了一个番茄,盘子里剩余:0
girl1,洗了一个番茄,盘子里剩余:1
girl1,洗了一个番茄,盘子里剩余:2
girl1,洗了一个番茄,盘子里剩余:3
girl2,洗了一个番茄,盘子里剩余:4
girl2,洗了一个番茄,盘子里剩余:5
boy1,吃了一个番茄,盘子里剩余:4
boy1,吃了一个番茄,盘子里剩余:3
boy1,吃了一个番茄,盘子里剩余:2
boy1,吃了一个番茄,盘子里剩余:1
boy1,吃了一个番茄,盘子里剩余:0
girl2,洗了一个番茄,盘子里剩余:1
girl2,洗了一个番茄,盘子里剩余:2
girl2,洗了一个番茄,盘子里剩余:3
girl2,洗了一个番茄,盘子里剩余:4
girl2,洗了一个番茄,盘子里剩余:5
boy2,吃了一个番茄,盘子里剩余:4
boy2,吃了一个番茄,盘子里剩余:3
boy2,吃了一个番茄,盘子里剩余:2
boy2,吃了一个番茄,盘子里剩余:1
boy2,吃了一个番茄,盘子里剩余:0
boy1,吃了一个番茄,盘子里剩余:-1
boy2,吃了一个番茄,盘子里剩余:-2
girl1,洗了一个番茄,盘子里剩余:-1
girl1,洗了一个番茄,盘子里剩余:0
girl1,洗了一个番茄,盘子里剩余:1
girl2,洗了一个番茄,盘子里剩余:2
girl2,洗了一个番茄,盘子里剩余:3
girl2,洗了一个番茄,盘子里剩余:4
boy2,吃了一个番茄,盘子里剩余:3
boy2,吃了一个番茄,盘子里剩余:2
boy2,吃了一个番茄,盘子里剩余:1
boy2,吃了一个番茄,盘子里剩余:0

很明显出了问题,盘子里都没了你还再吃,都成负的了,肯盘子啊,这就引出一个问题,虚假唤醒

3 虚假唤醒

假设盘子里现在没有了,男孩1和2都在挂起等待,当女孩1生产完成一个后,通知你俩都能吃,当时请仔细看消费者代码,是一个if判断,也就是男孩被挂起等待的地方是一个if判断里面,当被唤醒的时候,他们俩就直接执行下面的吃的操作了,所以就会产生虚假唤醒问题,也可以看官网jdk文档 ,也有提过虚假唤醒,查看Object的wait方法便可看到

public final void wait()
                throws InterruptedException

Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. In other words, this method behaves exactly as if it simply performs the call wait(0).

The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object's monitor to wake up either through a call to the notify method or the notifyAll method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.

As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:

     synchronized (obj) {
         while ()
             obj.wait();
         ... // Perform action appropriate to condition
     }
 

This method should only be called by a thread that is the owner of this object's monitor. See the notify method for a description of the ways in which a thread can become the owner of a monitor.

将if条件判断改为while就可以解决问题,为什么呢,其实很好理解,当男孩1和男孩2都被唤醒后,他俩还是要先执行一下自身的while循环判断当前盘子里还有没有,没有的话继续等待,所有问题就解决啦

4 Lock的写法

Lock是并发包下的一个类,先写代码,在做总结

实现如下:

package productconsumer;

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

public class Demo2 {
    public static void main(String[] args) {
        Data2 data = new Data2();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.product();
            }
        }, "girl1").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.consumer();
            }
        }, "boy1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.product();
            }
        }, "girl2").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.consumer();
            }
        }, "boy2").start();
    }
}

class Data2 {
    //锁
    private ReentrantLock lock = new ReentrantLock();
    //
    private Condition condition = lock.newCondition();
    //盘子里的数量
    private int num = 0;

    /**
     * 生产者
     */
    public void product() {
        //上锁
        lock.lock();
        try {
            while (num >= 5) {
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + ",洗了一个,盘子里剩余:" + num);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            lock.unlock();
        }
    }

    /**
     * 消费者
     */
    public void consumer() {
        //上锁
        lock.lock();
        try {
            while (num <= 0) {
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + ",吃了一个,盘子里剩余:" + num);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

如果面试的时候让写一个生产者消费者模型,我觉得还是写第二种比较好,这样引出话题,往并发包下探讨呗。。

5 synchronized与Lock锁的区别

  1. synchronized是java内置的关键字,Lock是一个类,是java从1.5版本后引入的
  2. synchronized可以自动处理锁的释放问题,Lock需要手动释放,相当于一个自动挡一个手动挡
  3. synchronized无法判断是否获取到锁,lock可以判断是否获取到锁
  4. synchronized当线程1阻塞,线程2会一直等待下去,lock可以使用trylock,尝试获取锁,获取不到可以结束等待
  5. synchronized 可重入锁,不可中断,是飞公平锁。Lock是可重入锁,可中断,可以设置为公平锁

总的来说,简单的代码还是用synchronized来控制,现在jdk对于synchronized的优化了很多,但是如果你想更精准的控制线程或者一些发杂的场景下使用还是lock比较好用,记得一位大佬说过的话:新技术的出现肯定有存在的道理,不能因为习惯而不去接受它。

你可能感兴趣的:(后端开发)