当前位置: 首页 > >

源码解析:ArrayBlockingQueue和LinkedBlockingQueue的区别

发布时间:

???????? 对于ArrayBlockingQueue和LinkedBlockingQueue已经都只是知道如何使用以及他们各自的特点,没有去看看源码到底驶入实现的!因为ArrayBlockingQueue和LinkedBlockingQueue是java开发中常常用到的数据结构,所以有必要看看源码深入一下.下面从源码来了解他们各自的特点,最后再做一下总结.


???????? (一)ArrayBlockingQueue:下面一步一步来解析.


首先来看看ArrayBlockingQueue继承了谁和实现来那些接口:


public class ArrayBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
?????????????? 从上面的源码可以知道ArrayBlockingQueue继承了AbstractQueue,并且实现了BlockingQueue和Serializable接口.



从ArrayBlockingQueue名就可以知道,其内部对于数据的存储采用了Array数组方式,所以在ArrayBlockingQueue类中有如下定义:

/** The queued items 使用这个数组items来对数据排队列*/
final Object[] items;

/** items index for next take, poll, peek or remove 记录对下次获取队列数组中数据的下标索引*/
int takeIndex;

/** items index for next put, offer, or add 记录对下次装入数据到队列数组中的下标索引*/
int putIndex;

/** Number of elements in the queue 记录队列中数据总量,即数组中数据总量*/
?int count;


????????? 上面定义的数组items就是用来保存数据的.


????????? 当通过take或者poll获取数据的时候其实都是从这个数组中取出数据的,这两个方法最后都会调用dequeue()这个方法取出数据:



private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;//获取存储数据的数组
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];//通过数组下标索引takeIndex来获取数组中的数据元素
items[takeIndex] = null;//清楚掉这个索引下的数据
takeIndex = inc(takeIndex);//将takeIndex自加1来计算出下一次获取数据的位置(数组索引)
count--;//数组中存储的数据数量更新:减1
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); //通知其他处于等待状态的线程:当往这个队满的时候,往这个队列里面加入数据就可以阻塞,这里就通知被阻塞的线程可以操作了.
return x;//返回获取的数据
}
????? 然后来看看enqueue这个方法,和上面的dequeue方法相反,enqueue是用来往队列里面加入数据的最终执行方法,所以offer和put都是最终调用dequeue来实现其功能的.



private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
items[putIndex] = x;//直接把寻要加入队列的数据放入数组的putIndex位置中
putIndex = inc(putIndex);//更新putIndex的值(自加1的方式)
count++;//更新存储数据的数量:自加1
notEmpty.signal();//通知等待的线程(阻塞):当队列为空的时候,获取一个数据就会等待(阻塞),这里就是通知被阻塞的线程,已经有数据了可以开始.
}
????? 通过上面的方法可以知道,往队列里面加入数据和读取数据其实就是从一个数组里面获取数据的加入数据.



?????? 对上面的方法分析,可以得出ArrayBlockingQueue对其数组的管理方式:


?????? (1)takeIndex和putIndex以及count都为0


?????? (2)加入数据时候,takeIndex加1,count加1


?????? (3)取出数据时候putIndex加1,count减1


?????? (4)每次往队列里面加入数据或取出数据都先判断 count的大小,如果count==0则取出数据可能阻塞,如果加入数据时候count==数组长度,则加入数据可能阻塞.


?????? (5)这样对数组的操作结果是:takeIndex和putIndex一直往前移,到数组最大长度时候回到0位置;在这个循环移动过程中takeIndex一直在putIndex前面,如果位置相同那么count就等于0.也就是说:在对这个队列操作过程中putIndex一直在追逐takeIndex,一旦追到那么队列就没有任何存储数据了.


这里来看看ArrayBlockingQueue是如何实现线程安全的.


??????????? ArrayBlockingQueue的线程安全和LinkedBlockingQueue一样,都是采用了重入锁(ReentrantLock)来实现的,只是具体实现方法有区别.在ArrayBlockingQueue中定义来一个ReentrantLock和他的两个Condition:



/** Main lock guarding all access */
final ReentrantLock lock;

/** Condition for waiting takes 唤醒处于阻塞状态的线程*/
private final Condition notEmpty;

/** Condition for waiting puts 唤醒处于阻塞状态的线程*/
private final Condition notFull;
????? 这里解释一下Condition,我的理解是:Condition是存在于一个的ReentrantLock,用于调度那些因为ReentrantLock而阻塞的线程!在ReentrantLock中,我们可以通过Condition来阻塞线程使其处于等待状态,同时可以通Condition来唤醒处于等待状态的线程.


????? 这里ArrayBlockingQueue正是使用了Condition来实现其阻塞队列的.我们以put方法为例子来说明Condition的使用:



public void put(E e) throws InterruptedException {
checkNotNull(e);//首先判断加入的数据是否为null,如果是null则会抛出异常NullPointerException
final ReentrantLock lock = this.lock;//准备使用重入锁
lock.lockInterruptibly();//使用可以中断的方式获取锁
try {
while (count == items.length)//这里判断是否队列已满,实际上就是数组是否已经存储满数据
notFull.await();//如果已经满,则会使用Condition来阻塞这次操作,阻塞这个线程.处于等待状态
enqueue(e);//否则往数组里面加入数据
} finally {
lock.unlock();//释放锁
}
}
?????
上面的解释可以知道:一旦队列数据满,就调用notFull的await()来阻塞这次操作,直到dequeue()方面里面notFull调用signal()来告知队列的数据不是满的时候才会解除阻塞.

? ? ?
同理,在调用take方法获取数据时候,也是一样,首先判断存储的数据数量是否为0,是的话就会使用notEmpty阻塞,直到enqueue方法调用notEmpty调用signal来告知不是空的为止.


? ? ?


????? 通过上面的分析可以知道,当加入数据和取出数据的时候,一旦不满足相应的条件就会阻塞,直到被唤醒.那么只是希望阻塞一段时间如何实现的呢?其实Condition有一个方法就是来实现阻塞一段时间awaitNanos();下面来看看offer(E e, long timeout, TimeUnit unit)是如何实现的:



public boolean offer(E e, long timeout, TimeUnit unit)//往队列加入数据,如果阻塞,就等待一段时间
throws InterruptedException {

checkNotNull(e);//检测加入数据是否为null
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);//阻塞一段时间,更新nanos
}
enqueue(e);//加入数据
return true;//操作成功
} finally {
lock.unlock();//释放锁
}
}
?????? 从上面的分析可以对ArrayBlockingQueue得出如下结论:


? ? ?? (1) ArrayBlockingQueue采用数组来存储数据,由于采用双下标索引来指明加入的数据,所以不管是加入时间和取出数据效率都很高.直接从数组中取出!不需要查找,对数组数据移动等操作.


? ? ? (2)由于采用数组来存储数据,所以ArrayBlockingQueue的容量必须事先定义确认.


? ? ? (3)采用了重入锁ReentrantLock来事先多线程安全.


? ?? (二)LinkedBlockingQueue:下面一步一步来解析.


首先来看看ArrayBlockingQueue继承了谁和实现来那些接口:


? ? ?? public class LinkedBlockingQueue extends AbstractQueue implements BlockingQueue, java.io.Serializable {
?????? 很显然LinkedBlockingQueueArrayBlockingQueue一样.


LinkedBlockingQueue内部对于数据的存储是采用链表实现的.这是一个单项链表,并且使用两个链节点来标识链表的头和尾部,



static class Node {
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 next;

Node(E x) { item = x; }
}
??????? 上面是链表节点的定义.



/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();//原子常数,标记加入数据的数量.

/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node head; //链表的头,头节点存储的数据是为空的,不存储数据

/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node last;//链表的尾
???????? 上面是链表的头尾节点定义.

???????? 当通过take或者poll获取数据的时候其实都是从这个数组中取出数据的,这两个方法最后都会调用dequeue()这个方法取出数据:


private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node h = head;//获取链表头节点
Node first = h.next;//获取头节点下面第一个存储数据的节点
h.next = h; // help GC //让头节点的next指向自己:从这个链表中将头节点与链表分离
head = first; //把头节点的下一个节点作为新的头节点.
E x = first.item;//取出需要的这个数据:从这个新的头节点取出(实际就是原来的头节点后面第一个节点(第一个存储数据的节点))
first.item = null;//将头节点的数据修改成null,头节点不存储数据
return x;//返回取出的数据
}
?????? 从上面的分析可以知道:
每次从这个队列中取出数据实际上是从这个链表中取出数据的:从头节点寻找到下面的第一个节点,这个节点的数据就是需要获取的数据,并且移除头节点,把第一个保存数据的节点修改成新的头节点.

?????? 从上面的分析可以知道,
对比ArrayBlockingQueue的dequeue的方法,显然LinkedBlockingQueue的dequeue方法需要做更多工作,效率会低许多.


?????? 然后来看看enqueue这个方法,和上面的dequeue方法相反,enqueue是用来往队列里面加入数据的最终执行方法,所以offer和put都是最终调用dequeue来实现其功能的.



private void enqueue(Node node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
????? 加入数据很简单,就是从链表尾部加入,只需要操作尾部节点last.并且把尾节点改为这个加入的数据节点.这个方法比较简单,甚至比ArrayBlockingQueue还简单.不过实际上并不可以之对比enqueue就卡一判断他们加入数据的效率.

???? 所以我们接着看看put的方法:




public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();//检测加入的数据是否null
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node node = new Node(e);//为这个加入的数据创建一个链表节点的对象
final ReentrantLock putLock = this.putLock;//获取重入锁
final AtomicInteger count = this.count;//获取记录加入数据数量的原子常数
putLock.lockInterruptibly();//获取可以中断的锁
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {//判断是否已经满了,
notFull.await();//阻塞,等待
}
enqueue(node);//加入这个数据
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();//如果数量没有满,则唤醒因为数据满而在等待的任务线程
} finally {
putLock.unlock();//是否锁
}
if (c == 0)
signalNotEmpty();//如果存储的数据为null,没有存储数据,则唤醒因为没有任何数据而在等待的任务线程
}
??????? 从 上面代码可以看出,加入一个数据,还需要创建一个链表节点的对象.而ArrayBlockingQueue在加入数据时候不需要创建额外的对象.


?????? 结论:在加入数据时候,ArrayBlockingQueue效率会高一点!只是对数组操作效率高,而LinkedBlockingQueue还需要创建额外的对象,并且使用AtomicInteger变量来存储保存数据的数量.


这里来看看LinkedBlockingQueue是如何实现线程安全的. 在LinkedBlockingQueue中定义了两个ReentrantLock,分别对获取数据和加入数据来同步,因为在ReentrantLock中加入数据和获取数据是对队列的链表的头节点和尾节点操作的,所以是需要两个重入锁的.此外,由于加入数据和获取数据都需要更新存储数据的数量,所以保存存储数据数量的变量使用了原子常数:? private final AtomicInteger count = new AtomicInteger();下面来看看获取数据的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) {//存储数据数量为0
notEmpty.await();//阻塞,,等待
}
x = dequeue();//获取数据
c = count.getAndDecrement();//自加1
if (c > 1)
notEmpty.signal();//通知存储数据不为空
} finally {
takeLock.unlock();//释放锁
}
if (c == capacity)
signalNotFull();//通知存储数据已经满
return x;//返回获取的数据
}
?????
对比LinkedBlockingQueue和ArrayBlockingQueue区别结论:


????? (1)都是阻塞队列.


????? (2)存储数据方式不一样:ArrayBlockingQueue使用数组方式,并且使用两个下标来表明取出数据的位置和加入数据的位置.


????????? LinkedBlockingQueue使用的是单项链表方式存储数据,使用头和尾节点来指明链表取出数据和加入数据的地方,并且头节点不存储数据.


???????? 都通过变量来记录存储数据的数量:ArrayBlockingQueue使用int变量来记录存储数据数量,而LinkedBlockingQueue使用线程安全的AtomicInteger来记录数据数量,很显然AtomicInteger的效率更低.


??? (3)由于ArrayBlockingQueue采用数组方式存储数据,所以其最大容易是在定义ArrayBlockingQueue的时候就已经确定的.不能再次修改.


??????? 而LinkedBlockingQueue采用链表存储数据,所以其容易可以不用指定.


??? (4)向对来说,由于ArrayBlockingQueue采用数组来存储数据,所有在加入数据和获取数据时候效率都会更高.


??? (5)都是使用ReentrantLock来实现线程安全,不过LinkedBlockingQueue采用了两个重入锁,并且使用了 AtomicInteger,所以相对来说实现同步ArrayBlockingQueue更简单效率更高.



友情链接: