白话JUC--Queue体系--LinkedBlockingQueue

廉价感情. 提交于 2020-01-10 15:37:59

首先看一下LinkedBlockingQueue继承关系
在这里插入图片描述

LinkedBlockingQueue简介

LinkedBlockingQueue是由链表实现的阻塞队列

    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; }
    }

因为每个节点会将元素封装成一个Node,从而实现链表结构,元素按照 FIFO 的形式来访问,队列头部为待的时间最久的元素,尾部则是最少,新元素插在尾部。

LinkedBlockingQueue源码分析

LinkedBlockingQueue主要成员变量

    /** The capacity bound, or Integer.MAX_VALUE if none */
    队列容量
    private final int capacity;

    /** Current number of elements */
    元素数量
    private final AtomicInteger count = new AtomicInteger();

    /**
     * Head of linked list.
     * Invariant: head.item == null
     */
    头节点,不存数据
    transient Node<E> head;

    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    尾节点,入队
    private transient Node<E> last;

    /** Lock held by take, poll, etc */
    出队锁
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    出队等待条件
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    入队锁
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    入队等待条件
    private final Condition notFull = putLock.newCondition();

通过几个主要成员分析,我们知道LinkedBlockingQueue是利用链表存储元素的,利用两个重入锁保证并发安全

LinkedBlockingQueue构造方法

    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

可以发现如果没有执行大小,那么队列的大小是Integer.MAX_VALUE的大小

初始化的同时初始化last和head,初始化一个空的节点

LinkedBlockingQueue添加元素

LinkedBlockingQueue–offer

add实际上调用子类的offer方法添加数据

    public boolean offer(E e) {
    	// 判断是否为空
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        // 如果队列已经满了,返回false
        if (count.get() == capacity)
            return false;
        int c = -1;
        // 封装成Node节点
        Node<E> node = new Node<E>(e);
        // 获得putLock锁
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
        	// 判断元素的个数是否小于容量
            if (count.get() < capacity) {
            	// 如果小于,则将node添加到链表中
                enqueue(node);
                // 获得元素数据,并进行增加
                c = count.getAndIncrement();
                // 如果发现增加后的数量小于容量
                if (c + 1 < capacity)
                	// 唤醒notFull,通知入队可以继续添加数据
                	// 因为此时判断还没有达到队列最大容量
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        // 此时至少有一个元素放入到队列中了
        if (c == 0)
        	// 唤醒
            signalNotEmpty();
        return c >= 0;
    }

添加到链表中

    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        // 从右往左赋值,将node的值给last节点的next
        // 同时更新last节点为最新的node节点
        last = last.next = node;
    }

在判断c == 0的时候,说明此时至少有一个元素放入到队列中了,就可以唤醒取操作去取出元素

    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        // 获得takeLock锁
        takeLock.lock();
        try {
        	// 通知取操作可以继续取元素
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

通过putLock的方式保证了操作队列尾部增加元素的原子性,同时如果插入成功会返回true,如果已经满了则丢弃当前元素返回false,所以这个方法是非阻塞的方法

LinkedBlockingQueue–put

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        // 封装成node
        Node<E> node = new Node<E>(e);
        // 获取putLock锁
        final ReentrantLock putLock = this.putLock;
        // 获取元素个数
        final AtomicInteger count = this.count;
        // 加锁,此锁可以被中断,如果在加锁的过程中被其他线程设置了中断标志,则会抛出InterruptedException异常,从阻塞中唤醒
        putLock.lockInterruptibly();
        try {
			// while循环判断元素个数是否等于队列大小
			// 如果相等,则说明队列已经满了,则阻塞在notFull上
			// 为什么要使用while循环,因为方式notFull虚假唤醒,导致已经满了的队列,依然往里面添加
            while (count.get() == capacity) {
                notFull.await();
            }
            // 添加元素
            enqueue(node);
            // 获取并增加元素数量
            c = count.getAndIncrement();
            // 判断是否小于队列数量
            if (c + 1 < capacity)
            	// notFull唤醒,通知其他添加线程可以向队列中添加数据
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        // 通知其他出队线程可以取出数据
        if (c == 0)
            signalNotEmpty();
    }

通过源码分析可以知道,如果队列有空间,则添加后直接返回,但是如果队列已经满了,则阻塞在notFull条件上,直到有其他线程唤醒,所以是阻塞的方法

在这里插入图片描述

LinkedBlockingQueue–poll

    public E poll() {
        final AtomicInteger count = this.count;
        // 如果元素个数为0的话,直接返回null
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        // 获得takeLock锁
        final ReentrantLock takeLock = this.takeLock;
        // 加锁
        takeLock.lock();
        try {
        	// 如果数量大于0,说明队列中还有元素
            if (count.get() > 0) {
            	// 取出元素
                x = dequeue();
                // 获取元素数量,并减少一
                c = count.getAndDecrement();
                // 如果发现移除完元素,还有元素,那么就通知其他取出线程,可以继续取出元素
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        // 判断之前的元素个数是否与队列大小相等,如果相等,说明之前队列是满的
        // 移除一个元素后至少会有一个空位置,这时可以调用signalNotFull方法,通知添加线程可以继续添加元素
        if (c == capacity)
            signalNotFull();
        return x;
    }

通知添加线程可以继续添加元素

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        // 获取head节点
        Node<E> h = head;
        // 获取头节点的下一个元素,为新的头节点
        Node<E> first = h.next;
        // 将之前的头结点断开,将自己的引用赋给自己的next,此对象则不可达,垃圾回收会回收这个对象
        h.next = h; // help GC
        // 将新的first节点,也就是之前的next节点,赋给head节点
        head = first;
        // 返回之前next节点的item,也就是现在的first的item
        E x = first.item;
        // 将此时的item设置为null
        first.item = null;
        return x;
    }

在这里插入图片描述
实际上head不负责保存数据,只是起到一个哨兵作用,实际返回的时候会返回head的next的item元素

而且poll操作,如果队列为空则返回null,这个方法是不阻塞的

LinkedBlockingQueue–take

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        // 获取takeLock锁
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
        	// 判断元素数量是否为0,如果为0,说明为空,阻塞在notEmpty条件上
            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;
    }

与poll方法类似,获取队列中的头部元素,并从队列中移除,如果没有元素,则阻塞在notEmpty条件上,直到队列不为空,返回元素。

LinkedBlockingQueue–remove

    public boolean remove(Object o) {
        if (o == null) return false;
        // 对整个队列进行加锁
        fullyLock();
        try {
        	// 从head开始遍历
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {
                // 如果发现p中的元素和需要移除的元素相等,说明找到了这个需要移除的元素
                if (o.equals(p.item)) {
                    unlink(p, trail);
                    return true;
                }
            }
            return false;
        } finally {
        	// 整个队列解锁
            fullyUnlock();
        }
    }

    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }

    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }
    void unlink(Node<E> p, Node<E> trail) {
		// 将p的item设置为null
        p.item = null;
        // trail为p的上一个元素节点,将trail的next设置为p的next
        // 所以就掠过了p这个元素
        trail.next = p.next;
        // 如果发现p是last节点,则重新设置last节点为trail
        if (last == p)
            last = trail;
        // 获得元素个数并减一,如果和队列大小相等
        // 此时可以唤醒添加线程继续添加元素,因为上面已经移除掉一个元素了
        if (count.getAndDecrement() == capacity)
            notFull.signal();
    }

所以移除一个线程就是将上个一元素的next设置为这个元素的next

在这里插入图片描述
同样的remove方法是非阻塞的,如果队列中有则返回true,如果没有则返回false

LinkedBlockingQueue–总结

  • LinkedBlockingQueue不允许添加null元素
  • LinkedBlockingQueue是一个基于链表的队列,并且是一个FIFO队列
  • LinkedBlockingQueue如果不指定队列的容量,默认容量大小为Integer.MAX_VALUE,有可能OOM
  • LinkedBlockingQueue入队和出队操作采用了不同的锁来进行控制,这样入队和出队操作可以同时进行。但同时只能有一个线程可以进行入队或出队操作
  • LinkedBlockingQueue内部采用的是可重入独占的非公平锁(ReentrantLock默认是非公平的锁),通过锁进行添加和取出的同步
  • LinkedBlockingQueue通过原子变量count来记录元素个数(这里稍微说一下,因为添加和取出的时候都会对count进行修改,而且整个操作是原子性的,所以可以通过count返回队列中元素的个数,如果整个操作不是原子性的,则不能通过count返回队列中的元素个数)

LinkedBlockingQueue&ArrayBlockingQueue对比

  • ArrayBlockingQueue底层是数组,容量有限制,初始化时需指定;LinkedBlockingQueue底层是链表,可以不指定大小,容量是int的最大值
  • ArrayBlockingQueue维护一把锁,同一时刻只能有一个线程操作队列;LinkedBlockingQueue维护两把锁,同一时刻可以有两个线程在两段操作,互补干扰
  • LinkedBlockingQueue中类似于remove的方法,需要对整个队列进行加锁
  • LinkedBlockingQueue的吞吐量要高于 ArrayBlockingQueue

LinkedBlockingQueue在线程池中使用

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

newFixedThreadPool使用的就是LinkedBlockingQueue队列,原因就是因为它的容量无限大(int的最大值),如果任务非常忙的时候,使用一个有界队列来处理,可能很快就满了,触发拒绝策略,而使用LinkedBlockingQueue队列,可以很好的适应任务繁忙的情况,即使任务非常多,也可以扩容,当处理完成之后,节点也会被清除掉,非常灵活

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!