Queue接口中定义了 6 个方法:
public interface Queue<E> extends Collection<E> {
boolean add(e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
每个Queue方法都有两种形式:
(1)如果操作失败则抛出异常,
(2)如果操作失败,则返回特殊值(null或false,具体取决于操作),接口的常规结构如下表所示。
Queue从Collection继承的add方法插入一个元素,除非它违反了队列的容量限制,在这种情况下它会抛出IllegalStateException;offer方法与add不同之处仅在于它通过返回false来表示插入元素失败。
remove和poll方法都移除并返回队列的头部,确切地移除哪个元素是由具体的实现来决定的,仅当队列为空时,remove和poll方法的行为才有所不同,在这些情况下,remove抛出NoSuchElementException,而poll返回null。
element和peek方法返回队列头部的元素,但不移除,它们之间的差异与remove和poll的方式完全相同,如果队列为空,则element抛出NoSuchElementException,而peek返回null。
队列一般不要插入空元素。
//capacity表示容量大小,默认内部采用非公平锁
public ArrayBlockingQueue(int capacity)
//capacity:容量大小,fair:内部是否是使用公平锁
public ArrayBlockingQueue(int capacity, boolean fair)
**需求:**业务系统中有很多地方需要推送通知,由于需要推送的数据太多,我们将需要推送的信息先丢到阻塞队列中,然后开一个线程进行处理真实发送,代码如下:
package com.cloud;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueue {
//推送队列
static ArrayBlockingQueue<String> pushQueue = new ArrayBlockingQueue<String>(10000);
static {
//启动一个线程做真实推送
new Thread(() -> {
while (true) {
String msg;
try {
long starTime = System.currentTimeMillis();
//获取一条推送消息,此方法会进行阻塞,直到返回结果
msg = pushQueue.take();
long endTime = System.currentTimeMillis();
//模拟推送耗时
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(String.format("[%s,%s,take耗时:%s],%s,发送消息:%s", starTime, endTime, (endTime - starTime), Thread.currentThread().getName(), msg));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
//推送消息,需要发送推送消息的调用该方法,会将推送信息先加入推送队列
public static void pushMsg(String msg) throws InterruptedException {
pushQueue.put(msg);
}
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 5; i++) {
String msg = "一起来学java高并发,第" + i + "天";
//模拟耗时
TimeUnit.SECONDS.sleep(i);
BlockingQueue.pushMsg(msg);
}
}
}
输出:
[1692848849086,1692848850088,take耗时:1002],Thread-0,发送消息:一起来学java高并发,第1天
[1692848850614,1692848852093,take耗时:1479],Thread-0,发送消息:一起来学java高并发,第2天
[1692848852595,1692848855097,take耗时:2502],Thread-0,发送消息:一起来学java高并发,第3天
[1692848855599,1692848859108,take耗时:3509],Thread-0,发送消息:一起来学java高并发,第4天
[1692848859617,1692848864122,take耗时:4505],Thread-0,发送消息:一起来学java高并发,第5天
代码中我们使用了有界队列ArrayBlockingQueue,创建ArrayBlockingQueue时候需要制定容量大小,调用pushQueue.put将推送信息放入队列中,如果队列已满,此方法会阻塞。代码中在静态块中启动了一个线程,调用pushQueue.take();从队列中获取待推送的信息进行推送处理。
注意:ArrayBlockingQueue如果队列容量设置的太小,消费者发送的太快,消费者消费的太慢的情况下,会导致队列空间满,调用PUT方法会导致发送者线程阻塞,所以注意设置合理的大小,协调好消费者的速度。
//默认构造方法,容量大小为Integer.MAX_VALUE
public LinkedBlockingQueue();
//创建指定容量大小的LinkedBlockingQueue
public LinkedBlockingQueue(int capacity);
//容量为Integer.MAX_VALUE,并将传入的集合丢入队列中
public LinkedBlockingQueue(Collection<? extends E> c);
LinkedBlockingQueue的用法和ArrayBlockingQueue类似,建议使用的时候指定容量,如果不指定容量,插入的太快,移除的太慢,可能会产生 OOM。
//默认构造方法,默认初始化容量是11
public PriorityBlockingQueue();
//指定队列的初始化容量
public PriorityBlockingQueue(int initialCapacity);
//指定队列的初始化容量和放入元素的比较器
public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator);
//传入集合放入来初始化队列,传入的集合可以实现SortedSet接口或者PriorityQueue接口进行排序,如果没有实现这2个接口,按正常顺序放入队列
public PriorityBlockingQueue(Collection<? extends E> c);
优先级队列放入元素的时候,会进行排序,所以我们需要指定排序规则,有2种方式:
1.创建PriorityBlockingQueue指定比较器Comparator.
2.放入的元素需要实现Comparable接口.
上面2种方式必须选一个,如果2个都有,则走第一个规则排序。
需求:还是上面的推送业务,目前推送是按照放入的先后顺序进行发送的,
比如,有些公告比较紧急,优先级比较高,需要快点发送,怎么搞?
此时PriorityBlockingQueue就派上用场了,代码如下:
package com.cloud;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
public class PriorityQueue {
//推送信息封装
static class Msg implements Comparable<Msg> {
//优先级,越小优先级越高
private int priority;
//推送的信息
private String msg;
public Msg(int priority, String msg) {
this.priority = priority;
this.msg = msg;
}
@Override
public int compareTo(Msg o) {
return Integer.compare(this.priority, o.priority);
}
@Override
public String toString() {
return "Msg{" +
"priority=" + priority +
", msg='" + msg + '\'' +
'}';
}
}
//推送队列
static PriorityBlockingQueue<Msg> pushQueue = new PriorityBlockingQueue<Msg>();
static {
//启动一个线程做真实推送
new Thread(() -> {
while (true) {
Msg msg;
try {
long starTime = System.currentTimeMillis();
//获取一条推送消息,此方法会进行阻塞,直到返回结果
msg = pushQueue.take();
//模拟推送耗时
TimeUnit.MILLISECONDS.sleep(100);
long endTime = System.currentTimeMillis();
System.out.println(String.format("[%s,%s,take耗时:%s],%s,发送消息:%s", starTime, endTime, (endTime - starTime), Thread.currentThread().getName(), msg));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
//推送消息,需要发送推送消息的调用该方法,会将推送信息先加入推送队列
public static void pushMsg(int priority, String msg) throws InterruptedException {
pushQueue.put(new Msg(priority, msg));
}
public static void main(String[] args) throws InterruptedException {
for (int i = 5; i >= 1; i--) {
String msg = "一起来学java高并发,第" + i + "天";
PriorityQueue.pushMsg(i, msg);
}
}
}
运行结果:
[1692849888221,1692849888328,take耗时:107],Thread-0,发送消息:Msg{priority=1, msg=‘一起来学java高并发,第1天’}
[1692849888362,1692849888467,take耗时:105],Thread-0,发送消息:Msg{priority=2, msg=‘一起来学java高并发,第2天’}
[1692849888468,1692849888578,take耗时:110],Thread-0,发送消息:Msg{priority=3, msg=‘一起来学java高并发,第3天’}
[1692849888578,1692849888689,take耗时:111],Thread-0,发送消息:Msg{priority=4, msg=‘一起来学java高并发,第4天’}
[1692849888689,1692849888798,take耗时:109],Thread-0,发送消息:Msg{priority=5, msg=‘一起来学java高并发,第5天’}
main中放入了 5 条推送信息,i 作为消息的优先级按倒序放入的,最终输出结果中按照优先级由小到大输出。
注意 Msg 实现了Comparable接口,具有了比较功能。
来个示例代码:
package com.cloud;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class SynchronousQue {
static SynchronousQueue<String> queue = new SynchronousQueue<>();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
long starTime = System.currentTimeMillis();
queue.put("java高并发!");
long endTime = System.currentTimeMillis();
System.out.println(String.format("[%s,%s,take耗时:%s],%s", starTime,
endTime, (endTime - starTime), Thread.currentThread().getName()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//休眠5秒之后,从队列中take一个元素
TimeUnit.SECONDS.sleep(5);
System.out.println(System.currentTimeMillis() + "调用take获取并移除元素," +
queue.take());
}
}
输出:
1692852559384调用take获取并移除元素,java高并发!
[1692852554382,1692852559384,take耗时:5002],Thread-0
main 方法中启动了一个线程,调用queue.put方法向队列中丢入一条数据,调用的时候产生了阻塞,从输出结果中可以看出,直到take方法被调用时,put 方法才从阻塞状态恢复正常。
我们先看一下DelayQueue类的声明:
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>implements BlockingQueue<E>
元素 E 需要实现接口Delayed,我们看一下这个接口的代码:
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
Delayed继承了Comparable接口,这个接口是用来做比较用的,DelayQueue内部使用PriorityQueue来存储数据的,PriorityQueue是一个优先级队列,丢入的数据会进行排序,排序方法调用的是Comparable接口中的方法。下面主要说一下Delayed接口中的getDelay方法:此方法在给定的时间单位内返回与此对象关联的剩余延迟时间。
对推送我们再做一下处理,让其支持定时发送(定时在将来某个时间也可以说是延迟发送),代码如下:
package com.cloud;
import java.util.Calendar;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayedQueue {
//推送信息封装
static class Msg implements Delayed {
//优先级,越小优先级越高
private int priority;
//推送的信息
private String msg;
//定时发送时间,毫秒格式
private long sendTimeMs;
public Msg(int priority, String msg, long sendTimeMs) {
this.priority = priority;
this.msg = msg;
this.sendTimeMs = sendTimeMs;
}
@Override
public String toString() {
return "Msg{" +
"priority=" + priority +
", msg='" + msg + '\'' +
", sendTimeMs=" + sendTimeMs +
'}';
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.sendTimeMs - Calendar.getInstance().getTimeInMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
if (o instanceof Msg) {
Msg c2 = (Msg) o;
return Integer.compare(this.priority, c2.priority);
}
return 0;
}
}
//推送队列
static DelayQueue<Msg> pushQueue = new DelayQueue<Msg>();
static {
//启动一个线程做真实推送
new Thread(() -> {
while (true) {
Msg msg;
try {
//获取一条推送消息,此方法会进行阻塞,直到返回结果
msg = pushQueue.take();
//此处可以做真实推送
long endTime = System.currentTimeMillis();
System.out.println(String.format("定时发送时间:%s,实际发送时间:%s,发送消息:%s", msg.sendTimeMs, endTime, msg));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
//推送消息,需要发送推送消息的调用该方法,会将推送信息先加入推送队列
public static void pushMsg(int priority, String msg, long sendTimeMs) throws InterruptedException {
pushQueue.put(new Msg(priority, msg, sendTimeMs));
}
public static void main(String[] args) throws InterruptedException {
for (int i = 5; i >= 1; i--) {
String msg = "一起来学java高并发,第" + i + "天";
DelayedQueue.pushMsg(i, msg, Calendar.getInstance().getTimeInMillis() + i * 2000);
}
}
}
输出:
定时发送时间:1692853261877,实际发送时间:1692853261883,发送消息:Msg{priority=1, msg=‘一起来学java高并发,第1天’, sendTimeMs=1692853261877}
定时发送时间:1692853263877,实际发送时间:1692853263886,发送消息:Msg{priority=2, msg=‘一起来学java高并发,第2天’, sendTimeMs=1692853263877}
定时发送时间:1692853265877,实际发送时间:1692853265880,发送消息:Msg{priority=3, msg=‘一起来学java高并发,第3天’, sendTimeMs=1692853265877}
定时发送时间:1692853267877,实际发送时间:1692853267878,发送消息:Msg{priority=4, msg=‘一起来学java高并发,第4天’, sendTimeMs=1692853267877}
定时发送时间:1692853269852,实际发送时间:1692853269861,发送消息:Msg{priority=5, msg=‘一起来学java高并发,第5天’, sendTimeMs=1692853269852}
可以看出发送时间和定时发送时间基本一致,代码中Msg需要实现Delayed接口,重点在于getDelay方法,这个方法返回剩余的延迟时间,代码中使用this.sendTimeMs减去当前时间的毫秒格式时间,得到剩余延迟时间。
public interface TransferQueue<E> extends BlockingQueue<E> {
// 如果存在一个消费者已经等待接收它,则立即传送指定的元素,否则返回false,并且不进入队列。
boolean tryTransfer(E e);
// 如果存在一个消费者已经等待接收它,则立即传送指定的元素,否则等待直到元素被消费者接收。
void transfer(E e) throws InterruptedException;
// 在上述方法的基础上设置超时时间
boolean tryTransfer(E e, long timeout, TimeUnit unit)throws InterruptedException;
// 如果至少有一位消费者在等待,则返回true
boolean hasWaitingConsumer();
// 获取所有等待获取元素的消费线程数量
int getWaitingConsumerCount();
}