大家好,我是指北君。
在前面的文章中,已经对 ArrayBlockingQueue 进行了一次源码分析,对它的核心源码做了分析,今天来解析一波同为 BlockingQueue 家族中的一员的 LinkedBlockingQueue。它的底层基于单向链表实现。
先看一看它的 Node 内部类和主要属性、构造函数。
Node
1 |
|
Node 是 LinkedBlockingQueue 的基石。 它如第一张图所示的一个单向链表形式的内部类,item 是当前节点的内容,next 指向的是下一个 Node 节点。
属性
1 |
|
- 从属性中就可以看出 LinkedBlockingQueue 和 ArrayBlockingQueue 的差异点: ArrayBlockingQueue 只有一把 ReentrantLock 锁,入队和出队相互互斥。 LinkedBlockingQueue 分了出队锁 takeLock 和入队锁 putLock,两把锁相互不阻塞。
- capacity 是 LinkedBlockingQueue 的容量,表示 LinkedBlockingQueue 是一个有界队列。
构造函数
LinkedBlockingQueue 提供了三个构造函数。
LinkedBlockingQueue()
1 |
|
LinkedBlockingQueue() 构造函数直接调用带参数的构造函数,参数被设置为 2 的 31 次方 - 1
LinkedBlockingQueue(int capacity)
1 |
|
LinkedBlockingQueue(int capacity) 构造函数做了三件事:
- 先检查参数是否大于 0,不大于 0 就抛出异常。
- 设置 capacity 容量为参数大小。
- 构造了一个 null 节点,赋值给 last 和 head。
- head 的 item 元素永远是一个 null。
LinkedBlockingQueue(Collection<? extends E> c)
1 |
|
- LinkedBlockingQueue(Collection<? extends E> c) 调用了 LinkedBlockingQueue(int capacity) 构造函数并将参数设置成了最大值。
- putLock 加锁后,将集合中的元素一个个遍历并且入队。
- 设置元素的数量就是集合中元素的数量。
- 在遍历元素时,会将 null 元素抛出异常并且检查队列是否满了。
入队
LinkedBlockingQueue 有多种入队方法,当队列满了之后它们的处理方法各不相同。在入队之前和入队之后的操作都是相同的。
offer
1 |
|
offer() 方法做了以下几件事情:
- 检查需要入队的元素是否为 null。
- 判断队列是否满了,满了就返回 false。
- 队列没有满,创建一个新的 Node 节点。
- putLock 锁加锁,不让其他线程操作队列。
- 当队列没有满队的时候,将元素入队并且将局部变量 c 设置为入队之前元素的数量,元素数量 + 1。
- 再次判断队列是否满了,如果队列中还有空位,则唤醒正在阻塞的入队线程。这些阻塞的线程来自 put()、offer(E e, long timeout, TimeUnit unit) 方法。
- 释放 putLock 锁
- 当入队之前是一个空队列的时候,调用 signalNotEmpty() 方法开启 takeLock 出队锁,将阻塞在 notEmpty 条件队列中的线程唤醒。
enqueue() 方法的源码比较简单,就是将 last 节点的 next 指向需要入队的元素,如下图所示。
add()
1 |
|
add() 方法调用的是 offer() 方法,它在队列满了的时候不是返回 false 而是抛出一个 Queue full
异常。
put()
1 |
|
put() 方法和 offer()、and() 的方法大致相同,不同的是对队列满了之后的操作,offer() 是直接返回 false,and() 是抛出异常,put() 则是将线程加入到 notFull 条件队列中阻塞入队线程。
offer(E e, long timeout, TimeUnit unit)
这是一个带超时时间的 offer() 方法。
1 |
|
这个方法是在一定时间内元素等待入队,就是在 timeout 时间内队列中有空余位置就将元素加入单向链表的队尾,如果在 timeout 时间内元素还没有入队,就返回 false。
入队总结
LinkedBlockingQueue 的入队一共有 offer()、add()、put()、offer(E e, long timeout, TimeUnit unit) 四种方法。这四种方法在队列满了之后的处理是不同的:
- offer() 方法在队列满了之后就返回 false。
- add() 方法在内部调用的是 offer() 方法,当队列满了就抛出
Queue full
异常。 - put() 方法在队列满了之后会将线程加入 notFull 中,等待被唤醒后加入队列。
- offer(E e, long timeout, TimeUnit unit) 方法在队列满了之后会等待一段 timeout 的时间,在这时间内入队就返回 true,在这段时间内未能入队就返回 false。
- 每个方法在入队完后都会唤醒在 notEmpty 队列中等待出队的线程。
出队
LinkedBlockingQueue 的出队也有几种不同的方法,它们对于空队列的处理方式各不相同。
poll()
1 |
|
poll() 方法在出队的时候做了以下几件事情:
- 先取出队列中元素的数量
- 队列的非空检查,当队列是空的,则返回 false。
- 初始化一个局部的元素变量。
- takeLock 出队锁加锁,不让其他线程操作队列的出队。
- 当队列中有元素的时候,将队列中的第一个元素弹出。
- 判断队列中是否还有元素,唤醒阻塞在 notEmpty 条件队列上的线程。
- takeLock 出队锁解锁
- 在原队列是满队的情况下,唤醒阻塞在 notFull 条件队列上的线程。
dequeue() 方法会将 LinkedBlockingQueue 中第一个元素取出。取的并不是 head 中的item,而是 head.next 中的 item。
poll(long timeout, TimeUnit unit)
1 |
|
poll(long timeout, TimeUnit unit) 方法是在一定时间内出队还未取到元素就阻塞线程,时间到了还没取到元素就返回 null,并不会一直阻塞线程。
take()
1 |
|
take() 方法和 poll() 最大的不同就是在空队列的时候会一直阻塞线程,poll() 则返回 null,poll(long timeout, TimeUnit unit) 则在一定时间内阻塞线程,超时后返回的 null。
remove()
1 |
|
remove() 并不会弹出元素,它是删除一个元素。遍历整个单向链表,找到需要删除的元素后,将元素前一个节点的next 指向删除元素的 next。将需要删除的元素设置为 null。
peek()
1 |
|
peek() 方法仅仅是取出第一个元素,没有修改节点的任何一个 next 属性,所以并不会将元素从队列中移除。
出队总结
LinkedBlockingQueue 的出队一共有 poll()、take()、poll(long timeout, TimeUnit unit) 三种方法,移除元素用 remove() 方法,取出第一个元素用 peek() 方法。
出队方法在遇到空队列的时候操作不同:
- poll() 方法遇到空队列就返回 null。
- take() 方法遇到空队列就将线程加入到 notEmpty 条件队列中并且阻塞线程。
- poll(long timeout, TimeUnit unit) 方法在遇到空队列就阻塞一段时间,这期间没获取到元素就返回 null。
总结
- LinkedBlockingQueue 是基于单向链表的,线程安全的。
- 是一个有界的队列,最大的容量是最大的 int 值。
- 出队入队基于两把锁。互不阻塞。
LinkedBlockingQueue 被用在线程池中,也可以用在生产者-消费者模式中。
1 |
|
我是指北君,操千曲而后晓声,观千剑而后识器。感谢各位人才的:点赞、收藏和评论,我们下期更精彩!