阻塞队列
阻塞队列(BlockingQueue)
基本概念
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加操作支持阻塞的插入和移除方法。
即在多线程中,阻塞的意思就是,在某些情况下会挂起线程,一旦条件成熟,被阻塞的线程将会被自动唤醒。
- 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程直到队列不满
- 支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空
方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,util) |
检查方法 | element() | peek() | 不可用 | 不可用 |
阻塞队列一共有7种:
队列名 | 作用 |
---|---|
ArrayBlockingQueue | 由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 由链表结构组成的有界阻塞队列 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 使用优先级队列实现的延迟无界阻塞队列 |
SynchronousQueue | 不存储元素的阻塞队列,也就单个元素的队列 |
LinkTransferQueue | 由链表结构组成的无界阻塞队列 |
LinkBlockingQueue | 由链表结构组成的双向阻塞队列 |
阻塞的实现
ReentrantLock(锁)+Condition(条件状态)实现队列的阻塞,通过等待/通知机制。来实现线程之间的通信。
类似于Object的wait()和notify()。通过Synchronized,在锁中使用wait()和notify()达到线程之间的通信
ArrayBlockingQueue
基于数组实现有界的阻塞队列(循环数组)
类的继承
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable
主要成员变量
private static final long serialVersionUID = -817911632652898426L;
final Object[] items; //底层存储元素的数组
int takeIndex; //进行取操作时的下标
int putIndex;//进行放操作时的下标
int count;//队列中元素的数量
final ReentrantLock lock;//阻塞时用的锁
private final Condition notEmpty;//满时的condition队列
private final Condition notFull;//空时的condition队列
构造器
参数由容量和全局锁是否是公平锁(默认是非公平锁)
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<? 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) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
其中第五行代码中获取互斥锁,解释为锁的母的不是为了互斥,而是为了保证可见性。
由于ArrayBlockingQueue操作的其实是一个items数组,这个数组不具备线程安全,所以保证可见性解释保证items的可见性。
主要方法
put()
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();
}
}
- 首先判断添加的是否为空,为空就抛出异常
- 然后给put方法上锁
- 当集合元素数量和集合长度相等时,调用put方法的线程将会被放入notFull队列上等待
- 如果不相等,意味着还是有位置的则之间enquue(),让e进入队列
enqueue()
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();
}
其实就是让该元素入队,并且唤醒因为集合空而等待的线程
take操作基本就是put反过来,真正实现的是dequeue
LinkedBlockingQueue
LinkedBlockingQueue底层是基于链表实现的,所以其基本成员变量和LinkedList差不多
类的继承
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable
构造器
手动设置容量,无参构造器是默认容量为最大容量
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
也支持将整个集合挪入队列中,默认容量同样是最大容量
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
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();
}
}
主要成员变量
链表就一定会有家店,内部节点类和ArrayBlockingQueue不同的是,它有两个全局锁,一个负责放元素,一个负责取元素
static class Node<E> {
E item;
Node<E> next;
Node(E x) {
item = x; }
}
除了节点以外
private transient Node<E> last;//尾节点
transient Node<E> head;//头节点
private final AtomicInteger count = new AtomicInteger();//计算当前阻塞队列中的元素个数
private final int capacity;//容量
//获取并移除元素时使用的锁,如take, poll, etc
private final ReentrantLock takeLock = new ReentrantLock();
//notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程
private final Condition notEmpty = takeLock.newCondition();
//添加元素时使用的锁如 put, offer, etc
private final ReentrantLock putLock = new ReentrantLock();
//notFull条件对象,当队列数据已满时用于挂起执行添加的线程
private final Condition notFull = putLock.newCondition();
主要方法
put()
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(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();
}
PriorityBlockQueue
其是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排列(但不能保证同优先级元素的顺序)
继承Compareable类实现compareTo()方法来指定元素排序规则,或者初始化
DelayQueue
基于PriorityQueue的延时阻塞队列,DelayQueue中的元素只有当其延时时间到达,才能够去当前队列中获取到该元素,DelayQueue是一个无界队列。主要用于缓存系统的设计(保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦获取元素,表示缓存有效期到了)、定时任务系统的设计(使用其保存要执行的任务和执行时间,一旦获取到就开始执行)
实现Delayed的三个步骤
- 第一步:继承Delayed接口
- 第二步:实现getDelay(TimeUnit unit),该方法返回当前元素还需要延时多长时间,单位是纳秒
- 第三步:实现compareTo方法来指定元素的顺序。
SynchronousQueue
其是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。如果设置为true,则等待的线程会采用先进先出的顺序访问队列
SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue.。
LinkTransferQueue
其是一个由链表结构组成的无界阻塞队列。相对于其他阻塞队列,其多了tryThransfer和trnsfer方法
transfer方法
如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回
tryTransfer方法
tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。
对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true
LinkedBlockingDeque
其是一个由链表结构组成的双向阻塞队列。即可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,
另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,
在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。
应用:消费者生产者模式
public class Data {
//flag 表示是否生产,默认生产
private volatile boolean flag=true;
//nums 表示产品
private AtomicInteger nums=new AtomicInteger();
//使用阻塞队列实现
BlockingDeque<Object>queue=null;
public Data(BlockingDeque<Object> queue){
this.queue=queue;
}
// 生产者
public void produce()throws Exception{
String data =null;
boolean retValue;
while (flag){
data =nums.incrementAndGet()+"";
retValue =queue.offer(data,2L, TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName()+"插入结果是:"+retValue);
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName()+"稍安勿躁,一会就好");
}
// 消费者
public void consumer() throws Exception{
Object result=null;
while (flag){
result=queue.poll(2L,TimeUnit.SECONDS);
if(result==null||((String)result).equalsIgnoreCase("")){
flag=false;
}
System.out.println(Thread.currentThread().getName()+"消费资源成功");
TimeUnit.SECONDS.sleep(1);
}
}
}
总结
- ArrayBlockingQueue和LinkedBlockingQueue的区别和联系?
- 数据存储容器不一样,ArrayBlockingQueue采用数组去存储数据、LinkedBlockingQueue采用链表去存储数据,在添加和删除队列中的元素的时候,会创建和销毁对象,在高并发和大量数据的时候,GC压力太大。
- ArrayBlockingQueue是有界的,初始化时必须要指定容量;LinkedBlockingQueue默认是无界的,Integer.MAX_VALUE, 当添加速度大于删除速度、有可能造成内存溢出。
- ArrayBlockingQueue在读和写使用的锁是一样的,即添加操作和删除操作使用的是同一个ReentrantLock,没有实现锁分离;LinkedBlockingQueue实现了锁分离,添加的时候采用putLock、删除的时候采用takeLock,这样能提高队列的吞吐量。
- ArrayBlockingQueue可以使用两把锁提高效率吗?
- 不能,主要原因是ArrayBlockingQueue底层循环数组来存储数据,LinkedBlockingQueue底层 链表来存储数据,链表队列的添加和删除,只是和某一个节点有关,为了防止head和last相互影响,就需要有一个原子性的计数器,每个添加操作先加入队列,计数器+1,这样是为了保证队列在移除的时候, 长度是大于等于计数器的,通过原子性的计数器,使得当前添加和移除互不干扰。对于循环数据来说,当我们走到最后一个位置需要返回到第一个位置,这样的操作是无法原子化,只能使用同一把锁来解决。
最后
- 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持
- 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我
- 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。