BlockingQueue
对于Queue而言,blockingqueue是主要的线程安全的版本,具有阻塞功能,可以允许添加、删除元素被阻塞,直到成功为止,blockingqueue相对于Queue而言增加了两个方法put/take元素。
BlockingQueue接口
属于并发容器中的接口,在java.util.concurrent包路径下。
BlockingQueue不接受null元素,加入尝试通过add\put、offer等添加一个null元素时,某些实现上会抛出nullpointExeception问题。
BlockingQueue是可以指定容量,如果给定的数据超过给定容量,便无法添加元素,如果没有指定容量约束,最大大小是Interger.MAX_VALUE值。
BlockingQueue实现类主要用于生产者-消费者队列,另支持Collection接口。
BlockingQueue实现了线程安全,所有排队方法都可以使用内部锁或者其他并发控制形式来达到线程安全的目的。
三个主要实现类介绍:
ArrayBlockingQueue:有界阻塞队列
LinkedBlockingQueue:无界阻塞队列
SynchronousQueue:同步队列
ArrayBlockingQueue:有界队列
ArrayBlockingQueue有界队列底层实现是数组,数组大小是固定的,假如数组一端为头,另一端为尾,那么头和尾构建一个FIFO队列。
ArrayBlockingQueue是一个阻塞的队列,继承了AbstractBlockingQueue,间接的实现了Queue接口和Collection接口。底层以数组的形式保存数据,所以它是基于数组的阻塞队列。ArrayBlockingQueue是有边界值的,在创建ArrayBlockingQueue时就要确定好该队列的大小,一旦创建,该队列大小不可更改。内部的全局锁是使用的ReentrantLock。
- add(E e):把 e 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true,否则报异常。
offer(E e):表示如果可能的话,将 e 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回true,否则返回 false 没有间,则调用此方法的线程被阻断直到 BlockingQueue 里面有空间再继续 。
poll(time):取走BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null 。(ArrayBlockingQueue中数据不能为null)
take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到。
Blocking 有新的对象被加入为止 remainingCapacity():剩余可用的大小。等于初始容量减去当前的 size。
属性及默认值
//存储的数据 存放在数组中
final Object[] items;
//读数据位置
int takeIndex;
//写入数据位置
int putIndex;
//数据数量
int count;
//队列同步相关属性
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
通过ArrayBlockingQueue数据结构可知:首先是有一个数组 T[],用来存储所有的元素,由于ArrayBlockingQueue最终设置为一个不可扩展大小的Queue,所以这里items就是初始化就固定大小的数组(final),另外有两个索引,头索引takeIndex,尾索引putIndex,一个队列的大小count,要阻塞的话就必须用到一个锁和两个条件(非空,非满),这三个条件都是不可变类型。
因为只有一把锁,所以任意时刻对队列只能有一个线程,意味着索引和大小的操作都是线程安全的,所以可以看到takeindex等不需要原子操作和volatile语义了。
构造函数:
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();
}
//通过初始容量capacity、公平性标志fair和集合c
public ArrayBlockingQueue(int capacity, boolean fair,Collection<? extends E> 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) {
//数据是不能为null
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
put操作
可阻塞的添加元素
public void put(E e) throws InterruptedException {
//检测插入数据不能为null
checkNotNull(e);
//添加可中断的锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) //容量满了需要阻塞
notFull.await();
//当前集合未满,执行插入操作
insert(e);
} finally {
//释放锁
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
//通知take操作已经有数据了,如果有take方法阻塞,此时可被唤醒来执行take操作
notEmpty.signal();
}
//循环数组的特殊标志处理 ,如果是到最大值则重定向到0号索引
final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
插入操作,在队列满的情况下会阻塞,直到有数据take出队列时才能结束阻塞,将当前数据插入队列。
take方法
将数据从队列中移除
public E take() throws InterruptedException {
//添加可中断的锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) //队列中没有数据时,需要阻塞,直到有数据put进入队列通知该操作可以继续执行
notEmpty.await();
//有数据时
return extract();
} finally {
//释放锁
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
E x = this.<E>cast(items[takeIndex]);
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
//发出通知 通知put方法,唤醒put操作
notFull.signal();
return x;
}
ArrayBlockingQueue特点:
1、底层数据结构是数组,且数组大小一旦确定不可更改
2、不能存储null
3、阻塞功能是通过一个锁和两个隶属于该锁的Condition进行通信完成阻塞
总结:ArrayBlockingQueue 是一种循环队列,通过维护队首、队尾的指针,来优化插入、删除,从而使时间复杂度为O(1)。
LinkedBlockingQueue:无界队列
1、底层数据结构
2、阻塞特征如何实现?有没有锁,有几把锁?
LinkedBlockingQueue有两个lock锁和两个Condition以及用于计数的AtomicInteger。
底层数据结构是链表,都是采用头尾节点,每个节点执行下一个节点的结构。
数据存储在Node结构中。
引入两把锁,一个入队列锁,一个出队列的锁。满足同时有一个队列不满的Condition和一个队列不空的Condition。
为什么使用两把锁,一把锁是否可以?
一把锁完全可以的,一把锁意味着入队列和出队列同时只能有一个在进行,另一个必须等待释放锁,而从实际实现上来看,head和last是分离的,相互独立的,入队列实现是不会修改出队列的数据的,同理,出队列时也不会修改入队列的数据,这两个操作实际是相互独立,这个锁相当于两个写入锁,入队列是一种写操作,操作head,出队列是一种写操作,操作的是tail,这两是无关的。
从属性我们知道,每个添加到LinkedBlockingQueue队列中的数据都将被封装成Node节点,添加到链表队列中,其中head和last分别指向队列的头结点和尾结点。与ArrayBlockingQueue不同的是,LinkedBlockingQueue内部分别使用了takeLock 和 putLock 对并发进行控制,也就是说,添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量。
这里如果不指定队列的容量大小,也就是使用默认的Integer.MAX_VALUE,如果存在添加速度大于删除速度时候,有可能会内存溢出,这点在使用前希望慎重考虑。
另外,LinkedBlockingQueue对每一个lock锁都提供了一个Condition用来挂起和唤醒其他线程。
总结:
LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。它和ArrayBlockingQueue的不同点在于:
①队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
②数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
③由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
④两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
SynchronousQueue:同步队列
SynchronousQueue:同步队列:每个插入操作必须等待另一个线程的移除操作,同样,任何一个移除操作都要等待另一个线程的插入操作,因此次队列中其实没有任何一个数据,或者说容量为0,
SynchronousQueue更像一个管道,不像容器,资源从一个方向快速的传递到另一个方向。