💪🏻 制定明确可量化的目标,坚持默默的做事。
一、继承实现关系图
二、低层数据存储结构
2.1 主要属性
public class LinkedBlockingDeque<E> extends AbstractQueue<E> implements BlockingDeque<E>, java.io.Serializable { ... static final class Node<E> { E item; Node<E> prev; Node<E> next; Node(E x) { item = x; } } transient Node<E> first; transient Node<E> last; private transient int count; private final int capacity; final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); private final Condition notFull = lock.newCondition(); ... }
说明:
- Node: 基于链表实现的双向链表结构
- first: 始终指向链表头,本身不存储元素
- last: 指向链表尾
- count: 链表元素的数量
- capacity: 链表的容量
- lock: 存取对象锁(存取同一把锁)
- notEmpty: 队列非空阻塞和唤醒条件
- notFull: 队列是否已满阻塞和唤醒条件
2.2 构造器:
public LinkedBlockingDeque() { this(Integer.MAX_VALUE); } public LinkedBlockingDeque(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; } public LinkedBlockingDeque(Collection<? extends E> c) { ... }
说明:
- 默认无参构造器,容量为Intege.MAX_VALUE
- 指定队列容量构造器
- 初始化元素构造器,容量为Integer.MAX_VALUE
三、特点及优缺点
- 双向并发无界阻塞链表
- 线程安全
- 可从队头或队尾插入元素、可从队头或队尾删除元素
- 插入和删除元素是同一把锁,效率不高
- 通过ReentrantLock实现锁
- 利用Condition实现队列的阻塞等待唤醒
四、源码详解
核心入队方法:
- offerFirst(E e):添加头节点元素(不阻塞)
- offerLast(E e):添加尾节点元素(不阻塞)
- putFirst(E e):添加头节点元素,若添加不成功则阻塞直到被唤醒且添加成功
- putLast(E e):添加尾节点元素,若添加不成功则阻塞直到被唤醒且添加成功
- offerFirst(E e, long timeout, TimeUnit unit):添加头节点元素,若添加不成功则阻塞,直到被唤醒且添加成功或超时退出
- offerLast(E e, long timeout, TimeUnit unit):添加尾节点元素,若添加不成功则阻塞,直到被唤醒且添加成功或超时退出
- linkFirst(Node<E> node):头部插入(private)
- linkLast(Node<E> node):尾部插入(private)
PS:添加头节点最终调linkFirst,添加尾节点最终调linkLast,下面详细阅读这两个方法
4.1 linkFirst(Node<E> node)
private boolean linkFirst(LinkedBlockingDeque.Node<E> node) { // 当前容量大于队列最大容量时,返回插入失败 if (count >= capacity) return false; // 队列头结点 LinkedBlockingDeque.Node<E> f = first; // 原来的头结点作为 新插入结点的后一个结点 node.next = f; // 替换头结点 为新插入的结点 first = node; // 尾结点不存在,将尾结点置为当前新插入的结点 if (last == null) last = node; else // 原来的头结点的上一个结点为当前新插入的结点 f.prev = node; // 当前容量加1 ++count; // 唤醒读取时因队列中无元素而导致阻塞的线程 notEmpty.signal(); // 返回插入成功信息 return true; }
4.2 linkLas(Node<E> node)
private boolean linkLast(Node<E> node) { // 当前容量大于队列最大容量时,直接返回插入失败 if (count >= capacity) return false; // 获取尾节点 Node<E> l = last; // 将新插入的前一个节点指向原来的尾节点 node.prev = l; // 尾结点设置为新插入的结点 last = node; // 若头结点为空,新插入的结点作为头节点 if (first == null) first = node; else // 将原尾结点的下一个结点指向新插入的节点 l.next = node; // 当前容量加1 ++count; // 唤醒读取时因队列中无元素而导致阻塞的线程 notEmpty.signal(); // 返回插入成功信息 return true; }
核心出队方法:
- pollFirst():删除头节点(不阻塞)
- pollLast():删除尾节点(不阻塞)
- takeFirst():删除头节点,删除不成功则阻塞直到被唤醒且删除成功
- takeLast():删除尾节点,删除不成功则阻塞直到被唤醒且删除成功
- pollFirst(long timeout, TimeUnit unit):删除头节点,若删除不成功则阻塞直到被唤醒且删除成功或者超时退出
- pollLast(long timeout, TimeUnit unit):删除尾节点,若删除不成功则阻塞直到被唤醒且删除成功或者超时退出
- unlinkFirst():删除头节点(private)
- unlinkLast():删除尾节点(private)
PS:添加头节点最终调linkFirst,添加尾节点最终调linkLast,下面详细阅读这两个方法
4.3 unlinkFirst()
// 删除头结点 private E unlinkFirst() { // 获取当前头结点 Node<E> f = first; // 若头结点为空,直接返回空 if (f == null) return null; // 获取头结点的下一个结点 Node<E> n = f.next; // 获取头结点元素(记录return需要用到的删除了哪个元素) E item = f.item; // 将头结点元素置为null f.item = null; // 将头结点的下一个结点指向自己 方便gc f.next = f; // 设置头结点为原头结点的下一个结点 first = n; // 若原头结点的下一个结点不存在(队列中没有了结点) if (n == null) // 将尾结点也置为null last = null; else // 新的头结点的前一个结点指向null,因为他已经作为了头结点 所以不需要指向上一个结点 n.prev = null; // 当前数量减1 --count; // 唤醒因添加元素时队列容量满导致阻塞的线程 notFull.signal(); // 返回原来的头结点中的元素 return item; }
4.4 unlinkLast()
// 删除尾结点 private E unlinkLast() { // 获取当前尾结点 Node<E> l = last; // 若尾结点不存在,直接返回null if (l == null) return null; // 获取当前尾结点的上一个结点 Node<E> p = l.prev; // 获取当前尾结点中的元素,需要返回记录 E item = l.item; // 将当前尾结点的元素置为null l.item = null; // 并将当前尾结点的上一个结点指向自己,方便gc l.prev = l; // 设置新的尾结点,为原来尾结点的前一个结点 last = p; // 若无新的尾结点,头结点置为空(队列中没有了结点) if (p == null) first = null; else // 将新的尾结点的下一个结点指向null,因为他已经为尾结点所以不需要指向下一个结点 p.next = null; // 数量减1 --count; // 唤醒因添加元素时队列容量满导致阻塞的线程 notFull.signal(); // 返回原来的尾结点元素 return item; }
五、作用及使用场景
- 线程池阻塞队列
- 生产者消费者模式
- 任务调度FIFO和FILO