什么是阻塞队列
BlockingQueue阻塞队列,排队拥堵,首先它是一个队列,而一个阻塞队列在数据结构中所起到的作用大致如下午所示:
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
当阻塞队列是满时,从队列中添加元素的操作将会被阻塞
也就是说 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其它线程往空的队列插入新的元素。
同理,试图往已经满的阻塞队列中添加新元素的线程,直到其它线程往满的队列中移除一个或多个元素,或者完全清空队列后,使队列重新变得空闲起来,并后续新增
为什么要有阻塞队列BlockingQueue
在多线程领域:所谓的阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动唤醒
使用阻塞队列的好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都帮你一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己取控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
BlockingQueue阻塞队列是属于一个接口,底下有七个实现类
- ArrayBlockQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界(但是默认大小 Integer.MAX_VALUE)的阻塞队列
- 有界,但是界限非常大,相当于无界,可以当成无界
- PriorityBlockQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
- 生产一个,消费一个,不存储元素,不消费不生产
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
阻塞队列引入
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法:
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
在阻塞队列不可用时,这两个附加操作提供了4种处理方式
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 不可用🙅 | 不可用🙅 |
抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出 IllegalStateException(“Queue full”)** 异常**。当队列为空时,从队列里获取元素时会抛出 NoSuchElementException** 异常** 。
返回特殊值:插入方法会返回是否成功,成功则返回 true。移除方法,则是从队列里拿出一个元素,如果没有则返回 null
一直阻塞:当阻塞队列满时,如果生产者线程往队列里 put 元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里 take 元素,队列也会阻塞消费者线程,直到队列可用。
超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。
如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true
阻塞队列
JDK7 提供了 7 个阻塞队列。分别是
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
ArrayBlockingQueue
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序
- items:一个Object的数组
- tackIndex:出队列的下标
- putIndex:入队列的下标
- count:队列中元素的数量
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
查看ArrayBlockingQueue 的构造函数
设置ReentrantLock的锁模式为公平锁
LinkedBlockingQueue
LinkedBlockingQueue 是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为 Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序
ArrayBlockingQueue 和LinkedBlockingQueue区别
1:底层实现不同
ArrayBlockingQueue 底层使用数组来维护队列,
LinkedBlockingQueue 底层使用链表来维护队列,在添加和删除队列中的元素的时候,会创建和销毁节点对象,在高并发和大量数据的时候,GC压力很大。
2:锁的方式不同
ArrayBlockingQueue 获取数据和添加数据都是使用同一个锁对象,这样添加和获取就不是一个并发的过程,不过,在ArrayBlockingQueue 中使用Condition的等待/通知机制,这样使得ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜
LinkedBlockingQueue 获取数据和添加数据使用不同的锁对象。
PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。继承Comparable类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
例子 学生实体类存入PriorityBlockingQueue 队列按照年龄升序排序
package blockingqueue;
import java.util.Formattable;
import java.util.concurrent.PriorityBlockingQueue;
public class PriorityBlockingQueueTest {
public static class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name,int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(Student o) {
return this.age > o.getAge() ? 1 : this.age <o.getAge() ? -1 : 0;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public static void main(String[] args) throws InterruptedException {
PriorityBlockingQueue<Student> queue = new PriorityBlockingQueue<Student>();
queue.put(new Student("小A",18));
queue.put(new Student("小B",17));
queue.put(new Student("小C",19));
queue.put(new Student("小D",20));
while (true){
System.out.println(queue.take().toString());
}
}
}
DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素
DelayQueue非常有用,可以将DelayQueue运用在以下应用场景。
缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的
实现DelayQueue的三个步骤
第一步:继承Delayed接口
第二步:实现getDelay(TimeUnit unit),该方法返回当前元素还需要延时多长时间,单位是纳秒
第三步:实现compareTo方法来指定元素的顺序
例子:使用Task类给定延迟时间,继承Delayed,并**重写getDelay()方法,然后重写compareTo()**方法来规定先到期的Task对象先出队列
package blockingqueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayQueueTest {
public static class Task implements Delayed {
private long outTime;//延迟时间
private long delayTime;//到期时间
public Task(long outTime){
this.outTime = outTime;
//使用的时间刻度是纳秒级别
delayTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(outTime,TimeUnit.SECONDS);
}
/**
* 获取剩余的到期时间 当返回负数的时候代表过期
* @param unit the time unit
* @return
*/
public long getDelay(TimeUnit unit){
return unit.convert(this.delayTime -
System.nanoTime(),TimeUnit.NANOSECONDS);
}
/**
* 对延迟时间排序
* @param o the object to be compared.
* @return
*/
public int compareTo(Delayed o){
if (o == this) return 0;
if (o instanceof Task){
Task task = (Task)o;
return Long.compare(this.outTime, task.outTime);
}
long d = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return d > 0 ? 1 : d < 0 ? -1 : 0;
}
public void print(){
System.out.println("task_"+outTime+"到期");
}
}
public static void main(String[] args) throws InterruptedException {
final DelayQueue<Task> queue = new DelayQueue<Task>();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i<11;i++){
Task task = new Task(i);
queue.put(task);
}
}
},"Produce-Thread").start();
Thread.sleep(1000);
new Thread(new Runnable() {
@Override
public void run() {
for (;;){
Task task = null;
try {
task = queue.take();
task.print();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"Consume-Thread").start();
}
}
SychronousQueue
SychronousQueue是一个不存储元素的阻塞队列.每一个put操作必须等待一个take操作,否则不能继续添加元素.
它支持公平访问队列. 默认情况下线程采用非公平性策略访问队列.使用以下构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列
public SynchronousQueue(boolean fair){
transferer = fair ? new Transfer Queue<E>() : new TransferStack<E>();
}
SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。
LinkedTransferQueue
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法
(1)transfer方法
如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回
(2)tryTransfer方法
tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。
对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true
LinkedBlockingDeque
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是JDK的bug,使用时还是用带有First和Last后缀的方法更清楚。在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在**“工作窃取”**模式中。
生产者与消费者模型
生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
实现
通过 “循环队列” 的方式来实现.
使用 synchronized 进行加锁控制.
put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一
定队列就不满了, 因为同时可能是唤醒了多个线程).
take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
package thread;
// 自己模拟实现一个阻塞队列.
// 基于数组的方式来实现队列.
// 提供两个核心方法:
// 1. put 入队列
// 2. take 出队列
class MyBlockingQueue {
// 假定最大是 1000 个元素. 当然也可以设定成可配置的.
private int[] items = new int[1000];
// 队首的位置
private int head = 0;
// 队尾的位置
private int tail = 0;
// 队列的元素个数
volatile private int size = 0;
// 入队列
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == items.length) {
// 队列已满, 无法插入
this.wait();
}
items[tail] = value;
tail++;
// 汤老湿个人还是更建议写这个版本.
if (tail == items.length) {
// 注意 如果 tail 达到数组末尾, 就需要从头开始~
tail = 0;
}
// 下面这个写法绝对不是错!! 也是正确的写法. 大家去掌握当然也没毛病
// tail = tail % items.length;
size++;
// 即使没人在等待, 多调用几次 notify 也没啥副作用~~
this.notify();
}
}
// 出队列
public Integer take() throws InterruptedException {
int ret = 0;
synchronized (this) {
while (size == 0) {
// 队列为空, 就等待
this.wait();
}
ret = items[head];
head++;
if (head == items.length) {
head = 0;
}
size--;
this.notify();
}
return ret;
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue = new MyBlockingQueue();
Thread customer = new Thread(() -> {
while (true) {
int value = 0;
try {
value = queue.take();
System.out.println("消费: " + value);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
Thread producer = new Thread(() -> {
int value = 0;
while (true) {
try {
queue.put(value);
System.out.println("生产: " + value);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
希望能帮到你