利用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,手动给消费制造一点延时,让它慢一点:

 

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

 

posted on 2020-07-08 23:49  不想下火车的人  阅读(146)  评论(0编辑  收藏  举报

导航