JUC学习之生产者消费者案例(synchronized方式)

一、简介

生产者消费者问题是线程模型中的经典问题,生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。

本文我们将总结通过wait()和notify()多线程通信来实现生产者-消费者模式。

  • wait():阻塞线程,将线程加入到等待队列中,会释放锁;
  • notify()/notifyAll():唤醒一个或者多个等待队列中的线程,不会释放锁;

二、实现

示例:有两个线程,共同操作初始值为0的一个变量,实现一个线程对象对该变量加1,一个线程减1.

先看一下下面的代码:

/**
 * 生产者-消费者模式
 * 示例:有两个线程,可以操作初始值为0的一个变量,
 * 实现一个线程对象对该变量加1,一个线程减1.
 * <p>
 * 注意: 防止虚假唤醒问题
 */
public class T05_ProducerAndConsumer {
    public static void main(String[] args) {
        AirCondition airCondition = new AirCondition();

        //生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.incr();
            }
        }, "A").start();

        //消费者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.decr();
            }
        }, "B").start();
    }
}

/**
 * 共享资源类
 */
class AirCondition {
    /**
     * 共享变量
     */
    private int number = 0;

    /**
     * 加1操作
     */
    public synchronized void incr() {
        //1.判断
        if (number != 0) {
            try {
                //不为0的时候,阻塞
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //2.操作共享变量
        number++;
        System.out.println(Thread.currentThread().getName() + "->number:" + number);
        //3.通知消费者进行消费
        this.notifyAll();
    }

    /**
     * 减1操作
     */
    public synchronized void decr() {
        if (number == 0) {
            try {
                //为0的时候,阻塞
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "->number:" + number);
        //通知生产者生产
        this.notifyAll();
    }
}

运行结果:

A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
B->number:0

由上面的结果可见,似乎运行结果也没什么问题,但是此时只有一个生产者线程、一个消费者线程,假如我们再加多一个生产者和消费者,再观察运行结果:

public class T05_ProducerAndConsumer {
    public static void main(String[] args) {
        AirCondition airCondition = new AirCondition();

        //生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.incr();
            }
        }, "A").start();

        //消费者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.decr();
            }
        }, "B").start();

        //生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.incr();
            }
        }, "C").start();

        //消费者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.decr();
            }
        }, "D").start();
    }
}

运行结果:

A->number:1
B->number:0
A->number:1
D->number:0
C->number:1
B->number:0
A->number:1
B->number:0
A->number:1
D->number:0
C->number:1
B->number:0
A->number:1
B->number:0
C->number:1
A->number:2
D->number:1
B->number:0
A->number:1
D->number:0
A->number:1
C->number:2
B->number:1
B->number:0
C->number:1
A->number:2
D->number:1
B->number:0
A->number:1
D->number:0
C->number:1
B->number:0
C->number:1
D->number:0
C->number:1
D->number:0
C->number:1
D->number:0
C->number:1
D->number:0

仔细观察:A->number:2、C->number:2,出现了错误的数据,正常我们应该是+1、-1交替进行,不可能出现2的情况。这其实就出现了常见的多线程通信中虚假唤醒。

出现这个问题的根本原因就是在多线程通信中的wait()方法判断不能使用if,必须使用while进行判断。通过JDK官网我们也可以看到:

//As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
 //线程中断和虚假唤醒有可能发生,所以wait()方法 应该用到下面这样的循环中
 //if只会判断一次,而while会一直进行判断。
        synchronized (obj) {
         while (<condition does not hold>)
             obj.wait();
         ... // Perform action appropriate to condition
     }

下面我们修改一下代码:将if换成while判断,启动两个生产者两个消费者线程,观察运行结果。

/**
 * 生产者-消费者模式
 * 示例:有两个线程,可以操作初始值为0的一个变量,
 * 实现一个线程对象对该变量加1,一个线程减1.
 * <p>
 * 注意: 防止虚假唤醒问题
 */
public class T05_ProducerAndConsumer {
    public static void main(String[] args) {
        AirCondition airCondition = new AirCondition();

        //生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.incr();
            }
        }, "A").start();

        //消费者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.decr();
            }
        }, "B").start();

        //生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.incr();
            }
        }, "C").start();

        //消费者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                airCondition.decr();
            }
        }, "D").start();
    }
}

/**
 * 共享资源类
 */
class AirCondition {
    /**
     * 共享变量
     */
    private int number = 0;

    /**
     * 加1操作
     */
    public synchronized void incr() {
        //1.判断
        while (number != 0) {
            try {
                //不为0的时候,阻塞
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //2.操作共享变量
        number++;
        System.out.println(Thread.currentThread().getName() + "->number:" + number);
        //3.通知消费者进行消费
        this.notifyAll();
    }

    /**
     * 减1操作
     */
    public synchronized void decr() {
        while (number == 0) {
            try {
                //为0的时候,阻塞
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "->number:" + number);
        //通知生产者生产
        this.notifyAll();
    }
}

运行结果:

A->number:1
B->number:0
C->number:1
D->number:0
A->number:1
B->number:0
A->number:1
B->number:0
A->number:1
D->number:0
C->number:1
B->number:0
A->number:1
D->number:0
A->number:1
B->number:0
C->number:1
B->number:0
A->number:1
D->number:0
C->number:1
B->number:0
A->number:1
B->number:0
C->number:1
D->number:0
A->number:1
B->number:0
A->number:1
D->number:0
C->number:1
B->number:0
C->number:1
D->number:0
C->number:1
D->number:0
C->number:1
D->number:0
C->number:1
D->number:0

可见,0和1交替输出,这样也就避免了虚假唤醒问题。

虚假唤醒原因解析:

public synchronized void incr() {
    //假设A线程事先已经生产一次,此时number的值为1
    //由于多线程调度的原因,有可能下次进来的还是生产者线程
    //假设进来的是另外一个生产者线程C
    //此时判断1!=0,为true
    if(number != 0) {
        try {
            //C线程跑到这里,执行wait()后在这里等待 
            //注意:wait()方法会释放出锁,此时C就一直在这里阻塞着
            //这时候,在外面等待的线程,正常来说是希望消费者进来消费的,可是下一次进来的还可能是生产者
            //由于此时number=1,1!=0为true,此时生产者线程A又在这里等待着
            //所以此时,两个生产者线程A和C都阻塞在这里
            //因为两个生产者线程都进去了,所以下一次进来的肯定是消费者线程,假设有一个消费者进来将number修改为0了
            //消费者调用notifyAll()后,由于此时要注意线程A和线程C还在等待着,根据系统调度,会优先把执行权分配给等待时间比较久的线程
            //因为我们使用的是if判断,if只会判断一次,所以两个生产者线程A和C不会再次判断number!=0,导致两个生产者线程都往下执行
            //所以会出现number被A和C各加了1,故可能出现number = 2的结果,这就是虚假唤醒现象
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //2.操作共享变量
    number++;
    System.out.println(Thread.currentThread().getName() + "->number:" + number);
    //3.通知消费者进行消费
    this.notifyAll();
}

三、总结

在多线程通信中,注意判断的时候使用while,不能使用if,要防止发生虚假唤醒现象。本文主要总结了如何通过synchronized结合wait()和notifyAll()线程通信实现生产者消费者模式,下一节我们将会介绍使用Lock同步锁结合Condition实现生产者消费者的实现方式。

发布了220 篇原创文章 · 获赞 93 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/Weixiaohuai/article/details/104591792
今日推荐