学习BlockingQueue之ArrayBlockingQueue实现原理
一:对列的基本概念
1:对列 队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行 删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受 限制的线性表。
进行插入操作的端称为队尾,进行删除操作的端称为队头。 在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。 因为队列只允许在一端插入,在另一端删除,
所以只有最早进入队列的元素才能 最先从队列中删除,故队列又称为先进先出(FIFO—firstinfirstout)线性表
2:阻塞对列
a)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程, 直到队列不满。
b)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队 列变为非空。
二:常见的阻塞对列类型
·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
以上的阻塞队列都实现了 BlockingQueue 接口,也都是线程安全的。
三:实现方式
1:ArrayBlockingQueue
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对 元素进行排序。默认情况下不保证线程公平的访问队列,所谓公平访问队列是指 阻塞的线程,
可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访 问队列的资格,
有可能先阻塞的线程最后才访问队列。初始化时有参数可以设置。
示例代码:
public class AnalysisBlockingQueue { public static void main(String[] args) { BlockingQueue blockingQueue = new ArrayBlockingQueue(1000000); new Thread(new MyProducer(blockingQueue),"producer-thread").start(); new Thread(new MyConsumer(blockingQueue),"consumer-thread").start(); } } class MyProducer implements Runnable{ private BlockingQueue blockingQueue; MyProducer(BlockingQueue blockingQueue){ this.blockingQueue = blockingQueue; } @Override public void run() { for(int i=0;i<100;i++){ System.out.println(Thread.currentThread()+"正在生产第 "+i+" 个数据"); blockingQueue.offer(i); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyConsumer implements Runnable{ private BlockingQueue blockingQueue; MyConsumer(BlockingQueue blockingQueue){ this.blockingQueue = blockingQueue; } @Override public void run() { while(true){ Object obj= blockingQueue.poll(); System.out.println("正在消费数据 "+obj); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } }
执行结果:
来看一下ArrayBlockingQueue这个对列的源码
这个构造函数,支持设置容量,对列对于线程并发来说默认是非公平的
当然支持配置,也可以配置为公平的,无论对于生产者还是消费者,多个生产者线程(无论是先阻塞的还是后来的线程,抢占
对列资源是不公平的,后来的线程也可以先把数据放到对列中)
还支持将集合中的元素提前放入对列中
ArrayBlockingQueue只使用了一把锁,生产者和消费者在同一时刻只能进行一个操作,
在构造的时候对这个锁进行初始化 ,公平和非公平也是通过这把锁控制的
对列常用的方法总结:
我们先来看一下用的比较多的offer和poll:
offer代码:
首先检查放进来的元素是否为null,如果为null,则抛异常,然后在操作的时候将这个对列上锁,这时其他的线程,包括
消费者线程和生产者线程都会阻塞,因为它是全部的锁,所以效率应该很低。
如果对列中元素的数量等于数组的容量,放不下了,返回false,否则调用enqueue向对列放入数据。
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(); } }
接下来看一下enqueue入队方法:
将元素放入数组中,如果此时放入的元素是数组的最后一个元素,那么下一次要从第一个开始放,
因为消费者取数据的时候是从数组低下标开始取的。对列维持的总数量count++,然后通知阻塞的消费线程
可以消费数据。
private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); }
再来看一下取数据的poll方法:
先上锁,进行操作,如果对列元素数量为0则返回null,否则调用dequeue取元素
public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { return (count == 0) ? null : dequeue(); } finally { lock.unlock(); } }
在看一下dequeue方法:
从数组中取出对应下标的元素,然后将该下标指向null,如果取数据的下标到达数组的最后一个元素,则下次从
0下标开始取,对列数量count--,然后通知阻塞的生产者可以向对列放数据了。
private E dequeue() { // assert lock.getHoldCount() == 1; // assert items[takeIndex] != null; 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; }
这里面有个peek方法,取出数组中的下一个元素,但是不从对列中移除
public E peek() { final ReentrantLock lock = this.lock; lock.lock(); try { return itemAt(takeIndex); // null when queue is empty } finally { lock.unlock(); } }
再来看一下 add remove 和element方法:
add直接调用父类的add方法,其实现依赖于offer方法,
上面我们分析offer放成功,返回true,失败返回false,add在offer基础上又包装了一下,成功返回true,失败抛异常。
remove方法:这个remove不一定是移除对列头部的元素,可以移除对列中的任何元素
如果传进来要删除的元素为null,或者对列为空,或者没有在对列中找到该元素,那么返回false,
删除成功,则返回true
再来看一下put和take方法:
如果对列已经满了,那么生产者线程阻塞,等待消费者线程通知,如果不是满的,那么直接入队。
public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); enqueue(e); } 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(); } }
总结ArrayBlockingQueue:是一个底层数组结构的对列,生产者和消费者线程由同一个锁控制,生产和消费效率低。
可以配置显示锁公平还是非公平。
下一节看一下LinkedBlockingQueue对列原理