zoukankan      html  css  js  c++  java
  • 利用jvisualvm.exe搞一个关于生产者消费者的另一些纠结的问题

      在利用jvisualvm.exe搞一个关于生产者消费者的一个纠结的问题中,我们已经看到如何在生产者消费者模型中,由于队列的不安全导致消费者一直空转的情况,并通过使用线程安全的队列去解决该问题。接下来我们继续跟踪该问题的其他几种并发情况,现在先把生产者代码中使消费者优先执行的关键那一行休眠注释掉,还是用LinkedList作为队列跑一下,结果又让我们大跌眼镜:

      是的,看起来很完美的日志,生产者也生产了,消费者也消费了,最后一个元素都出队入队了,但依然卡住了。祭出jvisualvm神器,发现消费者线程还是在空转:

      这次队列的收尾两个对象都是null,除此之前一切正常,日志显示其他对象被生产出来又被消费掉了。咋回事呢?我们还得看看LinkedList的源码:

        /**
         * Pointer to first node.
         * Invariant: (first == null && last == null) ||
         *            (first.prev == null && first.item != null)
         */
        transient Node<E> first;
    
        /**
         * Pointer to last node.
         * Invariant: (first == null && last == null) ||
         *            (last.next == null && last.item != null)
         */
        transient Node<E> last;

      注释告诉我们第一个节点和最后一个节点只存在两种情况:要么同时为null,要么本身不为空,但它的前一个(对第一个节点来说)或下一个(对最后一个节点来说)是null。怎么理解呢,这就得从LinkedList本身说起了:

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable

      我们看到它实现了Deque接口,它是Queue的子类,支持双向遍历,所以LinkedList是一个双向链表,既可以从first节点出发向后遍历,又可以从last节点向前遍历。就像一条双头蛇,两边都是头,两边都是尾。而且它支持null对象,所以就出现上面的情况。一图抵千言,把图中的head和tail换成LinkedList中的first和last,一样的说法:

      多跑几次,也许你还会碰到这种情况,开始生产者消费者步调是一致的:

       然而到了后来,只有生产者一个人在玩,消费者又去默默假死了:

       看看队列的情况:

      嗯,前面消费者正常的消费掉了一大部分,生产者的生产速度跟不上,队首first变成了null,所以它没得办法,只能去死循环,因为如下两段代码决定了一旦队列为空,就会再次进入消费者的拉取循环中:

        /**
         * Retrieves and removes the head (first element) of this list.
         *
         * @return the head of this list, or {@code null} if this list is empty
         * @since 1.5
         */
        public E poll() {
            final Node<E> f = first;
            return (f == null) ? null : unlinkFirst(f);
        }
    // 如果拉取到的对象是null,跳过继续拉取
                if (element == null) {
                    continue;
                }

      虽然后来生产者继续补充产品,但如果它无法告知消费者,那么也将无奈的于事无补了,因为消费者已经没法享用。只能把这1260个数字(除掉队首的null)永远的留在队列中。由此我们知道,只要某一时刻消费者赶上了生产的速度,一旦队列空了,那么消费者就会出现假死。

       我们算下,队列在8738时步调一致,队列为空,下一刻消费者进入空转,生产者继续工作,继续往队列中投入9999-8739=1260个数字。跟上面的堆内存中的队列个数可以对上。最后我们来看看在队列线程不安全的情况下,程序能正常运行的情况:1、生产者的生产速度比消费者消费速度快,保持队列永远有值;2、生产者的生产速度比消费者消费速度快或者保持一致,但优先生产,保持队列不为空,并提前完成生产,结束生产者线程,双线程变成单线程,后面就是消费者慢慢自己玩了。一旦出现消费者取到队首为null的情况,就可能陷入空转的泥潭不可自拔。

      我们实现上面的正常情况2,手动给消费制造一点延时,让它慢一点:

       可以看到,只需要给消费类加一点点的休眠时间,生产者就能先完成任务,消费者随后也完成了消费。

  • 相关阅读:
    MCU软件最佳实践——独立按键
    MCU软件最佳实践——矩阵键盘驱动
    MCU软件最佳实践——使用printf打印数据
    CAP定理图解证明
    类型和变量
    数字ID过长 精度丢失 (已解决:后端方案)
    Springboot 异步线程池配置(小型应用)
    Java 数字 字符串 简单操作
    Java 网络请求
    Java 时间 日 周 月 季 年
  • 原文地址:https://www.cnblogs.com/wuxun1997/p/13270056.html
Copyright © 2011-2022 走看看