上一篇 并发编程4:Java 阻塞队列源码分析(上) 我们了解了 ArrayBlockingQueue
, LinkedBlockingQueue
和 PriorityBlockingQueue
,这篇文章来了解剩下的四种阻塞队列。
读完本文你将了解:
- 七种阻塞队列的后四种
- DelayQueue
- DelayQueue的关键属性
- 实现Delayed接口
- 延时阻塞队列如何实现
- DelayQueue使用场景
- SynchronousQueue
- LinkedTransferQueue
- TransferQueue
- tryTransfer和transfer
- LinkedBlockingDeque
- 关键属性
- DelayQueue
- 四种阻塞队列的特点
- 总结
- Thanks
七种阻塞队列的后四种
DelayQueue
DelayQueue
是一个支持延时获取元素的、无界阻塞队列。
队列使用 PriorityQueue
实现,队列中的元素必须实现 Delayed
接口:
- 1
- 2
- 1
- 2
Delayed
接口:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
可以看到,实现 Delayed
的类也需要实现 Comparable
接口,即实现 compareTo()
方法,保证集合中元素的顺序和 getDelay()
一致。
因此创建元素时可以指定多久才能从队列中获取当前元素。
DelayQueue 的关键属性
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
可以看到,DelayQueue
的属性只有四个,却都不简单:
- ReentrantLock lock
- 读写锁
- PriorityQueue q
- 无界的、优先级队列
- Thread leader
- Leader-Follower 模型中的 leader
- Condition available
- 队首有新元素可用或者有新线程成为 leader 时触发的 condition
简单介绍下关键属性。
1 PriorityQueue
是一个用数组实现的,基于二叉堆(元素[n] 的子孩子是 元素[2*n+1] 和元素[2*(n+1)] )数据结构的集合。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在添加元素时如果超出限制也会扩容:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
所以是无界的。
2.Leader-Follower 模型
这种模型中所有线程会有三种身份中的一种:leader、follower,以及一个干活中的状态:proccesser。
它的基本原则就是,永远最多只有一个 leader。而所有 follower 都在等待成为 leader。
线程池启动时会自动产生一个 Leader 负责等待事件,当有一个事件产生时,Leader 线程首先通知一个 Follower 线程将其提拔为新的 Leader,然后自己就去干活了,去处理这个事件。处理完毕后加入 Follower 线程等待队列,等待下次成为 Leader。
这种方法可以增强 CPU 高速缓存相似性,及消除动态内存分配和线程间的数据交换。这种模式是为了最小化任务等待时间,当一个线程成为 leader 后,它只需要等待下一个可执行任务的出现,而其他线程要无限制地等待。
实现 Delayed
接口
前面提到了,DelayQueue
的元素必须实现 Delayed
接口,我们以 JDK 中的 ScheduledFutureTask
为例,看下如何实现:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
可以看到,实现 Delayed
接口大概有三步:
- 构造函数中初始化基本数据,比如执行时间等数据
- 实现
getDelay()
方法,返回当前元素还需要延时多久执行 - 实现
compareTo()
方法,指定不同元素如何比较谁先执行
延时阻塞队列如何实现
DelayQueue
中只有延迟时间到了才能从队列中取出元素。
那这个是怎么实现的呢?我们看一下获取元素的实现,以 take()
为例:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
可以看到,在取元素时,会根据元素的延时执行时间是否为 0 进行判断,如果延时执行时间已经没有了,就直接返回;否则就要等待执行时间到达后再返回。其中的 Leader-Follower 模型的调度过程这里就不分析了,越分析内容越多 - -。
DelayQueue
使用场景:
- 缓存系统的设计
- 用
DelayQueue
保存元素的有效期,用一个线程来循环查询DelayQueue
,能查到元素,就说明缓存的有效期到了
- 用
- 定时任务调度
- 用
DelayQueue
保存定时执行的任务和执行时间,同样有一个循环查询线程,获取到任务就执行 TimerQueue
就是使用DelayQueue
实现的
- 用
SynchronousQueue
SynchronousQueue
支持公平访问队列,根据构造函数的参数不同,有两种实现方式:TransferQueue
和 TransferStack
,默认情况下是 false:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
SynchronousQueue
是一个不存储元素的阻塞队列。
这里的“不存储元素”指的是,SynchronousQueue
容量为 0,每添加一个元素必须等待被取走后才能继续添加元素。
我们看下它的 put()
的实现:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
可以看到,它的添加是调用的 transferer.transfer()
,如果返回 null 就调用 Thread.interrupted()
将中断标志位复位(设为 false),然后抛出异常。
看下 TransferStack.transfer()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
逻辑比较复杂,主要就是三步:
- 栈是空的或者栈顶元素的模式和当前要进行的操作一致
- 将节点推到堆栈上并等待匹配
- 等待参数中的时间后返回
- 如果取消就返回 null
- 如果栈不为空且栈顶元素模式与当前要进行的操作不一致,如果这个元素的模式是相反的模式(取对应放)
- 尝试将栈中一个模式匹配要求的节点推到堆栈上,与相应的等待节点匹配并返回
- 如果栈顶已经拥有另一个模式 匹配的节点
- 通过执行 POP 操作来找到匹配的元素,然后继续
看着有点晕,简单概括就是一个添加操作后必须等待一个获取操作才可以继续添加。
SynchronousQueue
的吞吐量高于 LinkedBlockingQueue
和 ArrayBlockingQueue
,有位前辈做了测试,可以点击 这篇文章 查看。这里引用一下结论:
LinkedBlockingQueue 性能表现远超 ArrayBlcokingQueue,不管线程多少,不管 Queue 长短,LinkedBlockingQueue 都胜过 ArrayBlockingQueue。
SynchronousQueue 表现很稳定,而且在 20 个线程之内不管 Queue 长短,SynchronousQueue 性能表现是最好的,(其实SynchronousQueue 跟 Queue 长短没有关系),如果 Queue 的 capability 只能是 1,那么毫无疑问选择 SynchronousQueue,这也是设计 SynchronousQueue 的目的吧。
但大家也可以看到当超过 1000 个线程时,SynchronousQueue 性能就直线下降了,只有最高峰的一半左右,而且当 Queue 大于 30 时,LinkedBlockingQueue 性能就超过 SynchronousQueue。
相较于其他队列有缓存的作用,SynchronousQueue
适用于单线程同步传递性场景,比如:消费者没拿走当前的产品,生产者是不能再给产品的,这样可以控制生产者生产的速率和消费者一致。
LinkedTransferQueue
LinkedTransferQueue
实现了 TransferQueue
接口, 是一个由链表组成的、无界阻塞队列。
- 1
- 2
- 1
- 2
TransferQueue
TransferQueue
也是一种阻塞队列,它用于生产者需要等待消费者消费事件的场景,与前面一节的 SynchronousQueue
有相似之处。它定义的方法如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
tryTransfer() 和 transfer()
相对于其他阻塞队列,LinkedTransferQueue
多了两个关键地方法:tryTransfer()
和 transfer()
。
分别来看看它是如何实现的。
1.transfer()
transfer()
方法的作用是:如果有等待接收元素的消费者线程,直接把生产者传入的元素 transfer 给消费者;如果没有消费者线程,transfer()
会将元素存放到队列尾部,并等待元素被消费者取走才返回:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
awaitMatch()
方法的作用是:CPU 自旋等待消费者取走元素,为了避免长时间消耗 CPU,在自旋一定次数后会调用 Thread.yield()
暂停当前正在执行的线程,改为执行其他线程。
2.tryTransfer()
tryTransfer()
的作用是:试探生产者传入的元素是否能 直接传递给消费者。
- 如果有等待接收的消费者,返回 true
- 没有则返回 false
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
可以看到,和 transfer()
必须等到消费者取出元素才返回不同的是,tryTransfer()
无论是否有消费者接收都会立即返回。
LinkedBlockingDeque
LinkedBlockingDeque
是一个由链表组成的、双向阻塞队列。
关键属性
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
可以看到,LinkedBlockingDeque
中持有队列首部和尾部节点,每个节点也是双向的。
双向的作用是:可以从队列两端插入和移除元素。多了一个操作队列的方向,在多线程同时入队时,可以减少一半的竞争。
除了 remove(Object)
等移除操作,LinkedBlockingDeque
的大多数操作的时间复杂度都是 O(n)。
LinkedBlockingDeque
多了获取和查询的 XXXFirst
和 XXXLast
的方法。
7 种阻塞队列的特点
这篇文章介绍的 4 种加上上一篇 细说并发4:Java 阻塞队列源码分析(上) 中 3 种,总共 7 种阻塞队列,这么多队列看的眼都花了。
这里简单总结下 Java 中 7 种阻塞队列的特点:
- ArrayBlockingQueue
- 环形数组实现的、有界的队列,一旦创建后,容量不可变
- 基于数组,在添加删除上性能还是不如链表
- LinkedBlockingQueue:
- 基于链表、有界阻塞队列
- 添加和获取是两个不同的锁,所以并发添加/获取效率更高些
Executors.newFixedThreadPool()
使用了这个队列
- PriorityBlockingQueue
- 基于数组的、支持优先级的、无界阻塞队列
- 使用自然排序或者定制排序指定排序规则
- 添加元素时,当数组中元素大于等于容量时,会扩容(当前队列中元素个数小于 64 个,数组容量就乘 3;否则就乘 2 加 2),拷贝数组
- DelayQueue
- 支持延时获取元素的、无界阻塞队列
- 添加元素时如果超出限制也会扩容
- Leader-Follower 模型
- SynchronousQueue
- 容量为 0
- 一个添加操作后必须等待一个获取操作才可以继续添加
- 吞吐量高于
LinkedBlockingQueue
和ArrayBlockingQueue
- LinkedTransferQueue
- 由链表组成的、无界阻塞队列
- 实现了
TransferQueue
接口 - CPU 自旋等待消费者取走元素,自旋一定次数后结束
- LinkedBlockingDeque
- 由双向链表组成的、双向阻塞队列
- 可以从队列两端插入和移除元素
- 多了一个操作队列的方向,在多线程同时入队时,可以减少一半的竞争
总结
在实际开发中可能接触不到阻塞队列,线程池或者其他池都将这些细节封装好了,但是在看一些开源框架的时候经常看到有使用它们,因此如果想要自己写牛逼的框架,这些底层的东西还是需要了解的。
我们结合源码和《Java 并发编程的艺术》相关章节分两篇文章介绍了 Java 中的阻塞队列,了解了 7 种阻塞队列的大致源码实现,后面遇到需要使用阻塞队列时心里应该有些底了。
学基础就是这样,不能指望立即有用,古话说得好:无用之用是为大用,不一定哪天就派上用场了!
Thanks
《Java 并发编程的艺术》
http://blog.csdn.net/goldlevi/article/details/7705180
http://stevex.blog.51cto.com/4300375/1287085/