JUC-7.生产者消费者案例-虚假唤醒

1. 生产者和消费者案例

以下为生产者和消费者共享店员进行进货和售货的案例,生产者在库存已满时也不停进货,消费者在库存为0时也不停售货。
那么在实际生产中,就会造成生产者在库存已满时进货,造成数据丢失。消费者也会在不停的消费。

/** * 生产者和消费者案例 * @author xiaobin * @date 2018/3/11 */
public class TestProductAndConsumer {
    public static void main(String[] args) {
        //在没有使用模式时
        Clerk clerk = new Clerk() ;
        Productor pro = new Productor(clerk);
        Consumer cus = new Consumer(clerk);

        new Thread(pro, "生产者A").start();
        new Thread(cus, "消费者B").start();
    }
}

//店员
class Clerk {
    private int product = 0 ;

    //进货
    public synchronized void get() {
        if (product >= 10) {
            System.out.println("产品已满!");
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + ++product);
        }
    }

    //卖货
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("没货了!");
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + --product);
        }
    }
}

//生产者
class Productor implements Runnable {
    private Clerk clerk ;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }


    @Override
    public void run() {
        for (int i = 0 ; i < 20 ; i++) {
            clerk.get();
        }
    }
}

//消费者
class Consumer implements Runnable {
    private Clerk clerk ;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        for (int i = 0 ; i < 20 ; i++) {
            clerk.sale();
        }
    }
}

以上案例 输出结果。

...
生产者A : 9
生产者A : 10
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
产品已满!
消费者B : 9
消费者B : 8
消费者B : 7
消费者B : 6
消费者B : 5
消费者B : 4
消费者B : 3
消费者B : 2
消费者B : 1
消费者B : 0
没货了!
没货了!
没货了!
没货了!
没货了!
没货了!
...

2. 等待唤醒机制

2.1 解决以上问题

我们可以通过等待唤醒机制,解决上面问题。我们使用线程同步技术,只需要将店员的功能修改成如下便可以达到目的。

//店员
class Clerk {
    private int product = 0 ;

    //进货
    public synchronized void get() {
        if (product >= 10) {
            System.out.println("产品已满!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + ++product);
            this.notifyAll();
        }
    }

    //卖货
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("没货了!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + --product);
            this.notifyAll();
        }
    }
}

输出结果

生产者A : 8
生产者A : 9
生产者A : 10
产品已满!
消费者B : 9
消费者B : 8
消费者B : 7
消费者B : 6
消费者B : 5
消费者B : 4
消费者B : 3
消费者B : 2
消费者B : 1
消费者B : 0
没货了!
生产者A : 1
生产者A : 2
生产者A : 3
生产者A : 4

2.2 以上存在的问题

我们分析,上面案例其实是存在问题的,我们做些修改,把这个问题暴露出来。
修改如下:
– 生产者库存改为1
– 生产时增加0.2秒的延迟

//店员
class Clerk {
    private int product = 0 ;
    //进货
    public synchronized void get() {
        //修改库存为1
        if (product >= 1) {
            System.out.println("产品已满!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + ++product);
            this.notifyAll();
        }
    }
    //卖货
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("没货了!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " : " + --product);
            this.notifyAll();
        }
    }
}
//生产者
class Productor implements Runnable {
    private Clerk clerk ;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        for (int i = 0 ; i < 20 ; i++) {
            try {
                //给店员处理增加0.2秒的延迟。
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.get();
        }
    }
}

运行结果如下:

没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
没货了!
生产者A : 1
消费者B : 0
生产者A : 1
产品已满!

分析:
同时,我们发现程序并没有停止 !!!
生产者延迟了0.2秒,所以消费者是更快的。
问题出在哪?
我们现在是有20次生产的机会,假设只给二次机会生产者,同时只给消费者一次机会。
因为生产者sleep了,所以消费者先获取的锁,消费者获取锁之后,判断product < 0 进入wait()释放锁,生产者获得锁,进行生产,
此时product = 1,并通知消费者线程,消费者线程重新获得锁,直接从wait()代码开始执行,其实什么也没干,消费者就结束了,那么
生产者又进入第二次生产,发现库存满了,然后生产者进入等待。然后我们分析当前状态,消费者线程停了,并且一次都没有消费,库存
里仍然有1个,而且生产者认为库存满了,一直在等待消费线程通知。也就是说消费者,需要做二次循环才能消费一个资源。那么扩大到
20次循环,生产者只生产了10次,消费者就已经结束了。。上面输出结果,数一下其实就是大概这样,生产者阻塞在了第11次循环。

可以想个实际的例子,你去买烟,店员说柜台没货了,然后让你在原地等待,他打电话让人去仓库送货过来,送货人员货到了通知给店员,
店员居然不给你烟,直接让你走,请问你走不走啊?白来一趟吗

2.3 以上存在的问题解决方案

我们发现,消费者wait()中唤醒后,其实什么都没有做,因为下面的真正消费的代码是else了,但实际既然发起了一次消费,即使没货
消费者做了等待,就应该在等待之后做一次真正的消费,也就是和else里的工作一样。
所以我们做如下修改,去掉else,把里面代码放到外面执行就没有问题了

//店员
class Clerk {
    private int product = 0 ;

    //进货
    public synchronized void get() {
        //修改库存为1
        if (product >= 1) {
            System.out.println("产品已满!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //去掉else
        System.out.println(Thread.currentThread().getName() + " : " + ++product);
        this.notifyAll();
    }

    //卖货
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("没货了!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //去掉else
        System.out.println(Thread.currentThread().getName() + " : " + --product);
        this.notifyAll();
    }
}

最后整个程序就能正常结束了。

2.4 多个生产者和多个消费者产生虚假唤醒

上面生产者和消费者各一个时,确实没问题了,下面我们看看多个生产者和多个消费者的情况是怎样的。

new Thread(pro, "生产者A").start();
new Thread(cus, "消费者B").start();

new Thread(pro, "生产者C").start();
new Thread(cus, "消费者D").start();

结果如下:

生产者A : -23
生产者C : -22
生产者A : -21
生产者C : -20
生产者A : -19
生产者C : -18
生产者A : -17
生产者C : -16
生产者A : -15
生产者C : -14
生产者A : -13
生产者C : -12
生产者A : -11
生产者C : -10
生产者A : -9
生产者C : -8
生产者A : -7
生产者C : -6
生产者A : -5
生产者C : -4
生产者A : -3
生产者C : -2
生产者A : -1
生产者C : 0

结果有很多负数,什么原因呢???
假设生产者通知获取资源,第一个消费者获取到锁,发现没库存,进入等待,释放锁,结果被第二个消费
者抢到锁,第二个消费者发现没有库存,又释放锁。之后有两种可能,一种是又被第一个消费者获取到锁,
执行库存扣减,变成负数了;另一种可能被一个生产者获取到锁,库存做了一次加法,释放锁,后面至少
有一个消费者能正常消费。现在问题就出现在第一种情况。。消费者不停被唤醒,这就是是虚假的唤醒。

参考链接:http://blog.csdn.net/luckybug007/article/details/70053669

2.5 消除虚假唤醒

wait()方法注释有虚假唤醒的解决方法,就是把if换成while,在被唤醒后重新做一次判断,看是否是真的
满足唤醒自己的条件达到。

于是修改代码:

//店员
class Clerk {
    private int product = 0 ;

    //进货
    public synchronized void get() {
        //修改库存为1
        while (product >= 1) {
            System.out.println("产品已满!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //去掉else
        System.out.println(Thread.currentThread().getName() + " : " + ++product);
        this.notifyAll();
    }

    //卖货
    public synchronized void sale() {
        while (product <= 0) {
            System.out.println("没货了!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //去掉else
        System.out.println(Thread.currentThread().getName() + " : " + --product);
        this.notifyAll();
    }
}

结果,一切正常:

没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者A : 1
消费者D : 0
没货了!
没货了!
生产者C : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
产品已满!
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
没货了!
生产者A : 1
消费者B : 0
没货了!
没货了!
生产者C : 1
消费者D : 0
没货了!
生产者A : 1
消费者B : 0

2.6 另外:

线程在获取synchronized锁时,状态为blocked状态,在获取到synchronized锁内,主动通过wait释放锁,状态为waiting状态,
这两种等待队列不是同一个队列。所以上面调用notify,目的是为了唤醒waiting状态线程,但是同时notify后也释放了synchronized
锁,blocked的队列也有机会获取锁。

为了避免虚假唤醒,应该总是使用循环。

    原文作者:JUC
    原文地址: https://blog.csdn.net/xbliu564/article/details/79521360
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞