目录
2.1 ArrayBlockingQueue 阻塞式获取和新增元素的方法为:
2.2 ArrayBlockingQueue 非阻塞式获取和新增元素的方法为:
1.简介
BlockingQueue
是基于阻塞机制实现的线程安全的队列。解决多线程之间数据共享的问题。阻塞机制的实现是通过在入队和出队时加锁的方式避免并发操作。常用于生产者-消费者模型中,往队列里添加元素的是生产者,从队列中获取元素的是消费者;通常情况下生产者和消费者都是由多个线程组成;生产者和消费者之间通过队列平衡两者的的处理能力、进行解耦等。
从下图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出。
常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue
就是其中的一种)
先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件
2.核心方法
2.1 ArrayBlockingQueue
阻塞式获取和新增元素的方法为:
put(E e)
:将元素插入队列中,如果队列已满,则该方法会一直阻塞,直到队列有空间可用或者线程被中断。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁
lock.lockInterruptibly();
try {
while (count == items.length) {
// 如果队列已满,线程阻塞,并添加到notFull条件队列中等待唤醒
notFull.await();
}
// 如果队列未满,则调用enqueue方法进行入队操作
enqueue(e);
} finally {
// 释放锁
lock.unlock();
}
}
private void enqueue(E x) {
//获取队列底层的数组
final Object[] items = this.items;
//将putindex位置的值设置为我们传入的x
items[putIndex] = x;
//更新putindex,如果putindex等于数组长度,则更新为0
if (++putIndex == items.length)
putIndex = 0;
//队列长度+1
count++;
//通知队列非空,那些因为获取元素而阻塞的线程可以继续工作了
notEmpty.signal();
}
put
方法进行阻塞式入队的基本流程为:
-
首先,在进行入队操作前,使用
ReentrantLock
进行加锁操作,保证只有一个线程执行入队或出队操作;如果锁被其他线程占用,则等待; -
如果加锁成功,则首先判断队列是否满,也就是
while(count == items.length)
;如果队列已满,则调用notFull.await()
,将当前线程阻塞,并添加到notFull条件队列
中等待唤醒;如果队列不满,则直接调用enqueue
方法,进行元素插入; -
当前线程添加到
notFull
条件队列中后,只有当其他线程有出队操作时,会调用notFull.signal()
方法唤醒等待的线程;当前线程被唤醒后,还需要再次进行一次队列是否满的判断,如果此时队列不满才可以进行enqueue
操作,否则仍然需要再次阻塞等待,这也就是为什么在判断队列是否满时使用while
的原因,即避免当前线程被意外唤醒,或者唤醒后被其他线程抢先完成入队操作。 -
最后,当完成入队操作后,在finally代码块中进行锁释放
lock.unlock
,完成put
入队操作
take()
:获取并移除队列头部的元素,如果队列为空,则该方法会一直阻塞,直到队列非空或者线程被中断。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁
lock.lockInterruptibly();
try {
while (count == 0)
// 判断队列是否为空,如果为空则线程阻塞,添加到notEmpty条件队列等待
notEmpty.await();
// 队列不为空,进行出队操作
return dequeue();
} finally {
// 释放锁
lock.unlock();
}
}
private E dequeue() {
//获取阻塞队列底层的数组
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//从队列中获取takeIndex位置的元素
E x = (E) items[takeIndex];
//将takeIndex置空
items[takeIndex] = null;
//takeIndex向后挪动,如果等于数组长度则更新为0
if (++takeIndex == items.length)
takeIndex = 0;
//队列长度减1
count--;
if (itrs != null)
itrs.elementDequeued();
//通知那些被打断的线程当前队列状态非满,可以继续存放元素
notFull.signal();
return x;
}
take
方法与put
方法类似,主要流程也是先加锁,然后循环判断队列是否为空,如果为空则添加到notEmpty条件队列等待,如果不为空则进行出队操作;最后进行锁释放
当消费者从队列中 take
或者 poll
等操作取出一个元素之后,就会通知队列非满,此时那些等待非满的生产者就会被唤醒等待获取 CPU 时间片进行入队操作。
当生产者将元素存到队列中后,就会触发通知队列非空,此时消费者就会被唤醒等待 CPU 时间片尝试获取元素。如此往复,两个条件对象就构成一个环路,控制着多线程之间的存和取。
方法实现
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
@RequestMapping(value = "put", method = RequestMethod.POST)
public void putDate() throws InterruptedException {
CompletableFuture.runAsync(() -> {
for(int i = 1;i <=10;i++){
// 向队列中添加元素,如果队列已满则阻塞等待
try {
queue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产者添加元素--:" + i);
}
}).exceptionally(e -> {
System.out.println("Exception");
return null;
});
}
@RequestMapping(value = "take", method = RequestMethod.POST)
public void takeDate() {
try {
int count = 0;
while (true) {
// 从队列中取出元素,如果队列为空则阻塞等待
Integer element = queue.take();
System.out.println("消费者取出元素:" + element);
++count;
if (count == 10) {
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
生产者添加元素--:1
生产者添加元素--:2
生产者添加元素--:3
生产者添加元素--:4
生产者添加元素--:5
生产者添加元素--:6
消费者取出元素:1
消费者取出元素:2
消费者取出元素:3
消费者取出元素:4
消费者取出元素:5
消费者取出元素:6
生产者添加元素--:7
消费者取出元素:7
生产者添加元素--:8
生产者添加元素--:9
生产者添加元素--:10
消费者取出元素:8
消费者取出元素:9
消费者取出元素:10
2.2 ArrayBlockingQueue
非阻塞式获取和新增元素的方法为:
offer(E e)
:将元素插入队列尾部。如果队列已满,则该方法会直接返回 false,不会等待并阻塞线程。poll()
:获取并移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。add(E e)
:将元素插入队列尾部。如果队列已满则会抛出IllegalStateException
异常,底层基于offer(E e)
方法。remove()
:移除队列头部的元素,如果队列为空则会抛出NoSuchElementException
异常,底层基于poll()
。peek()
:获取但不移除队列头部的元素,如果队列为空,则该方法会直接返回 null,不会等待并阻塞线程。
非阻塞新增 offer()源码
public boolean offer(E e) {
//确保插入的元素不为null
checkNotNull(e);
//获取锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//队列已满直接返回false
if (count == items.length)
return false;
else {
//反之将元素入队并直接返回true
enqueue(e);
return true;
}
} finally {
//释放锁
lock.unlock();
}
}
获取元素poll() 源码
public E poll() {
final ReentrantLock lock = this.lock;
//上锁
lock.lock();
try {
//如果队列为空直接返回null,反之出队返回元素值
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
新增方法详解
方法 | 队列满时处理方式 | 返回值 |
put(E e) | 线程阻塞,直到中断或被唤醒 | void |
offer(E e) | 直接返回 false | boolean |
offer(E e, long timeout, TimeUnit unit) | 指定超时时间内阻塞,超过规定时间还未添加成功则返回 false | boolean |
add(E e) | 直接抛出 异常 | boolean |
获取/移除元素:
方法 | 队列空时处理方式 | 方法返回值 |
| 线程阻塞,直到中断或被唤醒 | E |
| 返回 null | E |
poll(long timeout, TimeUnit unit) | 指定超时时间内阻塞,超过规定时间还是空的则返回 null | E |
| 返回 null | E |
| 直接抛出 异常 | boolean |
contains(Object o)
来判断指定元素是否存在于队列中。
3.阻塞队列的实现类
实现类 | 功能 |
ArrayBlockingQueue JDK1.5 | 基于数组的阻塞队列,使用数组存储数据,并需要指定其长度,所以是一个有界队列 |
LinkedBlockingQueue JDK1.5 | 基于链表的阻塞队列,使用链表存储数据,默认是一个无界队列;也可以通过构造方法中的capacity 设置最大元素数量,所以也可以作为有界队列 |
SynchronousQueue JDK1.6 | 一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并且立刻消费 |
PriorityBlockingQueue | 基于优先级别的阻塞队列,底层基于数组实现,是一个无界队列 |
DelayQueue JDK1.8 | 延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队 |
4.总结
-
ArrayBlockingQueue
的容量有限,一旦创建,容量不能改变。常用于多线程之间的数据共享,底层采用数组实现,从其名字就能看出来了。 -
保证线程安全,
ArrayBlockingQueue
的并发控制采用可重入锁ReentrantLock
,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。并且,它还支持公平和非公平两种方式的锁访问机制,默认是非公平锁。 -
线程间的等待唤醒实现:
- 当队列已满时,生产者线程会调用
notFull.await()
方法让生产者进行等待,等待队列非满时插入(非满条件)。 - 当队列为空时,消费者线程会调用
notEmpty.await()
方法让消费者进行等待,等待队列非空时消费(非空条件)。 - 当有新的元素被添加时,生产者线程会调用
notEmpty.signal()
方法唤醒正在等待消费的消费者线程。 - 当队列中有元素被取出时,消费者线程会调用
notFull.signal()
方法唤醒正在等待插入元素的生产者线程。