利用jvisualvm.exe搞一个关于生产者消费者的另一些纠结的问题

Posted 不想下火车的人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用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,手动给消费制造一点延时,让它慢一点:

 

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

 

以上是关于利用jvisualvm.exe搞一个关于生产者消费者的另一些纠结的问题的主要内容,如果未能解决你的问题,请参考以下文章

关于Python的协程问题总结

用阻塞队列和线程池简单实现生产者和消费者场景

操作系统关于多线程同步中的死锁问题一篇文章让你彻底搞明白死锁到底是什么情况及如何解决死锁

java 多线程 22 :生产者/消费者模式 进阶 利用await()/signal()实现

java heap space以及jvisualvm.exe 工具

关于生产者/消费者/订阅者模式的那些事