JUC-并发编程-08-阻塞队列BlockingQueue之-ArrayBlockingQueue

1、概述

我们在并发编程中,通常需要线程安全的队列。线程安全的队列分为两种:

  1. 阻塞队列,使用锁来实现
  2. 非阻塞队列,使用CAS来实现。

阻塞队列在实际应用中非常广泛,许多消息中间件中定义的队列,通常就是一种“阻塞队列”。其使用场景一般是在 “生产者-消费者” 模式中,用于线程之间的数据交换或系统解耦。“生产者-消费者”这种模式中,“生产者” 和 “消费者” 是相互独立的,两者之间的通信需要依靠一个队列。这个队列就是要说的阻塞队列。引入“阻塞队列”的最大好处就是解耦,在软件工程中,“高内聚,低耦合”是进行模块设计的准则之一,这样“生产者”和“消费者”其实是互不影响的,将来任意一方需要升级时,可以保证系统的平滑过渡。如下图:

JUC-并发编程-08-阻塞队列BlockingQueue之-ArrayBlockingQueue_第1张图片

1.1 什么是阻塞队列

阻塞队列BlockingQueue是一个支持两个附加操作的队列。提供了一些阻塞方法,主要作用是:

  1. 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
  2. 支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空。

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列是生存者用来存放元素,消费者用来获取元素的容器。

JUC-并发编程-08-阻塞队列BlockingQueue之-ArrayBlockingQueue_第2张图片

JUC-并发编程-08-阻塞队列BlockingQueue之-ArrayBlockingQueue_第3张图片

 JDK提供了7种阻塞队列。如下:

  1. ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
  3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  4. DelayQueue:使用优先级队列实现的无界阻塞队列。
  5. SynchronousQueue:不存储元素的阻塞队列
  6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

本期我们学习ArrayBlockingQueue。

2、ArrayBlockingQueue

2.1 继承关系

JUC-并发编程-08-阻塞队列BlockingQueue之-ArrayBlockingQueue_第4张图片

2.2 属性介绍

//数组,存储数据
final Object[] items;
//为下次 take,poll,peek,remove操作提供下标
int takeIndex;
//为下次 put, offer, or add操作提供下标
int putIndex;
//元素之和
int count;
//掌管所有访问操作的锁。全局共享。都会使用这个锁。aqs原理
final ReentrantLock lock;
//非空条件
private final Condition notEmpty;
//非满条件
private final Condition notFull;

通过属性,我们得到几个重要信息

  1. 利用数组存储元素;
  2. 通过放指针和取指针来标记下一次操作的位置
  3. 利用重入锁来保证并发安全

2.3 构造函数

	//创建一个指定大小,默认访问策略(非公平锁)
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
	//创建一个指定大小,指定访问策略(非公平锁/非公平锁)
    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();
    }
	//创建一个指定大小,指定访问策略(非公平锁/非公平锁)并且指定给定集合元素初始化,以集合迭代器的顺序添加到阻塞队列中
    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        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();
        }
    }

2.4 操作方法

2.4.1 入队

入队有四种方法,他们分别是add(E e)、offer(E e)、put(E e)、offer(E e, long timeout, TimeUnit unit),它们有什么区别呢?

  • add(E e)
//如果可以在不超过队列容量的情况下立即执行此操作,则在此队列的尾部插入指定的元素,
//如果成功,则返回true,如果队列已满则抛出IllegalStateException异常。 
//如果元素为null则抛出空指针异常nullPointException
public boolean add(E e) {
        return super.add(e);
    }
//调用父类的add方法
 public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
//调用offer方法后面会写到
  • offere(e)
    /**
     * 在不超过队列容量的情况下立即执行此操作,请在此队列的尾部插入指定的元素; 
     * 成功返回{@code true},如果此队列已满则返回{@code false}。
     * 通常,此方法比方法add 更可取,后者只能通过引发异常而无法插入元素。
     */
 public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
  • put(e)
//将指定的元素插入此队列的末尾,如果队列已满,则等待空间变为可用。
public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
// 如果数组满了,使用notFull等待
// notFull等待的意思是说现在队列满了
// 只有取走一个元素后,队列才不满
// 然后唤醒notFull,然后继续现在的逻辑
// 这里之所以使用while而不是if
// 是因为有可能多个线程阻塞在lock上
// 即使唤醒了可能其它线程先一步修改了队列又变成满的了
// 这时候需要再次等待
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
  •  offer(E e, long timeout, TimeUnit unit)
//如果队列满了则等待一段时间后如果队列依然满就返回false; 
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 {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }
  • 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,因为入队了一个元素,肯定不为空。
        notEmpty.signal();
    }

2.4.2 出队 

出队也有有四个方法,它们分别是remove()、poll()、take()、poll(long timeout, TimeUnit unit);

  • remove() 调用AbstractQueue#remove
//检索并删除此队列的头。此方法与{poll}的不同之处仅在于:如果此队列为空,它将引发异常。
public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }
  • poll()
//没有就返回null
public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : 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(long timeout, TimeUnit unit)
//设置指定时间段,当没有元素等待一段时间再出队,否则返回null  
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
  • dequeue()

 private E dequeue() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }
  • remove(E e)  删除指定元素
//从此队列中删除指定元素的单个实例,如果存在,则将其删除。
 public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
void removeAt(final int removeIndex) {
        final Object[] items = this.items;
        if (removeIndex == takeIndex) {
            // removing front item; just advance
            items[takeIndex] = null;
            if (++takeIndex == items.length)
                takeIndex = 0;
            count--;
            if (itrs != null)
                itrs.elementDequeued();
        } else {
            // an "interior" remove
            // slide over all others up through putIndex.
            final int putIndex = this.putIndex;
            for (int i = removeIndex;;) {
                int next = i + 1;
                if (next == items.length)
                    next = 0;
                if (next != putIndex) {
                    items[i] = items[next];
                    i = next;
                } else {
                    items[i] = null;
                    this.putIndex = i;
                    break;
                }
            }
            count--;
            if (itrs != null)
                itrs.removedAt(removeIndex);
        }
        notFull.signal();
    }

2.5 总结

  1. ArrayBlockingQueue不需要扩容,因为初始化就指定容量,并循环使用数组
  2. ArrayBlockingQueue利用takeIndex和普通Index循环利用环形数组
  3. 入队和出队各自定义不同方法满足不同用途
  4. 利用重入锁和两个条件保证并发安全

其实BlockingQueue接口入队和出队的方法在实现类中思想都差不多。

JUC-并发编程-08-阻塞队列BlockingQueue之-ArrayBlockingQueue_第5张图片

你可能感兴趣的:(并发编程,数据结构与算法)