Jdk源码分析-LinkedBlockingQueue类

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wojiushiwo945you/article/details/78559020

背景

近日看了下阻塞队列的实现源码,基于原来对并发包的理解,发现自己很容易就能理清楚了里面的实现原理。就像学生时代生硬记住的那些古诗句,在成年时的某一天,面对桃花满枝桠的场景时,突然就想明白了”桃之夭夭,灼灼其华“的含义。

类图结构

这里写图片描述

阻塞链表队列,顾名思义:它是一种用链表实现、长度可控的FIFO队列,是链表和队列两种数据结构的结合,同时它支持阻塞。

根据类图结构,我们理一下它的实现逻辑

1 维护两个链表指针head、last,作为入队、出队操作的对象
2 维护两个可重复入锁和两个条件队列,分别控制入队操作和出队操作
3 put、take方法是一对,它们支持阻塞 ,且是线程安全的
4 offer、poll方法是一对,它们不支持阻塞,且是线程安全的
5入队操作受notFull条件限制,只有队列notFull时才可以入队;同时入队操作会唤醒notEmpty条件队列里阻塞的线程。(在队列为空的情况下,一旦有数据入队,队列就非空了)
6出队操作受notEmpty条件限制,只有队列notEmpty时才可以出队;同时出队操作会唤醒notFull条件队列里面阻塞的线程(在队列满的情况下,一旦有数据出队,队列就非满了)

链表结构

LinkedBlockingQueue类支持泛型,它定义了一个包内静态类Node,就是链表节点。

    static class Node<E> {
        E item;

        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node<E> next;

        Node(E x) { item = x; }
    }

如源码所示,它维护一个item成员存储当前节点的值,同时有一个next指针指向它的后继节点。

阻塞原理

LinkedBlockingQueue类的阻塞受两种条件的限制notFull和notEmpty,如果当前操作依赖的条件不满足,那么该操作就会被阻塞在对应的条件队列上。

put,入队操作,必须在队列没有满时执行,否则就调用notFull.await将当前调用者线程阻塞,直到队列非满。一旦入队成功,就调用notEmpty.signal,唤醒notEmpty非空条件队列里阻塞的线程。

take,出队操作,必须在队列非空时才能执行,否则就调用notEmpty.await将当前调用者线程阻塞,直到队列非空;一旦出队成功,就调用notFull.signal,唤醒notFull非满条件队列里阻塞的线程。

这两个操作会交叉影响两个限制条件,并通过条件的阻塞、唤醒操作实现阻塞功能的。

扫描二维码关注公众号,回复: 3180948 查看本文章

put操作流程

put操作是线程安全、且支持阻塞的。源码也很简洁:

   public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

put操作被putLock保护,同时受notFull条件限制,并且会影响notEmpty事件。
从源码中可看出,put操作是支持中断的,所以调用者必须处理InterruptedException异常。

take操作流程

take操作是线程安全、且支持阻塞的。源码与put操作相反:

   public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

take操作被takeLock保护,同时受notEmpty条件限制,并且会影响notFull事件。它也支持中断,所以调用者也必须处理InterruptedException异常。

源码启示录

看这个类的时候,我想到了两点:

首先,对于count计数操作,这里使用了原子类AtomicInteger,在队列长度发生变化时,以此来控制count的增减,可以保证操作的原子性。

其次,其次put和take操作为什么使用了两个锁呢?如果使用同一个锁,notFull和notEmpty作为定义在这个锁上的两个条件,这种实现有什么问题吗?

想了一下,发现必须得用两个锁,否则容易出现死锁:如果一个put操作在获取锁之后,发现队列满了,被阻塞掉,且没有释放锁;而另一线程操作take时无法获取到锁,就不能将put操作的线程唤醒,并发编程中出现这种情况就不好玩了。

猜你喜欢

转载自blog.csdn.net/wojiushiwo945you/article/details/78559020