目录
1.LinkedBlockingQueue
1.1整体架构
- 主要实现了BlockingQueue和Queue接口
- Queue接口包含了:
遇到队列满或空的时候,抛异常,如 add、remove、element;
遇到队列满或空的时候,返回特殊值,如 offer、poll、peek。
- BlockingQueue接口包含了:
遇到队列满或空的时候,抛异常,如 add、remove、element;
遇到队列满或空的时候,返回特殊值/阻塞一段时间如 offer、poll、peek(不会进行阻塞,直接返回);
遇到队列满或空的时候,一直阻塞,如 put、take;
- 底层数据结构使用链表,链表大小可以在初始化进行设置,默认是int的最大值
// 链表结构 begin
//链表的元素
static class Node<E> {
E item;
//当前元素的下一个,为空表示当前节点是最后一个
Node<E> next;
Node(E x) { item = x; }
}
//链表的容量,默认 Integer.MAX_VALUE
private final int capacity;
//链表已有元素大小,使用 AtomicInteger,所以是线程安全的
private final AtomicInteger count = new AtomicInteger();
//链表头
transient Node<E> head;
//链表尾
private transient Node<E> last;
// 链表结构 end
// 锁 begin
//take 时的锁
private final ReentrantLock takeLock = new ReentrantLock();
// take 的条件队列,condition 可以简单理解为基于 ASQ 同步机制建立的条件队列
private final Condition notEmpty = takeLock.newCondition();
// put 时的锁,设计两把锁的目的,主要为了 take 和 put 可以同时进行
private final ReentrantLock putLock = new ReentrantLock();
// put 的条件队列
private final Condition notFull = putLock.newCondition();
// 锁 end
// 迭代器
// 实现了自己的迭代器
private class Itr implements Iterator<E> {
………………
}
1.2初始化源码解析
// 不指定容量,默认 Integer 的最大值
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE); //调用指定容量初始化
}
// 指定链表容量大小,链表头尾相等,节点值(item)都是 null
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0)
this.capacity = capacity;
// 头节点和尾节点永远不为空,会有一个head节点一直会指向这个哨兵节点!!!
last = head = new Node<E>(null);
}
// 已有集合数据进行初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);//调用指定容量初始化
final ReentrantLock putLock = this.putLock;
putLock.lock(); // 获取锁
try {
int n = 0;
for (E e : c) {
// 集合内的元素不能为空
if (e == null)
throw new NullPointerException();
// capacity 代表链表的大小,在这里是 Integer 的最大值
// 如果集合类的大小大于 Integer 的最大值,就会报错
// 其实这个判断完全可以放在 for 循环外面,这样可以减少 Integer 的最大值次循环(最坏情况)
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
第一种:不指定初始容量默认为int的最大值
第二种:指定初始值大小,
第三种:指定集合来进行初始化,需要加put锁来保证线程安全性
ps:三种初始化其实都是指定大小的初始化,只不过进行了封装,头节点永远的会指向一个空值的哨兵节点,可以简化编程的复杂程度!!!
1.3阻塞新增源码解析
// 把e新增到队列的尾部。
public void put(E e) throws InterruptedException {
// e 为空,抛出异常
if (e == null) throw new NullPointerException();
// 预先设置 c 为 -1,约定负数为新增失败
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock; //获取锁
final AtomicInteger count = this.count; //获取节点值
putLock.lockInterruptibly(); // 可中断锁
try {
// 队列满了
// 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后一定会唤醒此处被阻塞的线程)
while (count.get() == capacity) {
// await 无限等待
notFull.await();
}
// 队列没有满,直接新增到队列的尾部
enqueue(node);
// 新增计数赋值,注意这里 getAndIncrement 返回的是旧值
// 这里的 c 是比真实的 count 小 1 的
c = count.getAndIncrement();
// 如果链表现在的大小 小于链表的容量,说明队列未满
// 可以尝试唤醒一个 put 的等待线程
if (c + 1 < capacity)
notFull.signal();
} finally {
// 释放锁
putLock.unlock();
}
// c==0,代表队列里面有一个元素,会尝试唤醒一个take的等待线程
if (c == 0)
signalNotEmpty();
}
// 入队,把新元素放到队尾
private void enqueue(Node<E> node) {
last = last.next = node; //很简洁,第二个等号赋值后会返回node的值
}
获取到put锁,获取到原子类型count(底层使用的CAS操作),并且设置锁可中断,如果队列已满就会进行无期限等待(需要手动的进行唤醒)在进行尾部添加节点并且累加count值,如果链表没满会尝试唤醒一个put线程,然后释放锁。
最后在判断增加后队列是否只有一个值,如果是的话需要进行唤醒一个take线程
ps:尾部添加元素由于有了哨兵节点(head节点)变得非常优雅简介!否则是需要判断空链表,进行更新head节点的。
1.4阻塞删除源码分析
// 阻塞拿数据
public E take() throws InterruptedException {
E x;
// 默认负数,代表失败
int c = -1;
// count 代表当前链表数据的真实大小
final AtomicInteger count = this.count;// 获取原子类型进行操作
final ReentrantLock takeLock = this.takeLock;// 获取take锁
takeLock.lockInterruptibly();// 可中断锁
try {
// 空队列时,会被阻塞,等待其他线程唤醒
while (count.get() == 0) {
notEmpty.await();
}
// 非空队列,从队列的头部拿一个出来
x = dequeue();
// 原子操作减一,返回的值是旧值
c = count.getAndDecrement();
// 如果队列里面有值,从 take 的等待线程里面唤醒一个。
if (c > 1)
notEmpty.signal();
} finally {
// 释放锁
takeLock.unlock();
}
// 已经删除了一个元素,
// 如果队列空闲还剩下一个,尝试从 put 的等待线程中唤醒一个
if (c == capacity)
signalNotFull();
return x;
}
// 队头中取数据
// 每次更新的实际是哨兵节点,是哨兵节点不停地在移动
// 把原本的哨兵节点变为循环引用,然后更新哨兵为下一个元素,并将此节点变为哨兵节点
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // 当前哨兵节点变为循环引用,help GC
head = first;
E x = first.item;// 返回删除节点的元素
first.item = null;// 更新下一个节点为哨兵节点
return x;
}
整个过程和put方法大致类似,取头节点较为复杂(每次实际为哨兵节点在不停的移动)
获取take锁,获取原子类型队列的count,进行加锁并且设置锁为可中断,如果队列为空会无限阻塞(直到有人唤醒),然后从头节点获取值并且删除该节点,如果队列还有值会尝试唤醒一个take线程,然后释放锁,
如果队列在取元素之前是满的那么会尝试唤醒一个put线程,最后返回节点的值。
1.5查看元素源码分析
// 查看并不删除元素,如果队列为空,返回 null
public E peek() {
// count 代表队列实际大小,队列为空,直接返回 null
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
// 拿到队列头
Node<E> first = head.next;
// 判断队列头是否为空,并返回
// 不会被进行阻塞
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
只是取元素但不进行删除,还是会获取take锁防止节点被改变,主要就是进行了判断队列的空条件,不会被阻塞!