基于JDK1.8详细介绍了LinkedBlockingQueue的底层源码实现,包括锁分离的原理,以及入队列、出队列等操作源码。实际上LinkedBlockingQueue的源码还是非常简单的!
文章目录
1 LinkedBlockingQueue的概述
public class LinkedBlockingQueue< E >
extends AbstractQueue< E >
implements BlockingQueue< E >, Serializable
LinkedBlockingQueue来自于JDK1.5的JUC包,是一个支持并发操作的有界阻塞队列,底层数据结构是一个单链表。
作为有界队列,容量范围是[1, Integer.MAX_VALUE],可以指定容量,如果未指定容量,则默认容量等于 Integer.MAX_VALUE,即最大容量。
由于消费线程只操作队头,而生产线程只操作队尾,这里巧妙地采用了两把锁,对插入数据采用putLock,对移除数据采用takeLock,即生产者锁和消费者锁,这样避免了生产线程和消费线程竞争同一把锁的现象(比如ArrayBlockingQueue就只用了一把锁),因此LinkedBlockingQueue在高并发的情况下,性能会比ArrayBlockingQueue好很多,但是在需要遍历整个队列的情况下则要把两把锁都锁住(比如clear、contains操作)。
LinkedBlockingQueue的工作模式都是非公平的,也不能手动指定为公平模式,即获取锁的实际线程顺序不能保证是等待获取锁的线程顺序,这样的好处是可以提升并发量!
实现了Serializable接口,支持序列化,没有实现Cloneable,不支持克隆!
不支持null元素!
2 LinkedBlockingQueue的原理
2.1 主要属性
由于采用链表结构来保存数据,因此具有头、尾结点的引用head、last,链表结点类型是内部类Node类型。由于是一个有界队列,容量使用capacity变量来保存,capacity是int类型的,因此LinkedBlockingQueue的容量最大是Integer.MAX_VALUE。使用一个AtomicInteger类型的原子变量count来作为计数器,它是线程安全的。
具有两把锁takeLock、putLock,takeLock作为消费线程获取的锁,同时有个对应的notEmpty条件变量用于消费线程的阻塞和唤醒,putLock作为生产线程获取的锁,同时有个对应的notFull条件变量用于生产线程的阻塞和唤醒!
/**
* 阻塞队列的容量,默认为Integer.MAX_VALUE,最大为Integer.MAX_VALUE
*/
private final int capacity;
/**
* 阻塞队列的元素个数,原子变量
*/
private final AtomicInteger count = new AtomicInteger();
/**
* 阻塞队列的头结点,并不是真正的头结点
*/
transient Node<E> head;
/**
* 阻塞队列的尾结点
*/
private transient Node<E> last;
/**
* 消费线程使用的锁
*/
private final ReentrantLock takeLock = new ReentrantLock();
/**
* notEmpty条件对象,当队列为空时用于挂起消费线程
*/
private final Condition notEmpty = takeLock.newCondition();
/**
* 生产线程使用的锁
*/
private final ReentrantLock putLock = new ReentrantLock();
/**
* notFull条件对象,当队列已满时用于挂起生产线程
*/
private final Condition notFull = putLock.newCondition();
/**
* 链表的结点内部类,用于存储数据
*/
static class Node<E> {
/**
* 数据域
*/
E item;
/*
* 后继引用
*/
Node<E> next;
/**
* 构造器
*
* @param x 值
*/
Node(E x) {
item = x;
}
}
2.2 构造器
在构造器中会对LinkedBlockingQueue的内部队列进行简单的初始化,即头尾结点都指向同一个值为null的哨兵结点。
2.2.1 LinkedBlockingQueue()
public LinkedBlockingQueue()
创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue。
/**
* 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue。
*/
public LinkedBlockingQueue() {
//内部调用另一个构造器
this(Integer.MAX_VALUE);
}
2.2.2 LinkedBlockingQueue(capacity)
public LinkedBlockingQueue(int capacity)
创建一个具有指定容量的 LinkedBlockingQueue。如果 capacity 小于 1,则抛出IllegalArgumentException。
/**
* 创建一个具有指定容量的 LinkedBlockingQueue。
*
* @param capacity 指定容量
* @throws IllegalArgumentException 如果 capacity 小于 1
*/
public LinkedBlockingQueue(int capacity) {
//capacity大小校验
if (capacity <= 0) throw new IllegalArgumentException();
//capacity赋值
this.capacity = capacity;
//初始化头结点和尾节点,指向同一个值为null的哨兵结点
last = head = new Node<E>(null);
}
2.2.3 LinkedBlockingQueue( c )
public LinkedBlockingQueue(Collection<? extends E> c)
创建一个容量是 Integer.MAX_VALUE 的 LinkedBlockingQueue,包含指定集合的全部元素,元素按该集合迭代器的遍历顺序添加。
如果指定集合为或任意元素为null,则抛出NullPointerException。如果指定集合元素数量超过Integer.MAX_VALUE,那么抛出IllegalStateException。
/**
* 创建一个容量是 Integer.MAX_VALUE 的 LinkedBlockingQueue,最初包含指定集合的远不元素,元素按该集合迭代器的遍历顺序添加。
*
* @param c 指定集合
* @throws NullPointerException 如果指定集合为或任意元素为null
*/
public LinkedBlockingQueue(Collection<? extends E> c) {
//调用另一个构造器,初始化队列,容量为Integer.MAX_VALUE
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
//这里和ArrayBlockingQueue是一样的,需要加锁来保证数据的可见性,因为头、尾结点没有使用volatile修饰
//获取生产者锁
putLock.lock(); // Never contended, but necessary for visibility
try {
//n作为计数器
int n = 0;
//遍历指定集合
for (E e : c) {
//null校验
if (e == null)
throw new NullPointerException();
//容量校验
if (n == capacity)
throw new IllegalStateException("Queue full");
//调用enqueue方法插入新结点到队列尾部
enqueue(new Node<E>(e));
//计数器自增1
++n;
}
//设置队列的元素数量
count.set(n);
} finally {
//释放生产者锁
putLock.unlock();
}
}
/**
* 指定结点链接到队列尾部成为新的尾结点,在获取锁之后才会调用该方法
*
* @param node 指定结点
*/
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
//很简单,原尾结点的next引用指向node结点,然后last指向最新node结点
last = last.next = node;
}
2.3 入队操作
2.3.1 put(e)方法
public void put(E e)
将指定的元素插入此队列的尾部,如果该队列已满,则线程等待。
如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。另外,如果指定元素为 null则抛出NullPointerException 异常。
在ArrayBlockingQueue中,生产(放入数据)线程阻塞的时候,需要消费(移除数据)线程才能唤醒,并且因为它们获取的同一个锁,消费和生产不能并发进行(假设一个线程仅仅从事生产或者消费工作的一种)。在LinkedBlockingQueue中,如果有线程因为获取不到锁或者队列已满而导致生产(放入数据)线程阻塞,那么他可能被后面的消费线程唤醒也可能被后面的生产线程唤醒。因为它内部有两个锁,生产和消费获取不同的锁,可以并行执行生产和消费任务,不仅在消费数据的时候会唤醒阻塞的生产线程,在生产数据的时候如果队列容量还没满,也会唤醒此前阻塞的生产线程继续生产。
大概步骤为:
- 指定元素e的null校验;
- 新建结点node,lockInterruptibly可中断的等待获取生产者锁putLock,即响应中断;没有获取到锁就在同步队列中阻塞等待,被中断了则直接中断等待并抛出异常;
- 获取到锁之后,while循环判断此时结点数量是否等于容量,即队列是否满了,如果满了,那么该线程在notFull条件队列中等待并释放锁,被唤醒之后会继续尝试获取锁、并循环判断;
- 队列没有满,node结点添加到链表尾部成为新的尾结点;
- 获取此时计数器的值赋给c,并且计数器值自增1;
- 如果c+1小于capacity,说明此时队列未满,还可以入队,那么唤醒一个在notFull条件队列中等待的生产线程;
- 释放生产者锁putLock;
- 如果前面没有发生异常,那么执行最后的if语句:如果c为0,那么此时队列中还可能有存在1条数据,刚放进去的那么由于刚才队列没有数据,可能此时有消费者线程在等待,这里需要唤醒一个消费者线程。如果此前队列中就有数据没有消费完毕,那么也不必唤醒唤醒消费者。注意这里唤醒消费者线程的时候,必须先获取Condition关联的消费者锁!
/**
* 将指定的元素插入此队列的尾部,如果该队列已满,则线程等待。
*
* @param e 指定元素
* @throws InterruptedException 如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断
* 如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
* @throws NullPointerException 如果指定元素为 null
*/
public void put(E e) throws InterruptedException {
//e的null校验
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
//新建结点
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//可中断的等待获取生产者锁,即响应中断
putLock.lockInterruptibly();
try {
/*
* while循环判断此时结点数量是否等于容量,即队列是否满了
*/
while (count.get() == capacity) {
//如果满了,那么该线程在notFull条件队列中等待并释放锁,被唤醒之后会继续尝试获取锁、并循环判断
notFull.await();
}
// 队列没有满,结点添加到链表尾部
enqueue(node);
//获取此时计数器的值赋给c,并且计数器值自增1
c = count.getAndIncrement();
//如果c+1小于capacity,说明还可以入队
if (c + 1 < capacity)
//唤醒一个在notFull条件队列中等待的生产线程
notFull.signal();
} finally {
//释放生产者锁
putLock.unlock();
}
//如果前面没有抛出异常,那么在finally之后会执行下面的代码
//如果c为0,那么此时队列中还可能有存在1条数据,刚放进去的
//那么由于刚才队列没有数据,可能此时有消费者线程在等待,这里需要唤醒一个消费者线程
//如果此前队列中就有数据没有消费完毕,那么也不必唤醒唤醒消费者
if (c == 0)
//获取消费者锁并且尝试唤醒一个消费者线程
signalNotEmpty();
}
/**
* 指定结点链接到队列尾部成为新的尾结点,在获取锁之后才会调用该方法
*
* @param node 指定结点
*/
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
//很简单,原尾结点的next引用指向node结点,然后last指向最新node结点
last = last.next = node;
}
/**
* 唤醒一个在notEmpty条件队列中等待的消费线程,需要先获取消费者锁
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
//阻塞式的获取消费者锁,即不响应中断
takeLock.lock();
try {
//唤醒一个在notEmpty条件队列中等待的消费线程
//要想调用Condition对象的方法,必须先要获取该Condition对象对应的lock锁
notEmpty.signal();
} finally {
//释放消费者锁
takeLock.unlock();
}
}
2.3.2 offer(e)方法
public boolean offer(E e)
将指定的元素插入到此队列的尾部。在成功时返回 true,如果此队列已满,则不阻塞,立即返回 false。
相比于offer方法,如果因为获取不到锁而在同步队列中等待的时候被中断也会继续等待获取锁,即不响应中断。如果e元素为null则抛出NullPointerException异常。
另外这里的“不会阻塞”是说的获取锁之后如果发现此队列已满,则立即返回 false,而不会阻塞在条件队列上!因此如果该锁被其他线程获取了,当前调用offer方法的线程还是会因为获取不到锁而被阻塞在lock的同步队列中!
/**
* 将指定的元素插入到此队列的尾部。
*
* @param e 指定元素
* @return 在成功时返回 true,如果此队列已满,则不阻塞,立即返回 false。
* @throws NullPointerException 如果e元素为null
*/
public boolean offer(E e) {
//e的null校验
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
//在获取锁之前就判断一次,如果容量满了
if (count.get() == capacity)
//直接返回false,可以节省锁的获取和释放的开销
return false;
//初始化c为-1,表示存放元素失败
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
//不可中断的等待获取生产者锁,即不响应中断
putLock.lock();
try {
//如果队列未满
if (count.get() < capacity) {
//调用enqueue插入元素
enqueue(node);
//获取此时计数器的值赋给c,并且计数器值自增1,这里的c一定是大于等于0的值
c = count.getAndIncrement();
//如果c+1小于capacity,说明还可以入队
if (c + 1 < capacity)
//唤醒一个在notFull条件队列中等待的生产线程
notFull.signal();
}
} finally {
//释放生产者锁
putLock.unlock();
}
//如果前面没有抛出异常,那么在finally之后会执行下面的代码
//如果c为0,那么此时队列中还可能有存在1条数据,刚放进去的
//那么由于刚才队列没有数据,可能此时有消费者线程在等待,这里需要唤醒一个消费者线程
//如果此前队列中就有数据没有消费完毕,那么也不必唤醒唤醒消费者
if (c == 0)
signalNotEmpty();
//如果c>=0,表示该元素已添加到此队列,则返回 true;否则返回 false
return c >= 0;
}
2.3.3 offer(e, timeout, unit)方法
public boolean offer(E e, long timeout, TimeUnit unit)
将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间。如果插入成功,则返回 true;如果在空间可用前超过了指定的等待时间,则返回 false。
如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。另外,如果指定元素e为 null则抛出NullPointerException 异常。
/**
* 将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间。
*
* @return 如果插入成功,则返回 true;如果在空间可用前超过了指定的等待时间,则返回 false。
* @throws InterruptedException 如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断
* 如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
* @throws NullPointerException 如果指定元素为 null
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
//e的null校验
if (e == null) throw new NullPointerException();
//计算超时时间纳秒
long nanos = unit.toNanos(timeout);
//初始化c为-1,表示存放元素失败
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//可中断的等待获取生产者锁,即响应中断
putLock.lockInterruptibly();
try {
/*
* while循环判断此时结点数量是否等于容量,即队列是否满了
*/
while (count.get() == capacity) {
//如果剩余超时时间小于等于0,说明时间到了,还没有存入成功
if (nanos <= 0)
//直接返回false
return false;
//否则,该线程在notFull条件队列中等待nanos时间,支持中断唤醒
nanos = notFull.awaitNanos(nanos);
}
// 队列没有满,结点添加到链表尾部
enqueue(new Node<E>(e));
//获取此时计数器的值赋给c,并且计数器值自增1
c = count.getAndIncrement();
//如果c+1小于capacity,说明还可以入队
if (c + 1 < capacity)
//唤醒一个在notFull条件队列中等待的生产线程
notFull.signal();
} finally {
//释放生产者锁
putLock.unlock();
}
//如果前面没有抛出异常,那么在finally之后会执行下面的代码
//如果c为0,那么此时队列中还可能有存在1条数据,刚放进去的
//那么由于刚才队列没有数据,可能此时有消费者线程在等待,这里需要唤醒一个消费者线程
//如果此前队列中就有数据没有消费完毕,那么也不必唤醒唤醒消费者
if (c == 0)
//获取消费者锁并且尝试唤醒一个消费者线程
signalNotEmpty();
//到这里,一定是插入成功了,返回true
return true;
}
2.3.4 add(e)方法
public boolean add(E e)
将指定元素插入此队列中。成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException,如果e元素为null则抛出NullPointerException 异常。当使用有容量限制的队列时,通常首选 offer。
如果因为获取不到锁而在同步队列中等待的时候被中断也会继续等待获取锁,即不响应中断。如果e元素为null则抛出NullPointerException 异常。
内部实际上就是调用的offer,根据offer方法的返回值判断是否需要抛出异常!
/**
* 将指定的元素插入到此队列中(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用空间,则抛出 IllegalStateException。
* 如果 offer 成功,则此实现返回 true,否则抛出 IllegalStateException。
*
* @param e 指定元素
* @return 如果此 collection 由于调用而发生更改,则返回 true
* @throws IllegalStateException 如果此时由于容量限制不能添加元素
* @throws ClassCastException 如果指定元素的类不允许将该元素添加到此队列中
* @throws NullPointerException 如果指定元素为 null 并且此队列不允许 null 元素
* @throws IllegalArgumentException 如果此元素的某些属性不允许将该元素添加到此队列中
*/
public boolean add(E e) {
//实际上调用的offer操作
if (offer(e))
//如果offer成功则返回true
return true;
else
//offer失败则抛出IllegalStateException
throw new IllegalStateException("Queue full");
}
2.4 出队操作
2.4.1 take()方法
public E take()
获取并移除此队列的头部,在元素变得可用(队列非空)之前一直等待。
如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
在ArrayBlockingQueue中,消费(移除数据)线程阻塞的时候,需要生产(放入数据)线程才能唤醒,并且因为它们获取的同一个锁,消费和生产不能并发进行(假设一个线程仅仅从事生产或者消费工作的一种)。在LinkedBlockingQueue中,如果有线程因为获取不到消费者锁或者队列已空而导致消费(移除数据)线程阻塞,那么他可能被后面的生产线程唤醒也可能被后面的消费线程唤醒。因为它内部有两个锁,生产和消费获取不同的锁,可以并行执行生产和消费任务,不仅在生产数据的时候会唤醒阻塞的消费线程,在消费数据的时候如果队列容量还没空,也会唤醒此前阻塞的消费线程继续消费。
大概步骤为:
- 指定元素e的null校验;
- lockInterruptibly可中断的等待获取消费者锁takeLock,即响应中断;没有获取到锁就在同步队列中阻塞等待,被中断了则直接中断等待并抛出异常;
- 获取到锁之后,while循环判断此时结点数量是否等于0,即队列是否空了,如果空了,那么该线程在notEmpty条件队列中等待并释放锁,被唤醒之后会继续尝试获取锁、并循环判断;
- 队列没有空,调用dequeue方法获取并移除此队列的头部;获取此时计
- 数器的值赋给c,并且计数器值自减1;
- 如果c大于1,说明此时队列未空,说明还可以出队列,那么唤醒一个在notEmpty条件队列中等待的消费线程;
- 释放消费者锁putLock;
- 如果前面没有发生异常,那么执行最后的if语句:如果c为capacity,那么此前队列中可能具有满的数据,可能此时有生产者线程在等待,这里需要唤醒一个生产者线程。如果此前队列中的数据没有满,那么也不必唤醒生产者。注意这里唤醒生产者线程的时候,必须先获取Condition关联的生产者锁!
/**
* 获取并移除此队列的头部
*
* @return 被移除的队列头部元素
* @throws InterruptedException 因为获取不到锁而等待的时候被中断
*/
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//可中断的等待获取消费者锁,即响应中断
takeLock.lockInterruptibly();
try {
/*
* while循环判断此时结点数量是否等于0,即队列是否空了
*/
while (count.get() == 0) {
//如果空了,那么该线程在notEmpty条件队列中等待并释放锁,被唤醒之后会继续尝试获取锁、并循环判断
notEmpty.await();
}
// 队列没有空,获取并移除此队列的头部
x = dequeue();
//获取此时计数器的值赋给c,并且计数器值自减1
c = count.getAndDecrement();
//如果c大于1,说明还可以出队列
if (c > 1)
//唤醒一个在notEmpty条件队列中等待的消费线程
notEmpty.signal();
} finally {
//释放消费者锁
takeLock.unlock();
}
//如果前面没有抛出异常,那么在finally之后会执行下面的代码
//如果c为capacity,那么此前队列中可能具有满的数据,可能此时有生产者线程在等待,
//这里需要唤醒一个生产者线程
//如果此前队列中的数据没有满,那么也不必唤醒唤醒生产者
if (c == capacity)
//获取生产者锁并且尝试唤醒一个生产者线程
signalNotFull();
//返回被移除的队列头部元素
return x;
}
/**
* 唤醒一个生产者线程,只会在take/poll方法中被调用
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
//阻塞式的获取生产者锁,即不响应中断
putLock.lock();
try {
//唤醒一个在notFull条件队列中等待的生产线程
notFull.signal();
} finally {
//释放生产者锁
putLock.unlock();
}
}
2.4.1.1 dequeue移除头结点
dequeue方法用于移除头结点,并返回item值。
在LinkedBlockingQueue中,head指向的结点是一个哨兵结点,head的后继才可能是真正的头结点。
因此在dequeue中,移除头结点就是获取head的next作为新的head,保存item值并将item引用置为null。原来的head的next指向自己,为什么不指向null呢?因为在LinkedBlockingQueue中,一个结点的next为null的话那么表示遍历到了队列末尾,这在迭代器的时候会用到,而如果指向自己则表示头结点出了队列。后面的迭代器部分会讲到!
/**
* 获取并移除此队列的头部,这里面的新的头部元素会变成哨兵结点,即item置为null
*
* @return 被移除的队列头部元素
*/
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
//获取此时头部元素
Node<E> h = head;
//first指向下一个元素
Node<E> first = h.next;
//原头结点的next指向自己,为什么不指向null呢?
//因为在LinkedBlockingQueue中,一个结点的next为null的话
//那么表示遍历到了队列末尾,这在迭代器的时候会用到,如果指向自己则表示头结点出了队列
h.next = h; // help GC
//head指向此时的头部元素
head = first;
//获取此时头部元素的值
E x = first.item;
//此时头部元素的值置空,即变成哨兵结点
first.item = null;
//返回头部元素的值
return x;
}
2.4.2 poll()方法
public E poll()
获取并移除此队列的头,如果此队列为空,则直接返回 null。
相比于take方法,如果因为获取不到锁而在同步队列中等待的时候被中断也会继续等待获取锁,即不响应中断。但是如果队列为空那么直接返回null而不会阻塞等待。
/**
* @return 获取并移除此队列的头,如果此队列为空,则返回 null。
*/
public E poll() {
final AtomicInteger count = this.count;
//在获取锁之前就判断一次,如果队列空了
if (count.get() == 0)
//直接返回null
return null;
//表示移除的队列头部元素,默认为null
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
//不可中断的等待获取消费者锁,即不响应中断
takeLock.lock();
try {
//判断此时结点数量是否大于0,即队列是否不为空
if (count.get() > 0) {
//队列没有空,获取并移除此队列的头部
x = dequeue();
//获取此时计数器的值赋给c,并且计数器值自减1
c = count.getAndDecrement();
//如果c大于1,说明还可以出队列
if (c > 1)
//唤醒一个在notEmpty条件队列中等待的消费线程
notEmpty.signal();
}
} finally {
//释放消费者锁
takeLock.unlock();
}
//如果前面没有抛出异常,那么在finally之后会执行下面的代码
//如果c为capacity,那么此前队列中可能具有满的数据,可能此时有生产者线程在等待,
//这里需要唤醒一个生产者线程
//如果此前队列中的数据没有满,那么也不必唤醒唤醒生产者
if (c == capacity)
//获取生产者锁并且尝试唤醒一个生产者线程
signalNotFull();
//返回被移除的队列头部元素
return x;
}
2.4.3 poll(timeout, unit)方法
public E poll(long timeout, TimeUnit unit)
获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。返回此队列的头部;如果在元素可用前超过了指定的等待时间,则返回 null。
如果因为获取不到锁而在同步队列中等待的时候被中断则抛出InterruptedException,即响应中断,如果因为队列满了在条件队列中等待的时候在其他线程调用signal、signalAll方法唤醒该线程之前就因为中断而被唤醒了,也会抛出InterruptedException。
/**
* 获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。
*
* @param timeout 时间
* @param unit 时间单位
* @return 此队列的头部;如果在元素可用前超过了指定的等待时间,则返回 null
* @throws InterruptedException 因为获取不到锁而等待时被中断
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
//表示移除的队列头部元素,默认为null
E x = null;
int c = -1;
//计算超时时间纳秒
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//可中断的等待获取锁,即响应中断
takeLock.lockInterruptibly();
try {
/*
* while循环判断此时结点数量是否等于0,即队列是否空了
*/
while (count.get() == 0) {
//如果剩余超时时间小于等于0,说明时间到了
if (nanos <= 0)
//直接返回null
return null;
//否则,该线程在notEmpty条件队列中等待nanos时间
nanos = notEmpty.awaitNanos(nanos);
}
//获取此时计数器的值赋给c,并且计数器值自减1
c = count.getAndDecrement();
//如果c大于1,说明还可以出队列
if (c > 1)
//唤醒一个在notEmpty条件队列中等待的消费线程
notEmpty.signal();
} finally {
//释放消费者锁
takeLock.unlock();
}
//如果前面没有抛出异常,那么在finally之后会执行下面的代码
//如果c为capacity,那么此前队列中可能具有满的数据,可能此时有生产者线程在等待,
//这里需要唤醒一个生产者线程
//如果此前队列中的数据没有满,那么也不必唤醒唤醒生产者
if (c == capacity)
//获取生产者锁并且尝试唤醒一个生产者线程
signalNotFull();
//返回被移除的队列头部元素
return x;
}
2.4.4 remove()方法
public E remove()
获取并移除此队列的头。此方法与 poll 唯一的不同在于此队列为空时将抛出一个NoSuchElementException异常。
相比于take方法,如果因为获取不到锁而在同步队列中等待的时候被中断也会继续等待获取锁,即不响应中断。
内部实际上就是调用的poll方法,根据poll方法的返回值判断是否需要抛出异常!
/**
* 获取并移除此队列的头。此方法与 poll 唯一的不同在于此队列为空时将抛出一个NoSuchElementException异常。
*
* @return 队列头
* @throws NoSuchElementException 此队列为空
*/
public E remove() {
//直接调用poll方法,获取返回值x
E x = poll();
//如果x不为null,那么返回x;否则抛出NoSuchElementException异常
if (x != null)
return x;
else
throw new NoSuchElementException();
}
2.4.5 remove(o)方法
public boolean remove(Object o)
从此队列中移除指定元素的单个实例(如果存在)。如果移除成功则返回 true;没有找到指定元素或者指定元素为null则返回false。
从队列头开始遍历队列,查找和指定元素o使用equals比较返回true的元素p,然后调用unlink 移除p结点,这个方法需要同时获取两个锁,性能比较差,一般不推荐使用!
/**
* 从此队列中移除指定元素的单个实例(如果存在)
* 这个元素可能是真头结点也可能是尾结点,还可能是即使头结点也是尾结点,也可能不存在
* 因此需要遍历整个队列,需要同时获取生产者锁和消费者锁
*
* @param o 指定元素
* @return 如果移除成功则返回 true;没有找到指定元素或者指定元素为null则返回false。
*/
public boolean remove(Object o) {
//如果o为null,直接返回null
if (o == null) return false;
//同时获取生产者锁和消费者锁
fullyLock();
try {
/*
* 首先trail = head, p = trail.next
* 从head的后继开始遍历队列,如果p不为null那么继续,否则结束循环
* 一次循环之后trail = p, p = p.next,即向后推进
*/
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
//如果o和某个结点的item使用equals比较相等
if (o.equals(p.item)) {
//将该结点移除队列
unlink(p, trail);
//返回true
return true;
}
}
//没找到,返回false
return false;
} finally {
//同时释放生产者锁和消费者锁
fullyUnlock();
}
}
/**
* 同时获取生产者锁和消费者锁
*/
void fullyLock() {
putLock.lock();
takeLock.lock();
}
/**
* 同时释放生产者锁和消费者锁
*/
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
2.4.5.1 unlink移除指定结点
unlink方法用于将指定结点p移除队列。做法比较简单,就是将p的前驱的后继指向p的后继,但是之后没有将p的后继置为null,为什么呢?还是和dequeue的原因一样,因为指向null表示遍历到了队列末尾,而只有头结点出队列才会指向自己,这里不改变next指向主要是为了使用迭代器的时候不丢失后面的数据,后面的迭代器部分会讲到!
/**
* 将结点p移除队列
*
* @param p 被移除的结点
* @param trail 前驱
*/
void unlink(Node<E> p, Node<E> trail) {
// assert isFullyLocked();
// p.next is not changed, to allow iterators that are
// traversing p to maintain their weak-consistency guarantee.
//元素置空
p.item = null;
//前驱的后继指向p的后继
trail.next = p.next;
//如果last指向p,即p是尾结点
if (last == p)
//那么last指向前驱
last = trail;
//上面的操作中,p.next没有置为空也没有指向自己,因为指向null表示遍历到了队列末尾
//而只有头结点出队列才会指向自己,这里不改变next指向主要是为了使用迭代器的时候不丢失后面的数据
//count自减1,如果此前容量满了
if (count.getAndDecrement() == capacity)
//那么唤醒一个在notFull上等待的生产者线程
notFull.signal();
}
2.5 检查操作
2.5.1 peek()
public E peek()
获取但不移除此队列的头;如果此队列为空,则返回 null。
/**
* @return 获取但不移除此队列的头;如果此队列为空,则返回 null。
*/
public E peek() {
//在获取锁之前就判断一次,如果队列空了
if (count.get() == 0)
//直接返回nul
return null;
final ReentrantLock takeLock = this.takeLock;
//不可中断的等待获取消费者锁,即不响应中断
takeLock.lock();
try {
//获取head的next结点,它才可能是真正的头结点
Node<E> first = head.next;
//如果为null,说明队列空了
if (first == null)
//直接返回nul
return null;
else
//否则,返回值
return first.item;
} finally {
//释放消费者锁
takeLock.unlock();
}
}
2.5.2 element()方法
public E element()
获取但是不移除此队列的头。此方法与 peek 唯一的不同在于此队列为空时将抛出一个异常。
/**
* 获取,但是不移除此队列的头。此方法与 peek 唯一的不同在于:此队列为空时将抛出一个异常。
*
* @return 队头
* @throws NoSuchElementException 如果此队列为空
*/
public E element() {
//内部调用peek方法获取返回值x
E x = peek();
//如果x不为null,那么返回x;否则抛出NoSuchElementException异常
if (x != null)
return x;
else
throw new NoSuchElementException();
}
2.6 size操作
public int size()
返回此队列中元素的数量。和其他大部分并发容器比如ConcurrentLinkedQueue相比,这个方法返回的是原子变量的最新值,并且入队出队操作都是加了锁了,因此返回的是精确值!
public boolean isEmpty()
如果此队列不包含元素,则返回 true。
/**
* @return 返回此队列中元素的数量
*/
public int size() {
//由于计数器是一个AtomicInteger的原子变量,因此可以获取此时最新的元素数量值
return count.get();
}
public class AtomicInteger extends Number implements java.io.Serializable {
/**
* 实际上count就是AtomicInteger一个内部的volatile类型的变量
*/
private volatile int value;
/**
* @return 由于volatile的特性,可以直接获取最新的值
*/
public final int get() {
return value;
}
}
/**
* 直接判断size()方法是否返回0
*/
public boolean isEmpty() {
return size() == 0;
}
2.7 迭代操作
public Iterator iterator()
返回在队列中的元素上按适当顺序进行迭代的迭代器Iterator。和其他并发容器一样,返回的 Iterator 是一个“弱一致”的,不会抛出 ConcurrentModificationException,即支持并发修改,但是不保证迭代获取的元素就是此时队列中的元素!
下面的源码解析包括迭代器的方法,实际上还是很简单的,弱一致性的原理在注释中也讲的很清楚,实际下一次要返回的值在上一次迭代的时候就确定了,使用currentElement保存,相当于值的快照,弱一致性是很显而易见的!
另外在nextNode方法中循环获取下一次要迭代的有效结点的时候,就会判断我们前面说的两种移除结点的情况:
- p.next == p,即说明p结点是此前被删除的头结点,这就是take()、poll()等方法造成的结果,那么此时应该寻找真正的head的next作为下一个有效结点,因此下一次要迭代的结点就是head.next;
- 否则,p.item == null,这就是remove(o)方法造成的结果,即p不是被删除的队列的头结点,此时 p.next 没有变化,那么p=p.next,继续下一次循环向后查找有效的结点。
最后,由于迭代操作也需要遍历整个队列,因此迭代器的初始化、next、remove方法调用时都需要同时获取生产者锁和消费者锁,此时生产者线程和消费者线程都不能操作,因此会影响并发效率!
/**
* @return 返回在队列中的元素上按适当顺序进行迭代的迭代器Iterator。
* 和其他并发容器一样,返回的 Iterator 是一个“弱一致”的,不会抛出 ConcurrentModificationException,即支持并发修改,
* 但是不保证迭代获取的元素就是此时队列中的元素!
*/
public Iterator<E> iterator() {
//返回一个Itr对象
return new Itr();
}
/**
1. LinkedBlockingQueue的迭代器的实现内部类
2. 弱一致性的迭代器实现
3. 由于是迭代操作,需要遍历队列,因此需要同时获取两把锁
*/
private class Itr implements Iterator<E> {
/**
* 下一个要迭代的结点
*/
private Node<E> current;
/**
* 最后一次迭代的结点,用于辅助remove方法
*/
private Node<E> lastRet;
/**
* 下一个要返回的值,这个值实际上是上一次迭代就保存好的,下一次直接返回
* 为什么这样做呢,这是为了和hasNext方法统一,因为hasNext方法判断的是结点是或否为null
* 如果我们返回结点的item,那么item可能因为结点被删除为null,那么就返回null了
* 这不符合程序思维:因为hasNext表示有数据,但是next方法却返回null,并且LinkedBlockingQueue的“对外”规定item是不能为null的
* 因此这里的值也是保存在获取下一个要返回结点时结点值的快照,这就是导致弱一致性的原因
*/
private E currentElement;
/**
* 迭代器的构造器
* 会初始化下一次要使用的数据
*/
Itr() {
//同时获取生产者锁和消费者锁
fullyLock();
try {
//为current赋值,这里尝试指向真正的头结点即head.next
current = head.next;
//如果不为null,说明此时有数据
if (current != null)
//为current赋值,下一个要返回的值
currentElement = current.item;
} finally {
//同时释放生产者锁和消费者锁
fullyUnlock();
}
}
/**
* 是否有下一个结点
*
* @return true 是 false 否
*/
public boolean hasNext() {
//返回current是否为null
return current != null;
}
/**
* 返回 p 的下一个实时有效结点,如果没有找到则返回 null。
* <p>
* 这里需要处理删除的两种情况:
* 1 当p是此前被删除的头结点时,此时 p.next == p
* 2 当p不是被删除的队列的头结点时, 此时 p.next 没有变化,但是item p.item == null
*/
private Node<E> nextNode(Node<E> p) {
//开启一个死循环
for (; ; ) {
//首先获取p的后继s
Node<E> s = p.next;
/*
* 如果s==p
* 那么表示p是被删除的前head结点
* 那么此时应该寻找真正的head的next作为下一个有效结点,可能为null
*/
if (s == p)
//直接返回此时最新的head的next
return head.next;
/*
* 如果s != p
* 如果s == null,说明p就是队列最后一个结点
* 或者 s != null,并且 s.item != null为 true ,表示s没有被删除,为正常结点
* 以上两种情况,均直接返回s就行了,表示迭代完毕或者正常返回
*/
if (s == null || s.item != null)
return s;
//到这里,说明s.item为null,即s被删除了
//由于s不是作为头结点,因此s.next没有变化,那么继续向后查找有效的结点
//p赋值为s,继续下一次循环,即向后推进
p = s;
}
}
/**
* 返回currentElement的值
* lastRet等于此时的current,同时计算下一个要返回的结点current和下一个要返回的值currentElement
*
* @return 下一个结点的值
*/
public E next() {
//同时获取生产者锁和消费者锁
fullyLock();
try {
//如果current为null,表示已经没有下一个结点了,直接抛出NoSuchElementException异常
if (current == null)
throw new NoSuchElementException();
//如果current不为null
//x保存currentElement
E x = currentElement;
//lastRet等于此时的current
lastRet = current;
//计算下一个要返回的Node
current = nextNode(current);
//计算下一个要返回的item,如果current等于null,那么currentElement等于null,否则currentElement等于current.item
currentElement = (current == null) ? null : current.item;
//返回x这里也能看出“弱一致性”原因,因为这个x是上一次保存的,这一次直接返回了,很有可能不是最新的数据了
return x;
} finally {
//同时释放生产者锁和消费者锁
fullyUnlock();
}
}
/**
* 移除上一次next方法返回currentElement对应的node,即最后迭代的结点lastRet
*/
public void remove() {
//如果lastRet 为null,表示最后迭代的结点已经被移除了,或者还没有迭代,直接抛出IllegalStateException异常
if (lastRet == null)
throw new IllegalStateException();
//同时获取生产者锁和消费者锁
fullyLock();
try {
//保存lastRet
Node<E> node = lastRet;
//lastRet置为null
lastRet = null;
/*
* 类似于remove(o)方法,遍历移除node结点,但是有区别
*/
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
//在remove(o)方法中,使用 equals判断item 是否相等
//在这里使用 == 判断相等,即这里需要真正是同一个对象,才能删除
if (p == node) {
unlink(p, trail);
break;
}
}
} finally {
//同时释放生产者锁和消费者锁
fullyUnlock();
}
}
}
3 LinkedBlockingQueue的总结
LinkedBlockingQueue和ArrayBlockingQueue都是常用的两个阻塞队列,它们有很多的相同点和不同点:
- ArrayBlockingQueue底层采用数组结构来实现阻塞队列,内部没有数据结点,数组位置直接存放的元素值;LinkedBlockingQueue底层采用单链表来实现阻塞队列,内部具有结点的实现类Node,每一个元素值都有一个Node结点对象来保存。这样看起来,LinkedBlockingQueue占用的内存空间要更多一些,但是ArrayBlockingQueue对内存空间的质量要求更高一些(要求连续空间)!
- ArrayBlockingQueue在初始化的时候必须指定容量,最大为Integer.MAX_VALUE;LinkedBlockingQueue则可以不指定容量,默认就是最大容量Integer.MAX_VALUE,如果但是容量过大、元素过大,并且生产线程速度快于消费线程,则可能造成内存溢出!
- ArrayBlockingQueue内部采用一个lock锁来控制同步,生产者线程和消费者线程甚至size计数线程都必须获取该锁,使用条件队列notEmpty用于消费线程的阻塞和唤醒,使用条件队列notFull条件变量用于生产线程的阻塞和唤醒,并发效率很低;LinkedBlockingQueue采用了锁分离技术,具有两把锁takeLock、putLock,takeLock作为消费线程获取的锁,同时有个对应的notEmpty条件变量用于消费线程的阻塞和唤醒,putLock作为生产线程获取的锁,同时有个对应的notFull条件变量用于生产线程的阻塞和唤醒!这样避免了生产线程和消费线程竞争同一把锁的现象,因此LinkedBlockingQueue在高并发的情况下,性能会比ArrayBlockingQueue好很多,但是在需要遍历整个队列的情况下则要把两把锁都锁住(比如clear、contains、remove(o)、迭代器等方法)。
- ArrayBlockingQueue的工作模式可以自己指定公平模式或者非公平模式;LinkedBlockingQueue的工作模式都是非公平的,也不能手动指定为公平模式,即获取锁的实际线程顺序不能保证是等待获取锁的线程顺序,这样的好处是可以提升并发量!
最后,和ArrayBlockingQueue一样,LinkedBlockingQueue的源码看起来也非常简单,主要是因为对于线程的获取锁、释放锁、等待、唤醒等基本操作的源码都交给ReentrantLock、AQS和Concition来实现了。如果想要搞定这些,那还需要花一定时间!
相关文章:
- ArrayBlockingQueue:JUC—ArrayBlockingQueue源码深度解析。
- AQS:JUC—五万字的AbstractQueuedSynchronizer(AQS)源码深度解析与应用案例。
- ReentrantLock:JUC—ReentrantLock源码深度解析。
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!