并发编程4:Java 阻塞队列源码分析(上)
来源:互联网 发布:淘宝6s官换机是真的吗 编辑:程序博客网 时间:2024/05/20 05:55
上篇文章 并发编程3:线程池的使用与执行流程 中我们了解到,线程池中需要使用阻塞队列来保存待执行的任务。这篇文章我们来详细了解下 Java 中的阻塞队列究竟是什么。
读完你将了解:
- 什么是阻塞队列
- 七种阻塞队列的前三种
- ArrayBlockingQueue
- 看它的主要属性
- 构造函数
- 四种添加元素方法的实现
- 四种获取元素的实现
- LinkedBlockingQueue
- LinkedBlockingQueue属性
- LinkedBlockingQueue添加元素
- LinkedBlockingQueue获取元素
- PriorityBlockingQueue
- 为什么是无界
- 如何保证优先级
- ArrayBlockingQueue
什么是阻塞队列
阻塞队列其实就是生产者-消费者模型中的容器。
当生产者往队列中添加元素时,如果队列已经满了,生产者所在的线程就会阻塞,直到消费者取元素时 notify 它;
消费者去队列中取元素时,如果队列中是空的,消费者所在的线程就会阻塞,直到生产者放入元素 notify 它。
具体到 Java 中,使用 BlockingQueue
接口表示阻塞队列:
- 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
可以看到,在队列操作(添加/获取)当前不可用时,BlockingQueue
的方法有四种处理方式:
- 抛出异常
- 对应的是
add()
,remove()
,element()
- 对应的是
- 返回某个值(null 或者 false)
offer()
,poll()
,peek()
- 阻塞当前线程,直到操作可以进行
put()
,take()
- 阻塞一段时间,超时后退出
offer
,poll()
总结下来如图所示:
BlockingQueue
中不允许有 null 元素,因此在 add()
, offer()
, put()
时如果参数是 null,会抛出空指针。null 是用来有异常情况时做返回值的。
七种阻塞队列的前三种
Java 中提供了 7 种 BlockingQueue
的实现,在看线程池之前我根本搞不清楚究竟选择哪个,直到完整地对比总结以后,发现其实也没什么复杂。
现在我们一起来看一下这 7 种实现。
1.ArrayBlockingQueue
ArrayBlockingQueue
是一个使用数组实现的、有界的队列,一旦创建后,容量不可变。队列中的元素按 FIFO 的顺序,每次取元素从头部取,加元素加到尾部。
默认情况下 ArrayBlockingQueue
不保证线程公平的访问队列,即在队列可用时,阻塞的线程都可以争夺访问队列的资格。
不保证公平性有助于提高吞吐量。
看它的主要属性:
- 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
- 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
可以看到,ArrayBlockingQueue
使用可重入锁 ReentrantLock
实现的访问公平性,两个 Condition
保证了添加和获取元素的并发控制。
构造函数:
- 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
- 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
可以看到,有三种构造函数:
- 默认的构造函数只指定了队列的容量,设置为非公平的线程访问策略
- 第二种构造函数中,使用
ReentrantLock
创建了 2 个Condition
锁 - 第三种构造函数可以在创建队列时,将指定的元素添加到队列中
四种添加元素方法的实现
第一种 add()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
add(E)
调用了父类的方法,而父类里调用的是 offer(E)
,如果返回 false 就泡出异常。
第二种 offer()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
可以看到 offer(E)
方法先拿到锁,如果当前队列中元素已满,就立即返回 false,这点比 add()
友好一些;
如果没满就调用 enqueue(E)
入队:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
可以看到,enqueue(E)
方法会将元素添加到数组队列尾部。
如果添加元素后队列满了,就修改 putIndex
为 0 ,0.0 为啥这样,先留着回头看。
添加后调用 notEmpty.signal()
通知唤醒阻塞在获取元素的线程。
第三种 put()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
可以看到,put()
方法可以响应中断,当队列满了,就调用 notFull.await()
阻塞等待,等有消费者获取元素后继续执行;
可以添加时还是调用 enqueue(E)
。
第四种 offer(E,long,TimeUnit)
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
可以看到 offer()
和 put()
方法很相似,不同之处在于允许设置等待超时时间,超过这么久如果还不能有位置,就返回 false;否则调用 enqueue(E)
,然后返回 true。
总体来看添加元素很简单嘛 ✧(≖ ◡ ≖✿)嘿嘿。
四种获取元素的实现:
第一种 poll()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
poll()
如果在队列中没有元素时会立即返回 null;如果有元素调用 dequeue()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
默认情况下 dequeue()
方法会从队首移除元素(即 takeIndex
位置)。
移除后会向后移动 takeIndex
,如果已经到队尾,就归零。结合前面添加元素时的归零,可以看到,其实 ArrayBlockingQueue
是个环形数组。
然后调用 itrs. elementDequeued()
,这个 itrs
是 ArrayBlockingQueue
的内部类 Itrs
的对象,看起来像是个迭代器,实际上它的作用是保证循环数组迭代时的正确性,具体实现比较复杂,这里暂不介绍。
第二种 take()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
take()
方法可以响应中断,与 poll()
不同的是,如果队列中没有数据会一直阻塞等待,直到中断或者有元素,有元素时还是调用 dequeue()
方法。
第三种 带参数的 poll()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
带参数的 poll()
方法相当于无参 poll()
和 take()
的中和版,允许阻塞一段时间,如果在阻塞一段时间还没有元素进来,就返回 null。
第四种 peek()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
peel()
方法很简单,直接返回数组中队尾的元素,并不会删除元素。如果队列中没有元素返回的是 null。
一波源码看下来,ArrayBlockingQueue
使用可重入锁 ReentrantLock
控制队列的访问,两个 Condition
实现生产者-消费者模型,看起来很简单的样子,这背后要感谢 ReentrantLock
和 Condition
的功劳!
2.LinkedBlockingQueue
LinkedBlockingQueue
是一个使用链表实现的、有界阻塞队列。队列的默认最大长度为 Integer.MAX_VALUE
,添加的元素按 FIFO 顺序。
LinkedBlockingQueue
属性:
- 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
- 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
可以看到,LinkedBlockingQueue
中有两个 ReentrantLock
,一个用于添加另一个用于获取,这和 ArrayBlockingQueue
不同。
LinkedBlockingQueue
构造函数:
- 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
- 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
三种构造函数也很简单,看注释就好了。
LinkedBlockingQueue
添加元素
第一种 put(E)
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
可以看到,LinkedBlockingQueue
使用了 AtomicInteger 类型的 count
保存队列元素个数,在添加时,如果队列满了就阻塞等待。
这时有两种继续执行的情况:
- 有消费者取元素,
count
会减少,小于队列容量 - 或者调用了
notFull.signal()
入队调用的 enqueue()
很简单,链表尾部添加节点即可:
- 1
- 2
- 3
- 1
- 2
- 3
在入队后,还会判断一次队列中的元素个数,如果此时小于队列容量,唤醒其他阻塞的添加线程。
最后还会判断容量,如果这时队列中没有元素,就通知 notEmpty
上阻塞的:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
这里我很懵逼啊,为什么没有元素了要告诉取元素阻塞的线程呢?
第二种 offer(E)
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
可以看到 offer
是一贯的直接返回结果。
第三种 offer(E,long,TimeUnit)
:
- 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
- 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
和 ArrayBlockingQueue
一样,带阻塞时间参数的 offer()
方法会阻塞一段时间,然后没结果就返回。
LinkedBlockingQueue
获取元素
第一种 take()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
注意:这里和前面一样,都使用的是 AtomicInteger.getAndDecrement() 方法,这个方法先返回当前值,然后加 1 ,所以后面判断是判断之前的情况。
队首元素出队:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
剩下的几种其实跟之前介绍的差不多,就懒得多费口舌了。
LinkedBlockingQueue
比 ArrayBlockingQueue
的优势就是添加和获取是两个不同的锁,所以并发添加/获取效率更高些,因此数组元素个数用的是 AtomicInteger
类型的,这样在添加、获取时通过判断数组元素个数可以感知到并发的获取/添加操作 ;此外就是链表比数组的优势了。
3.PriorityBlockingQueue
PriorityBlockingQueue
是基于数组的、支持优先级的、无界阻塞队列。
默认情况下队列中的元素按自然排序升序排列,我们可以实现元素的 compareTo()
指定元素的排序规则,或者在初始化它时在构造函数中传递 Comparator
排序规则。
不能保证同一优先级元素的顺序
这里就不再像前面那么详细地介绍源码了。
先看看 PriorityBlockingQueue
属性*:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
PriorityBlockingQueue
在初始化时创建指定容量的数组,默认是 11 。
为什么是“无界”
这不是有界吗,为什么说是无界阻塞队列呢,答案这里:
- 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
- 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
可以看到,几种添加方法最终都是调用了 offer()
方法,在添加元素时,当数组中元素大于等于容量时,调用 tryGrow()
扩容:
- 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
- 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
可以看到,在扩容时,如果当前队列中元素个数小于 64 个,数组容量就就乘 2 加 2;否则变成原来的 1.5 倍(原来容量越大,扩容成本越高,所以容量设置的小一点)。
如何保证优先级
再来看看 PriorityBlockingQueue
是如何保证优先级的:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
可以看到,在添加元素时,会根据队列中的定制排序 comparator
是否为空调用不同的排序方法。
不了解
Comparator
和Comparable
可以看这篇 Java 解惑:Comparable 和 Comparator 的区别。
如果没有设置 comparator
调用的是 siftUpComparable()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
可以看到,这里的排序有点最大堆的意思,每从队尾添加一个元素都会从下往上挨个比较自己和“父节点”的大小,如果小就交换,否则就停止。
比较使用的是队列元素中重写的 compareTo()
方法。
如果设置 comparator
调用的是 siftUpUsingComparator()
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
同样的比较,不同的是使用的是 Comparator.compare()
方法。
了解了添加时的排序,那获取元素时是如何保证按添加的顺序取出呢?以 poll() 为例
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
获取时主要调用的是 dequeue()
方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
出队时同样进行了两种不同的比较,我们选其中一种 siftDownComparable()
看一下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
可以看到,取元素时,在移除第一个元素后,会用堆排序将当前堆再排一次序。
经过源码分析我们了解了 PriorityBlockingQueue
为什么是无界、有优先级的队列了。因为它可以扩容,在添加、删除元素后都会进行排序。
由于篇幅原因,我们将阻塞队列分两篇介绍。下一篇介绍后四种队列。
- 并发编程4:Java 阻塞队列源码分析(上)
- 并发编程4:Java 阻塞队列源码分析(上)
- 并发编程5:Java 阻塞队列源码分析(下)
- 并发编程5:Java 阻塞队列源码分析(下)
- Java并发编程—阻塞队列源码分析
- JDK源码分析之主要阻塞队列实现类ArrayBlockingQueue -- java消息队列/java并发编程/阻塞队列
- Java并发编程(六)阻塞队列
- Java并发编程(六)阻塞队列
- Java并发编程:阻塞队列 (转载)
- Java 并发 --- 阻塞队列之ArrayBlockingQueue源码分析
- Java 并发 --- 阻塞队列之LinkedBlockingQueue源码分析
- Java 并发 --- 阻塞队列之PriorityBlockingQueuey源码分析
- Java 并发 --- 阻塞队列之DelayQueue源码分析
- Java 并发 --- 阻塞队列之SynchronousQueue源码分析
- Java 并发 --- 阻塞队列之LinkedTransferQueue源码分析
- Java 并发 --- 阻塞队列之LinkedBlockingDeque源码分析
- Java 并发 --- 非阻塞队列之ConcurrentLinkedQueue源码分析
- Java并发编程:阻塞队列
- Java基础之--多线程
- 关于冒泡排序法的优化
- DKOM隐藏驱动
- logback 常用配置详解(二) <appender>
- js数组去重
- 并发编程4:Java 阻塞队列源码分析(上)
- linux mint 安装后要装的东西
- java代码注释规范
- 20170828_字符串编辑距离_字符串相似度_DP
- csdn如何转载文章
- FSMC工程 ILI9325驱动LCD
- 使用贝塞尔曲线和Pathmeasure画粘连体
- Ubuntu14.04 流媒体服务器
- Junit的使用