ArrayBlockingQueue LinkedBlockingQueue阻塞队列

前言

    ArrayBlockingQueue和LinkedBlockingQueue都是JUC包下的阻塞队列,只是实现的方式不同,我们都清楚阻塞队列被运用在线程池中,用来存储要被执行的任务,除了被运用在线程池中,如果有场景需要保证任务被顺序执行,也可以单独的使用到阻塞队列,因为队列具有先入先出(FIFO)的特点


ArrayBlockingQueue主要实现代码结构

public class ArrayBlockingQueue extends AbstractQueue<E>
        implements BlockingQueue<E>,java.io.Serializable {
    /** 存储元素的数组结构 */
    final Object[] items;
    /** 出队的下标索引 */
    int takeIndex;
    /** 入队的下标索引 */
    int putIndex;
    /** 储存的元素数量 */
    int count;
    /** 控制入队和出队操作的可重入锁 */
    final ReentrantLock lock;
    /** 非空条件锁 */
    private final Condition notEmpty;
    /** 非满条件锁 */
    private final Condition notFull;
}

可以从主要实现的代码结构上看出:

    1. 采用数组储存元素

    2. 出队和入队都具有下标索引,控制着出队和入队的位置

    3. 使用一把ReentrantLock可重入锁来控制着入队和出队操作的并发访问

    4. 使用两个Condition条件锁来达到线程的等待和唤醒,当出队队列中的元素为空则调用notEmpty.await()进行等待,注意await()会释放当前锁,当有元素入队时,调用notEmpty.signal()唤醒前面等待的线程,注意signal()会重新获得锁,入队操作类似


构造方法

public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        // 注意的点,这里加锁,保证items的可见性,因为存在指令重排序问题,加锁完成对items数组的初始化
        lock.lock();
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }
public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        // 需要预先分配容量
        this.items = new Object[capacity];
        // 支持公平锁,非公平锁,默认非公平锁
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

注意的点:

    1. 必须传入数组初始容量,对JVM比较友好,不需要后面动态扩展数组内存大小

    2. 支持公平锁,非公平锁

    3. 如果传入了collection集合,则需要加锁完成对items数组的初始化,保证items的可见性,因为存在指令重排序问题(这里不在详细描述了)


入队操作

// 最终还是调用offer(E e)进行入队
public boolean add(E e) {
   return super.add(e);
}

// offer(E e)入队操作
public boolean offer(E e) {
        checkNotNull(e);
        // 进行加锁操作
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    		// 如果队列已满,则返回false入队失败,不会进行notFull.await()等待操作
            if (count == items.length)
                return false;
            else {
            	// 真正执行入队操作,将元素放到items中去
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
   
// 还提供了offer(E e, long timeout, TimeUnit unit)超时等待方法
public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        // 加锁,支持锁中断
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        	// 队列已满,则等待nanos纳秒
        	// 如果唤醒这个线程时依然没有空间且时间到了就返回false
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            // 真正执行入队操作
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

// 提供了put(E e)入队操作,一直等待直到入队
public void put(E e) throws InterruptedException {
        checkNotNull(e);
        // 加锁,支持锁中断
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        	// 队列已满,则调用notFull.await()等待,释放锁,重新被唤醒时,再获取到锁
        	// 被唤醒时,再次判断队列是否已满,还是已满,则还是等待
        	// 直到入队完成
            while (count == items.length)
                notFull.await();
            // 入队
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    
// 无论是offer还是put入队操作,调用的入队方法都是enqueue(E x)
private void enqueue(E x) {

        final Object[] items = this.items;
        // 将元素放到数组的入队下标索引
        items[putIndex] = x;
        // 判断入队下标索引是否达到数组长度,如果已经达到,则返回数组头部
        if (++putIndex == items.length)
            putIndex = 0;
        // 数量+1
        count++;
        // 唤醒非空的条件锁,需要唤醒之前等待出队的线程
        notEmpty.signal();
    }

    整体来说,入队操作的方法还是比较简单易懂的,需要注意的点是当队列已满,需要调用notFull.await()等待操作,当有元素从队列出队时,调用notFull.signal()唤醒操作


出队操作

public E poll() {
		// 加锁操作
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	// 判断队列是否为空
        	// 如果队列为空,则直接返回null,不会调用notEmpty.await()等待操作
        	// 如果队列不为空,则调用dequeue出队操作
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }
    
// 还提供poll(long timeout, TimeUnit unit)出队超时等待
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        	// 如果队列无元素,则等待nanos纳秒
        	// 如果下一次线程被唤醒但队列依然无元素且已超时就返回null
            while (count == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            // 执行出队操作
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
// 提供了take()出队操作,一直等待直到有元素出队 
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
// 无论是poll还是take出队操作,调用的出队方法都是dequeue(),完成出队
private E dequeue() {
       
        final Object[] items = this.items;
        // 获取到数组的出队下标索引位置的元素
        E x = (E) items[takeIndex];
        // 将数组的出队下标索引位置至为null
        items[takeIndex] = null;
        // 判断出队下标索引是否达到数组长度,如果已经达到,则返回数组头部
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        // 唤醒非满的条件锁,需要唤醒之前等待入队的线程
        notFull.signal();
        return x;
    }

    ArrayBlockingQueue主要的代码实现还是比较容易看懂的,基本上围绕着加锁,条件锁的等待和唤醒,数组下标的循环利用


LinkedBlockingQueue主要实现代码结构

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    /** 链表容量 */      
    private final int capacity;
    /** 链表元素的数量,需要采用原子类CAS操作完成++,--操作 */
    private final AtomicInteger count = new AtomicInteger();
    /** 链表的头节点,表示出队的指针 */
    transient Node<E> head;
    /** 链表的尾节点,表示入队的指针 */
    private transient Node<E> last;
    /** 用于控制出队的ReentrantLock可重入锁 */
    private final ReentrantLock takeLock = new ReentrantLock();
    /** 非空条件锁 从takeLock中获取*/
    private final Condition notEmpty = takeLock.newCondition();
    /** 用于控制入队的ReentrantLock可重入锁 */
    private final ReentrantLock putLock = new ReentrantLock();
    /** 非满条件锁 从putLock中获取 */
    private final Condition notFull = putLock.newCondition();
}

// 链表的节点Node类
static class Node<E> {
		// 节点的value元素
        E item;
        // 下一个节点
        Node<E> next;
        Node(E x) { item = x; }
}

从主要的实现代码结构上可以看出:

    1. 采用链表储存元素

    2. 具有头部和尾部指针,表示要出队和入队的节点Node位置

    3. 队列的数量采用原子类AtomicInteger, 完成计算

    4. 使用两把锁ReentrantLock分别控制出队和入队操作,出队和入队两个操作并不互斥,这也是跟ArrayBlockingQueue队列的最主要的区别,也是为什么count要采用原子类AtomicInteger进行计算了,因为入队和出队可以一起执行


构造方法

public LinkedBlockingQueue(Collection<? extends E> c) {
		// 链表默认的大小为 Integer.MAX_VALUE
        this(Integer.MAX_VALUE);
        // 锁住入队操作,完成节点的初始化
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); 
        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();
        }
    }

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

注意的点:

    1. 可以指定链表的容量,如果不指定则默认为Integer.MAX_VALUE

    2. 两个ReentrantLock都是非公平锁,不支持公平锁

    3. 传入collection集合,完成链表的初始化,需要putLock加锁控制完成对链表的初始化


入队操作

    这里大概主要讲一下put的具体操作,offer的入队操作类似

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        // putLock入队锁 加锁控制
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
        	// 如果链表元素已满,则调用notFull.await()进行等待
            while (count.get() == capacity) {
                notFull.await();
            }
            // 入队操作
            enqueue(node);
            // 链表数量通过CAS操作+1
            c = count.getAndIncrement();
            // 如果数量 < 容量 调用notFull.signal()唤醒等待入队的线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        // 表明之前队列是空,可能会存在等待出队的线程,这时候已经入队一个元素,则需要唤醒
        // c = count.getAndIncrement(); 得到的是还未累加之前的结果
        if (c == 0)
        	// 唤醒等待出队的线程
            signalNotEmpty();
    }
  
// 入队操作
private void enqueue(Node<E> node) {
		// 直接将尾部节点的next指针指向node节点
        last = last.next = node;
    }

    可以看出,LinkedBlockingQueue入队操作通过putLock加锁控制,这时不影响到出队操作,count累加采用CAS操作进行累加


出队操作

    重点分析下take()方法,poll出队操作类似

public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        // takeLock出队锁,进行加锁
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
        	// 如果队列为空,则需要进行等待
            while (count.get() == 0) {
                notEmpty.await();
            }
            // 出队
            x = dequeue();
            // 调用CAS操作进行递减
            c = count.getAndDecrement();
            // 如果递减之前的数量大于1,表明队列中还存在元素,则需要进行唤醒之前等到出队的线程
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        // 表明之前队列已满,可能会存在等待入队的线程,这时候已经出队一个元素,则需要唤醒
        if (c == capacity)
        	// 唤醒入队线程
            signalNotFull();
        return x;
    }

// 真正的出队操作
private E dequeue() {
		// h -> head头部节点,头部节点不存放数据
        Node<E> h = head;
        // 头部节点的下一个节点,第二个节点
        Node<E> first = h.next;
        // 头部节点的next指针指向自己,也就是断开与第二个节点的链接
        h.next = h;
        // 再让head指向第二个节点
        head = first;
        E x = first.item;
        // 将头部节点的item至为null,因为头部节点不存在数据
        first.item = null;
        return x;
    }

    可以看出入队和出队的源码还是比较简单易懂的,跟ArrayBlockingQueue的出队和入队的操作类似,只不过ArrayBlockingQueue采用数组存储元素,而LinkedBlockingQueue采用链表存储元素,ArrayBlockingQueue加锁控制使用一把锁,LinkedBlockingQueue则采用两把锁去分别控制出队和入队的


ArrayBlockingQueue和LinkedBlockingQueue的区别

    从上面的分析可以得出他们之间最主要的区别有以下几点:

        1. ArrayBlockingQueue采用数组进行存储,需要预先指定数组容量,对JVM比较友好,不需要后面JVM为其动态扩展其容量。LinkedBlockingQueue采用链表进行存储,储存元素的时候需要额外构造出Node节点,不需要预先指定链表大小,默认为Integer.MAX_VALUE,后面JVM需要为其动态的扩展内存空间

        2. ArrayBlockingQueue的入队和出队操作都采用一把ReentrantLock锁去控制,而LinkedBlockingQueue的入队和出队操作采用两把ReentrantLock锁去控制,入队和出队操作不互斥

        3. 在具有大量的出队和入队操作的场景下,ArrayBlockingQueue的性能要低于LinkedBlockingQueue的性能,就第2点讲的,ArrayBlockingQueue的出队和入队是互斥的,而LinkedBlockingQueue可以一边出队一边入队

    前面讲到在高并发场景下,ArrayBlockingQueue的性能要低于LinkedBlockingQueue,是因为ArrayBlockingQueue只采用一把锁去控制入队和出队的原因,那么是否ArrayBlockingQueue也能像LinkedBlockingQueue那样采用两把锁分别控制入队和出队操作了????

    我觉得也是可以的,ArrayBlockingQueue也可以做到像LinkedBlockingQueue那样采用两把锁去控制,如果采用两把锁去控制的话,那么ArrayBlockingQueue的count也要做成原子类AtomicInteger进行累加,累减操作。还有一点是LinkedBlockingQueue因为是链表储存,在入队时需要额外的构造新的Node节点,出队时需要改变Node节点的指针,效率没有ArrayBlockingQueue的数组对象来的快,所以LinkedBlockingQueue采用两把锁去提升效率,而在并发量不高的情况下,ArrayBlockingQueue的性能可能要比LinkedBlockingQueue高一些,因为ArrayBlockingQueue的不需要去操作额外的Node节点数据,count操作没有采用原子类的CAS操作,采用原生的++,--操作


总结

    ArrayBlockingQueue和LinkedBlockingQueue都是一种阻塞队列,只不过对应的实现方式而已,一个采用数组,一个采用链表,一个使用一把ReentrantLock锁去控制入队和出队操作,另外一个使用两把ReentrantLock锁去分别控制入队和出队操作,要选用那种队列的话,需要根据具体的业务场景去使用,像阻塞队列的话,JDK提供了7种阻塞队列,这只是其两种,剩下的5种:

  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
posted @ 2020-09-05 17:04  半分、  阅读(364)  评论(0编辑  收藏  举报