阻塞队列之LinkedBlockingQueue源码分析

LinkedBlockingQueue是一个基于单向链表实现的可选容量的阻塞队列,队列的头节点是等待时间最长的元素,队列的尾节点是等待时间最短的元素。新元素直接插入到尾节点的后面,成为新的尾节点,队列的检索操作在队列的头部获取元素。通常情况下,链表相比数组有更高的吞吐量,但是在大多数的并发应用程序中有不可预测的性能。LinkedBlockingQueue的构造方法可以设定容量大小,不指定容量大小,默认容量是Integer的最大值。

结构

LinkedBlockingQueue使用一个单向链表来实现队列,该队列至少有一个节点,头节点不存储元素。

    /**
     * Node节点类
     */
    static class Node<E> {
        E item;

        /**
         * - 后继节点node=head.next
         * - node为null表示head没有后继队列为空
         */
        Node<E> next;

        Node(E x) { item = x; }
    }
    /**
     * 队列中的头节点
     * head.item == null
     */
    transient Node<E> head;

    /**
     * 队列的尾节点
     * last.next == null
     */
    private transient Node<E> last;

属性

    /** 队列容量,最大值是Integer的最大值 */
    private final int capacity;

    /** 当前队列中元素的个数 */
    private final AtomicInteger count = new AtomicInteger();

    /** 出队的锁 */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** 当队列为空时保存出队的线程 */
    private final Condition notEmpty = takeLock.newCondition();

    /** 入队的锁 */
    private final ReentrantLock putLock = new ReentrantLock();

    /** 当队列满时保存入队的线程 */
    private final Condition notFull = putLock.newCondition();

LinkedBlockingQueue使用了两把锁,两个condition队列,一个是入队锁,一个是出队锁,在并发时,通过入队锁保证了线程入队列的串行化,在出队时,其中一个线程出队时,其他线程都将阻塞。虽然入队和出队同时只能有一个线程进行操作,但是可以同一时刻可以有两个线程共同操作,一个入队一个出队,所以在保证线程的时候,使用了AtomicInteger变量来表示当前队列中元素的个数,AtomicInteger是一个提供原子操作的Integer类,通过线程安全进行加减操作.这样就保证了入队和出队元素个数的一致性。相比于ArrayBlockingQueue来说,它极大了提高了吞吐量.在相同的场景下,使用LinkedBlockingQueue应该可以达到更好的效果。

构造方法

    /**
     * 默认构造创建一个容量是Integer的最大值的队列
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * 创建一个指定容量大小的队列,初始化链表,head=last=null
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    /**
     * 传入一个集合,遍历集合,当集合中的元素为null,直接抛出空指针异常,
     * 元素不为空时,直接把当前元素封装成一个node节点入队,如果队列中节点个数达到
     * 队列的最大容量,直接抛出异常 
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

put

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // 元素不能为null
     
        int c = -1;
        Node<E> node = new Node(e);  // 以当前元素新建node节点
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();  // 入队时使用响应中断锁
        try {
            // 当队列满了将线程放入condition队列等待
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);  // 入队操作
            c = count.getAndIncrement();  // 再次获取一下入队之前的元素个数
            if (c + 1 < capacity)  // 如果队列还有剩余容量,唤醒等待的入队线程
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)  // 如果队列本来为空,现在加入元素后,直接唤醒出队线程
            signalNotEmpty();
    }

添加元素首先进行非空判断,如果为空直接抛出空指针异常,然后把当前节点封装成一个node,此时进行加锁,把并行变成串行,只有一个线程才能进行添加操作,当队列满了的时候,让当前线程释放锁,进入condition等待队列,其他线程竞争到锁,如果队列仍然是满的,也会释放锁加入到condition队列,当有线程取出队列中的一个元素时,会发出唤醒信号,condition队列上的线程重新去竞争锁,继续进行入队操作,再次获取一下节点入队之前的元素个数,如果队列还有容量,直接唤醒notfull等待队列上的线程。最后再次判断添加元素之前队列是否为空,如果为空,现在添加了元素,就可以唤醒等待出队的线程。

    /**
     * 把当前节点追到到队列的尾节点
     */
    private void enqueue(Node<E> node) {
        last = last.next = node;
    }

看一下signalNotEmpty方法。

    /**
     * 使用take锁,释放notEmpty队列上的等待线程
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

offer方法与put的区别在于当队列满了的时候在进行添加元素直接返回false。offer(E e, long timeout, TimeUnit unit)方法和offer方法很相似,只不过是在给定时间内如果没有入队成功,直接返回false。

take

    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等待队列中
                notEmpty.await();
            }
            x = dequeue();  // 出队
            c = count.getAndDecrement();  // 再次获取一下出队之前的元素个数
            if (c > 1)    // 如果队列中还有元素,释放notEmpty等待队列上的线程
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)   // 再出队之前队列满了直接唤醒入队线程
            signalNotFull();
        return x;
    }

take和put的过程很相似,当队列为空,将线程加入到notEmpty的等待队列中,当队列不为空,就进行出队操作,再次确定一下出队前的元素个数,如果元素个数>1,唤醒notEmpty等待队列上的线程。最后再判断一下,如果队列本来就满了,现在正好取出了一个元素,唤醒入队的线程。

    /**
     * 出队时取队列的最前面的元素,要注意头节点是一个空节点
     */
    private E dequeue() {
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // 将当前头节点的引用指向自己,便于gc
        head = first; // 将头节点的next作为头节点,返回该节点的元素,并把将节点置为空
        E x = first.item;
        first.item = null;
        return x;
    }
    /**
     * 使用put锁,唤醒notfull队列上的线程
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

poll方法与take相比的不同在于当队列为空时直接返回null。poll(long timeout, TimeUnit unit)则是在指定的时间内没有获取到元素直接返回null。

peek

    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

peek是返回队列中head的下一个元素,并不是取出。

put和take可以允许两个线程在链表两端同时进行入队和出队操作,但是一端只能有一个线程执行,通过创建了两把锁来进行线程安全。入队和出队是相互联系的:当多线程中执行入队操作时,a线程抢占了putlock,添加了一个元素,此时正好队列满了,a线程释放锁,线程b获取了锁,但是马上释放锁,加入到notFull的等待队列,线程c加入到了notFull等待队列上b的后面。这时一个线程取出了一个元素,调用signalNotNull方法,通知notFull等待队列,线程b获取了锁,插入一个元素,如果这时检查发现还可以再添加一个元素,调用signal,唤醒c插入元素。出队与此过程相同。

remove

    public boolean remove(Object o) {
        if (o == null) return false;   // 要查找的元素为null,直接返回false
        fullyLock();  // 要同时上两把锁
        try {
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {   // 从头节点的下一个节点遍历
                if (o.equals(p.item)) {  // 如果找到直接移除,返回true
                    unlink(p, trail);
                    return true; 
                }
            }
            return false;    // 没找到返回false
        } finally {
            fullyUnlock();
        }
    }

remove操作的时候要同时上两把锁,这是因为要从头往后遍历,涉及到队列的两端,需要对两端加锁。

    void unlink(Node<E> p, Node<E> trail) {
        p.item = null;   // 把p置为null
        trail.next = p.next;  // 改变p的连接,便于gc
        if (last == p)    // 如果删除的是尾节点
            last = trail;  // 直接把p的前节点设为尾节点
        if (count.getAndDecrement() == capacity)  // 如果要删除前的队列满了,直接唤醒notfull等待队列上的线程
            notFull.signal();
    }

总结

LinkedBlockingQueue与 ArrayBlockingQueue是经常使用的阻塞队列,与ArrayBlockingQueue相比,它们都不允许存储null的元素,都是线程安全的队列。ArrayBlockingQueue的底层是基于数组实现的,必须要有容量限制,LinkedBlockingQueue是链表结构,容量是可以设置的,如果不设置,一直存放有可能造成内存溢出。ArrayBlockingQueue使用了一把锁和两个condition队列,同一时间只能由一个线程去操作,LinkedBlockingQueue使用了两把锁和两个condition队列,同一时间可以有两个线程操作,这样就提高了吞吐量。





猜你喜欢

转载自blog.csdn.net/qq_30572275/article/details/80413532