1.LinkedBlockQueue概述
基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
2.成员属性
static class Node<E> {
E item;//节点封装的数据
Node<E> next;//下一个节点
Node(E x) { item = x; }
}
/** 指定链表容量 */
private final int capacity;
/** 当前的元素个数
* 这里和ArrayBlockQueue不一样,因为这里采用生产消费锁分离,count需要具有原子性
*/
private final AtomicInteger count = new AtomicInteger(0);
/** 链表头节点 */
private transient Node<E> head;
/** 链表尾节点 */
private transient Node<E> last;
/** 出队锁 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 出队等待条件 */
private final Condition notEmpty = takeLock.newCondition();
/** 入队锁 */
private final ReentrantLock putLock = new ReentrantLock();
/** 入队等待条件 */
private final Condition notFull = putLock.newCondition();
3.构造器
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
//遍历传入的Collection
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
//尾插入队列中
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
4.常用方法
4.1put()、offer()、offer(E e, long timeout, TimeUnit unit)
/**
* 在队尾插一个元素
* 如果队列满了,一直阻塞,直到队列不满了或者线程被中断
*/
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
final ReentrantLock putLock = this.putLock;//入队锁
final AtomicInteger count = this.count;//当前队列中的元素个数
putLock.lockInterruptibly();//加锁
try {
while (count.get() == capacity) {//如果队列满了
//加入notFull等待队列,直到队列元素不满了,被其他线程使用notFull.signal()唤醒
notFull.await();
}
enqueue(e);//入队
// 将“当前节点数量”+1,并返回“原始的数量”
//因此这个c是未加1的值
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果在插入节点前,队列为空;则插入节点后说明队列一定不为空,唤醒notEmpty上的等待线程
if (c == 0)
signalNotEmpty();
}
/**
* 创建一个节点,并加入链表尾部
*/
private void enqueue(E x) {
/*
* 封装新节点,并赋给当前的最后一个节点的下一个节点,然后在将这个节点设为最后一个节点
*/
last = last.next = new Node<E>(x);
}
/**
* 唤醒notEmpty上的等待线程。
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
// 如果“队列已满”,则返回false,表示插入失败。
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
// 新建“节点e”
Node<E> node = new Node(e);
final ReentrantLock putLock = this.putLock;
// 获取“插入锁putLock”
putLock.lock();
try {
// 再次对“队列是不是满”的进行判断。
// 若“队列未满”,则插入节点。
if (count.get() < capacity) {
// 插入节点
enqueue(node);
// 将“当前节点数量”+1,并返回“原始的数量”
//因此这个c是未加1的值
c = count.getAndIncrement();
// 如果在插入元素之后,队列仍然未满,则唤醒notFull上的等待线程。
if (c + 1 < capacity)
notFull.signal();
}
} finally {
// 释放“插入锁putLock”
putLock.unlock();
}
// 如果在插入节点前,队列为空;则插入节点后说明队列一定不为空,唤醒notEmpty上的等待线程
if (c == 0)
signalNotEmpty();
return c >= 0;
}
/**
* 在队尾插入一个元素,,如果队列已满,则进入等待,直到出现以下三种情况:
* 1、被唤醒
* 2、等待时间超时
* 3、当前线程被中断
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null)
throw new NullPointerException();
long nanos = unit.toNanos(timeout);// 转换为纳秒
int c = -1;
final ReentrantLock putLock = this.putLock;// 入队锁
final AtomicInteger count = this.count;// 总数量
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {// 容量已满
if (nanos <= 0)// 已经超时
return false;
/*
* 进行等待: 在这个过程中可能发生三件事:
* 1、被唤醒-->继续当前这个while循环
* 2、超时-->继续当前这个while循环
* 3、被中断-->抛出中断异常InterruptedException
*/
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);// 入队
c = count.getAndIncrement();// 入队元素数量+1
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
具体入队与出队的原理图:
图中每一个节点前半部分表示封装的数据x,后边的表示指向的下一个引用。
1、初始化
初始化之后,初始化一个数据为null,且head和last节点都是这个节点。
2、入队两个元素过后
小结:
- put(E e):如果队列满了,一直阻塞,直到队列不满了或者线程被中断(阻塞)
- offer(E e):如果队列没满,入队并返回true; 如果队列满了,立即返回false(不阻塞)
- offer(E e, long timeout, TimeUnit unit):在队尾插入一个元素,,如果队列已满,则进入等待,直到出现以下三种情况:
- 被唤醒
- 等待时间超时
- 当前线程被中断
4.2take()、poll()、peek()
take():将队头元素出队,如果队列空了,一直阻塞,直到队列不为空或者线程被中断
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
// 获取“取出锁”,若当前线程是中断状态,则抛出InterruptedException异常
takeLock.lockInterruptibly();
try {
// 若“队列为空”,则一直等待。
while (count.get() == 0) {
notEmpty.await();
}
// 取出元素
x = dequeue();
// 取出元素之后,将“节点数量”-1;并返回“原始的节点数量”。
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
// 释放“取出锁”
takeLock.unlock();
}
// 如果在“取出元素之前”,队列是满的;则在取出元素之后,唤醒notFull上的等待线程。
if (c == capacity)
signalNotFull();
return x;
}
/**
* 从队列头部移除一个节点
*/
private E dequeue() {
Node<E> h = head;//获取头节点:x==null
Node<E> first = h.next;//将头节点的下一个节点赋值给first
h.next = h; // 将当前将要出队的节点置null(为了使其做head节点做准备)
head = first;//将当前将要出队的节点作为了头节点
E x = first.item;//获取出队节点的值
first.item = null;//将出队节点的值置空
return x;
}
//唤醒NotFull上的线程
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
//为什么这里加锁?如果队列只有一个结点,但是有两个线程执行这段代码怎么办?
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
poll():如果没有元素,直接返回null;如果有元素,出队
/**
* 出队:
* 1、如果没有元素,直接返回null
* 2、如果有元素,出队
*/
public E poll() {
final AtomicInteger count = this.count;// 获取元素数量
if (count.get() == 0)// 没有元素
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();// 获取出队锁
try {
if (count.get() > 0) {// 有元素
x = dequeue();// 出队
// 元素个数-1(注意:该方法是一个无限循环,直到减1成功为止,且返回旧值)
c = count.getAndDecrement();
if (c > 1)// 还有元素(如果旧值c==1的话,那么通过上边的操作之后,队列就空了)
notEmpty.signal();// 唤醒等待在notEmpty队列中的其中一条线程
}
} finally {
takeLock.unlock();// 释放出队锁
}
if (c == capacity)// c == capacity是怎么发生的?如果队列是一个满队列,注意:上边的c返回的是旧值
signalNotFull();
return x;
}
poll(long timeout, TimeUnit unit)
从队头删除一个元素,如果队列不空,出队;如果队列已空且已经超时,返回null;如果队列已空且时间未超时,则进入等待,直到出现以下三种情况:1、被唤醒 2、等待时间超时 3、当前线程被中断
/**
* 从队列头部删除一个元素,
* 如果队列不空,出队;
* 如果队列已空,判断时间是否超时,如果已经超时,返回null
* 如果队列已空且时间未超时,则进入等待,直到出现以下三种情况:
* 1、被唤醒
* 2、等待时间超时
* 3、当前线程被中断
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
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 (count.get() == 0) {//如果队列没有元素
if (nanos <= 0)//已经超时
return null;
/*
* 进行等待:
* 在这个过程中可能发生三件事:
* 1、被唤醒-->继续当前这个while循环
* 2、超时-->继续当前这个while循环
* 3、被中断-->抛出异常
*/
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();//出队
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
peek():队列为空返回null,否则返回队首元素
public E peek() {
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();
}
}
出队一个元素后
表面上看,只是将头节点的next指针指向了要删除的x1.next,事实上这样我觉的就完全可以,但是jdk实际上是将原来的head节点删除了,而上边看到的这个head节点,正是刚刚出队的x1节点,只是其值被置空了。
小结:
- poll():如果没有元素,直接返回null;如果有元素,出队
- take():如果队列空了,一直阻塞,直到队列不为空或者线程被中断-->阻塞
- poll(long timeout, TimeUnit unit):如果队列不空,出队;如果队列已空且已经超时,返回null;如果队列已空且时间未超时,则进入等待,直到出现以下三种情况:1、被唤醒 2、等待时间超时 3、当前线程被中断
- peek():队列为空返回null,否则返回队首元素
4.3clear()
public void clear() {
//获取两个锁,put锁和take锁
fullyLock();
try {
//遍历链表,将所有结点赋为null
for (Node<E> p, h = head; (p = h.next) != null; h = p) {
h.next = h;
p.item = null;
}
head = last;
//链表清空完毕后唤醒线程
if (count.getAndSet(0) == capacity)
notFull.signal();
} finally {
fullyUnlock();
}
}
void fullyLock() {
putLock.lock();
takeLock.lock();
}
5.ArrayBlockQueue和LinkedBlockQueue的区别
- ArrayBlockQueue底层是数组并且必须指定初始容量,LinkedBlockQueue底层是链表
- ArrayBlockQueue生产和消费用一个锁,LinkedBlockQueue生产和消费有两把锁,一把用于入队,一把用于出队,有效的避免了入队与出队时使用一把锁带来的竞争,因此count计数器需要具有原子性,所以用到了原子类。
- 由于ArrayBlockQueue入队和出队共用一把锁,因此LinkedBlockQueue性能上会更好一些。但是LinkedBlockQueue可能会产生内存溢出