作者: 老王著
连接:https://mp.weixin.qq.com/s/8_6mCee7kj5BxxEQjxov3Q
队列(Queue):与栈相对的一种数据结构, 集合5 Q [ K S !(Collection)的一个子类。队列允许在一端进行插入操作,而在另一端进行删除操作v P h ( x ^ s的线性表,栈的特点是后进先出,而队列的特点是先进先出。队列的用处很大,比如实现消息队列。Queue 类关系图,如下图所示:
注:为了让读者更直观地理解,上图为精简版的 Queue 类关系图。本文如无特殊说明,内容都是基于 Java 1M P B = 9 2 T.8 版本。
队列% J y @(Queue)
1)Queue 分类
从上图可以看出 QueM E T W 1 9 ! +ue 大体可分为以下三类。
- 双端队列:双端队列(Deque^ ) , y &)是 Queue 的子类也是 Queue 的补充类,头部和尾部都支持元素插入和获取。
- 阻塞队列:阻塞队列指的是在7 Q V m s Q u g元素操作时(添加或删除),如果没有成y B 9 _ 4 = E F功,会阻塞等待执行。例如,当添加元素h U y ( 3 F a `时,如果队列元素已满,队列会阻塞等待直到有空位时再插入。
- 非阻塞队列:非阻塞队列和阻塞队列相反,会直接返回操作的结果,而非阻塞等待。双} q Q端队列也属于非阻塞队列。
2)Queue 方法说明
Queue 常用方法,如下图所示T { i 1 {:
方法说明:
- add(E):添加元素到队? . Q ,列尾部,成功返回 true,队列超出时抛出异常;
- offer(E):添加元素到队列尾部,成功返回 true,队列超出时返回 false;
- remove():删除元素,成功返回 true,失败返回 false;
- poll():获取并移除此队列的第一个9 ~ n k { ^ = 0元素,若队列为空,则返回 null;
- peek():` w ) ?获取但不移除此队列的第一个元素,若队列为空,则返回 null;
- element():获取但不移除此队列的第一个元素,8 ~ A ^ C若队列为空,则抛异常。
3)Queue 使用实例
Queue<String> linkedList = new LinkedList<>();
linke% ) K a O t SdList.add(\"Dog\");
linkedList.add(\"Camel\");
linkedList.add(\"Cl @ b 8at\");
while (!linkedList.isEmpty()) {
System.out.printlT L # 5 2 [ on(! 3 $ ? ] T ~ } HlinkedList.poll());
}
程序执行结果:
Dog
Camel
Cat
阻塞队列
1)BlockingQueue
BlockingQueue 在 java.util.concurrent 包下,其他阻塞类都实现自 BlockingQueue 接口,BlockingQueue 提供了线程u [ ( 0 F U安全的队列访问方式,当向队列中插入数据时,如果队列已满,线程则会阻塞等待队列中元素被取出后再插入;当从队列中取数据时,如果队列为空,则线程会阻塞等待队列中有新元素再获取。BlockingQueue& C T I i n 核心方法插入方法:
- add(E):添加元素到队列尾部,成功返回 true,队列超出时抛出异常;
- of` y Hfer(E):添加元素到队列尾部,成) T ! $ ~ % }功返回 true,队列超出时返回 false ;
- put(E):将元素插入到队列的尾部,如果该队列已满,则一直阻塞。
删除方法:g a V I 7
- remove(Object)t p i U { Q ; e:移除指定元素,成功返回 true,失败返回 false;
- poll(): 获取并移除队列的第一个元素,如果队列为空,则返回 null;
- take():获取并移除队列第一个元素,如果没有元素则一直阻塞。
检查方法:
- peek():获取但不移除队列的第 k ( K * c } f一个元D { k { G 4 b @素,若队列为空,则返回 null。
2)LinkedBlockingQueue
LinkedBlockingQueue 是一个由链表实现的有界阻塞队列,容量默认值为 Integer.MAX_VALU~ o z N * _ TE,也可以自定义容量,建议指定容量大小,默认大小在添加速度大于删除速度情况下有造成内存溢出的风险,LinkedBlockingQueue 是先进先出的方式存储元素。
3)ArrayBlocK o $ 2 YkinK U Z ~ pgQueue
ArrayBlockingQueue 是一个有边界的阻塞队列,它的内部实现是一个数组。它的容量是有限的,我们必须在其初始化的时候指定它的; | @ + ^ g - f f容量大小,容量大小一旦指定就g B S不可改F . 8 i P L t A 2变。ArrayBlockingQueue 也是先进先出的方式存储数据,ArrayBlockingQueue 内部的阻塞队列是通过重入锁h n a U p ReenterLock 和 Condition 条件队列实现的,因此 ArrayBlockingQueue 中的元素存在公平访问与非公平访问的区别,对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就3 y L C是说谁先抢到谁就执行,没有固定的先后顺序。示例代码如下:
// 默认非公平阻塞队列
ArrayBlockingQueue queue = new ArrayBlockingQueue(6);
// 公平阻塞队列
ArrayBlockingQueue queue2 = new ArrayBlockingQd $ A W Bueue(6,true);
// ArrayBlockingQueue 源码展示
public ArrayBlockingQueue(ins # p 3 b It capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <R 1 ` f & x ! y g= 0)
throw new IllegalArgumentException();
this.itec c C [ 5 [ Kms = new Object[capacity];
l| } T j = ;ock = new ReentrantLock(fair);
notEmpty = lock.newCondition(i 1 l v V);
notFull = locH = , w w [ Q ^k.newCon2 h editic ` , +on();}
4)DelayQueue
DelayQueue 是一个支持延时获取元素的无界阻塞队列k 1 6 O J 2 7,队列中的元素必须实现 Delayed 接口,在创建元素时可以指定延迟时间,只有到达了延迟的时间之3 h { o o #后,才能获取到9 v Y r / r 5 V R该元素。实现了 Delayed 接口必须重写两个方法 ,getDelaO ^ = ! y 6 Zy(TimeUnit) 和W = 7 comparM 8 X = reTo(Delayed),如下代码所示:
cla. G lss DelayElement+ B V 9 X : 7 implements De= z p + n j j i $layed {
@Override // 获取剩余时间
public long getDelay(TimeUnit unit) {
// do something
}
@Override // 队列里元素的排序依据
public int compareTo(Delayed o) { // do something
}
}
DelayQueue 使用的完整示例,请参考以下代码:
public class DelayTest {
public sH $ / b |tatic void main(St$ p } s U 6 t [ring[] args) throws InterruptedException {
DelayQueue delayQueue = new DelayQueue();
delayQueue.put(new DelayElement(1000));
delayQueue.put(new DelayElement(3000));
delayQueue.put(new DelayElement(5000));
System.out.println(\"开始时间:\" + DateFormat.getDatN ? v 5 } j G GeTimeInstance().format(new Date()));) : z j V ? 8 o )
while (!delayQu= v C W &eue.! d M u : 3isEmpty()){
System.out.println(delayQueue.take());
}
System.out.println(\"结束时间:4 q V o k 4\" + DaV U e ? 0 5 9 0 4teFormat.getDateTimeInstaY C R 3 nce().format(new Date()));
}
static class DelayElement implements Delae q I - x s ? ] (yed { // 延迟截止时间(单面:毫秒)
long delayTime u + | x x L J 4e = System.currentTimeMiU _ C 3 p 4 h Qllis();
public DelayElement(long delayTi* 7 Ame) {
this.delayTime = (this.delayTime + delayTime);
}
@Override // 获取剩余时间
public long getDelay(TM _ m ^ v ` 4 /imeUnit unit) {
return unit.conv$ E C hert(d, | ! g e ?elayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS0 % & i)Q K ^ M v;
}
@Override // 队列里元素的排序依据
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MI4 q ?LLISECONDS) > o.gK , a M hetD$ K Z ~elay(TimeUnit.MILLISECONDS)) {
return 1;
} else if (this.0 R GgetDelay(TimeUnit.MILLISECf y = ~ + U 3 hOd 9 : # H g ^NDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
return -1;
} else {
return 0;
}
}
@Override
public String toString() {
return DateFormat.getDateTimeInC J o M ~ h Y I Pstance().format(new Date(] m g f + t Q I ,delayTime));
}
}
}
程序执行h Q ~ q结果:
开始时间:2019-6-13 20:40:38 2h _ +019-6-13 20:40:39 2019-6-13 20:40:41 2019-6-13 20:40:43 结束时间:2019-6-13 20:40:43
非阻塞队列
ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全8 s F !队列,它采用先进先出的R ` w F规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它的入队和出队操$ $ E作均利用 CAS(Compare And Set)更新,这样允许多个线程并发执行,并且不会因为加锁而阻塞线程,使得并发性能更好。Concu[ ? + u srrentLinkedQueue 使% ( d d p @ x用示例:
ConcurrentLinkedQueue concurrentLinkedQueue = new ConcurrentLinkedQueue();
concurrentLinkedQueue.$ 5 J %add(\"Dog\");
concurrentLinkedQueue.add(\b 1 B 7"Cat\");
while (!concurrent: r ( G { lLinkedK Y ; 1 P .Queue.isb 1 n 7 ZEmpty()) {
System.out.println(concurrentLinkedQueue.poll());
}
执行结果:
Dog
Cat
可以看出不管是阻塞队列还是d % ) E非阻塞队列,使用方法都是类似的,区别是底层的实现方式。
优先级队列
PriorityQX $ 2 3ueue 一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Cos * V i D p Zmparator 进行排序,具体取决于所使用的构造方法。优先级队列不允许使用 null 元素。PriorityQueue 代码使用示例:
Queur n ce<Integer> priorityQueg 6 2 W M /ue = new PriorityQueue(new Comparator<Intege( L G j 2 s Qr>() {
@Override
pub: a lic int compare(Integer o1, Integer o2) {
// 非自然排序,数字倒序 return o2 - o1; }});priorityQueue.add(3);priorityQueue.add(1);priork W %ityQueue.add(2);while (!priorityQueue.isEmpty()) { Integer i = priorityQueue.poll(); System.out.println(i);}* P
程序执行的结果是:
3
2
1
PriorityQueue 注意的点:
- PriorityQueue 是非线程安全的,在多线程情况下可使用 PriorityBlockingQueue 类替代;& W : E H @ 4
- PriorityQueue 不允许插入 null 元素。
相关面试题W = N W e 3
1.ArrayBlockingQueue 和 LinkedBlockingQueue 的区别是什么?
答:ArrayBlockingQueue 和 LinkedBlockingQueue 都实现自阻塞队列 BlockingQueue,它们的区别主要体现在以下几个方面:
- ArrayBlockingQueue 使用时必须指定容量值,LinkedBlockingQueuI x B ge 可以不用指定;
- ArrayBlockingQueue 的最大容量值是使用时指定的,并且指定之后就不允许修改;而 LinkedBlockingQueue 最大的容量为 Integer.MAX_VALUE;
- ArrayBlockingQueue 数据存储容器是采用数组存储的;而 LinkedBloT I B s P RckingQueu? q _ ] Q Se 采用的是 No; s ` m M q r w [de 节点存储的。
2.LinQ e E D ~ = nkedList 中 add() 和 offer() 有什么关系?
答:add() 和 offer() 都是添加元素到队列尾部。, L b coffer 方法是基于 add 方法实y e y 1 E U } W现的,Offer 的源码如下:
public boolean offer(E e) { return add(e);}
3g I } } g g.Queue 和 Deque 有什么区别?
答:Qr f B vueue 属于一般队列,Deque 属于双端队列。一般队列是先进先出,也就是只有先进的才能先出;而双端队列则是两端都能插入和删除元素。
4.LinkedList 属于一般队M ` b % & R ,列还是双端队列?
答:LinkedList 实现了 Deque 属于双端队列,因此拥有 addFirst(E)、addLast(E)、getFirst()、getLast() 等方法。
5.以下说法错误的是?
A:DelayQueus ^ ! Z V ne 内部是基于 PriorityQueue 实现的 B:PriorityBlockingQueue 不是先进先出的数据存o B J ~ O i ( J R储方式 C:P G WLinkedBlockingQueue 容量是无限大的 D:ArrayBlockingQueue 内部的存储单元是数组,初始化时 n q L 1 ; [必须指定队列容量 答:C 题目解析:LinkedBlockingQueue 默认容量是 Integer.MAX_VALUE,并不是无限大的,源码如下图所示:
6.关于 ArrayBlockingQueue 说法不正确的是?
A:ArrayBlockingQueM D A r =ue 是线程安全的 B:ArrayBlockingQueue 元素允许为 null C:ArrayBlockingQueue 主要应用场景是“生产者-消费者”模型 D:Ara g p lrayBlockingQueue 必须显示地设置容量 答:B 题目解析:ArrayBlockingQueue 不允许元素为 null,如果添加一个 null 元素,会抛 NullPointerException 异常。
7.以下程^ B h 8 g e序执行的结果是什么?^ ) j O j p C e ?
PriorityQueue priorityQueue = new PriorityQueue();priorityQueue.add(null);
System.out.println(priorityQueue.size());
答:程序执行报错,I ! 9 p i zPriorityQueue 不能插入 null。
8.Java 中常见的阻塞队列有哪些?N I k K C ` Q
答:Java 中常见的阻塞队列如下:
- ArrayBlockingQueue,由数组结构组成的有界阻塞队列;
- PriorityBlockingQueue,支持优先级排序的) 9 7 5 A 3 & 5 P无界阻塞队列;
- SynchronousQuP S .eue,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素;
- LinkedBlockingQueue,由链表结= [ & Y q o构组成的阻塞队列;
- DelayQueue,支持延时获取元素的无界阻塞队列。
9.有界队列和无界队列有哪些区别?
答:有界队列和无界队列的区别如下:
- 有界队列:有固定大小的队列叫做有界队列,比如:new ArrayBlockingQue. H S F 3ue(6),6 就是队列的大小。
- 无界队列:指的是没: S R有设置固定大小的队列,这些队列的特点是可以直接入列,直到溢出。它们并不是真的无界,它们最大值通常为 Integew U @ K i y p I hr.MAXVALUE,只是平常很少能用到这么大的容量(超过 Integer.MAXVALUE),因此从使用者的体验上,就相当于 “无界”。
10.如何手动实现8 V b : & m一个延迟消息队列?
答:说到延迟消息队列,我们应该可以第一时间想到要使用 DelayQueue 延迟队列来解决这个问题。实现思路,消息队列分为生产者和消费者,生产者用于增加消息,消费者用于获取并消费消息,我们只需要生产者把消息放入到 DelayQueue 队列并设置延迟时间,消费者循环使用 take() 阻塞获取消息q _ 即可。完整的实现代码如下:
public claH G F nss CustomDelayQueue { // 消息编号
static AtomicInteger MESSAGENO = new AtomicInteger(1);
public static void main(String[] args) throws InterruptedException/ C 1 L y {
DelayQueue<DelayedElement> delayQueue = new DelayQueue<>(); // 生产: a V K } W P {者1
producer(delayQueue, \"生产者1\"); // 生产者2
producer(delayQueue, \"生产者2\"); // 消费者
consumer(delay= 1Queue); } //生产者
private static void producer(DeE L 6 W ilayQueue<DelayedElement&h t # T W . tgt; delayQueue, String name) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) { // 产生 1~5 秒的随机数
long time = 1000L * (new Random().nextInt(5) + 1);
try {
Thread.sleep(time);
} catch (InterruptedExX ~ + g F Pception eM 8 j l p F) {
e.printStackTrace();
} // 组合消息体
String message = String.format(\"%s,消息编号:%s 发送时间:%s 延迟:%s 秒I X ] { c ,\", name, MESSAGENO.getAndIncrement(), DateFormat.getDateTimeInstance().forma; = y R | ` l , }t(new Date()), time / 1000); // 生产消息
delayQm T 0ueue.put(new DelayedElement(message, time));
}
}
}).start();
}, . k 2 ~ T h p //消费者
private static void consumer(DelayQueue<Delay3 Y / D f iedElement> delayQueue) {
new Thread(new Runnable() {
@Override
public void runL R H k : X() {V @ ^ r z p d | 6
while (true) {
DelayedE[ p vlement element = null;
try { // 消费消息
element = delayQueue.ta+ 2 T # Lke();
System.out.println(e! ? t ilement);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start(); }
/s K T - K/ 延迟队列对象
static class DelayedElement implements Delayed {
// 过期时间(单位:毫秒)
long time = System.currentTimeMillis();
// 消息体
String message;
// 参数:delayTime 延迟时间(单位毫秒)
public DelayedElement(String message, long delayTime) {
this.time += delayTime;
this.messap ) ] V %ge = message;
}
@Override
// 获取过期时间 public long getDelay( C W s pTimeUnit unit) { returnk U 9 j c $ unit[ ! w . f h x.convert: p . ] . G e c u(time - System.currentTimeMillis(),y Z K F 3 TimeUni. { V # c $ + r vt.MILLISECONDS); } @Override // 队列元素排序 public int compareTo(Delayed o) { if (thisR s | Y @ L U H h.getDelay(TimeUnit.ME D z n fILLISECOND7 / E Q = ) US) >P - t w b O s; o.getDelay(TimeUnit.MILLISECONDS)) return 1; else if (this.getDelay(Ti4 1 z g D b #meUnit.MILLISECONDS) < o.] @ tgetDelay(TimeUnit.MILLISECONDS)) return -1; else return 0; } @Override public String toString() { // 打印消息 return message + \" |执行时间:\" + DateFormat.& R h k ] ) 1getDatej 6 f Q ~ 5 -TimC H reInstance().format(new Date()); } }}
以上程序支持多生产者,执行的结果如下:
生产者1,7 . r G _ [ g T j消息编号:1 发送时间:2019-6-12 20:38:37 延迟:2 秒 |执行时间:2019-6-12 20:38:39 生产者2,消息编号:2 发送时间:2019-6-12 20:38:37 延迟:2@ T U x 秒 |执行时间:2019-6-12 20:38:39 生产者1,消息编号:3 发送时间:2019-6^ 5 w-12 20:38:41 延迟:4 秒 |执行时间:2019-6-12 20:38:45 生产者1,消息编号:5 发送时间:2019-6-L ; n N H T ~ i12 20:38:43 延迟:2 秒 |执行时间:2019-6-12 20:38:45 ....../ N V _
总结
队列(Queue)按照是否阻塞可分为? p n ) P [ i r ]:阻塞队列 BlockingQueue 和 非阻塞队列。其中,双端队列 Deque 也属于非阻塞} P * $ ; o * ) H队列,双端队列除了拥有x d M ^ + 7 1队列的先进先出的方) f * F法之外,还拥有自己独有的方法,如 addFirst()、addLast()、getFirst()、getLast() 等,支持首未插入和删除元素。队列中比较常用的两个队列还有 PriorityQueue(优先d 1 ? X & ~ L级队列)和 DelayQueuec % % # : [(延迟队列),可使用延迟队列来实现延迟消息队列,这也是面试中比较常考的问题之一。需要面试朋友对延迟队X # C列一定要做到心中有数,动手写b i I % x y一个消息队列也是非常有必要的。