1 概述
LinkedBlockingQueue(简称LBQ)是BlockingQueue接口的实现类,是一种无界、阻塞、线程安全的FIFO队列。LBQ底层存储基于链表,头节点为哨兵节点,里面的数据域(item)为空。插数据时从尾节点插入,取数据时从头节点(若队列非空,则找第一个item不为空的节点)获取。相对于非阻塞的ConcurrentLinkedQueue(简称CLQ队列),LBQ的实现比较简单,就是采用添加和取出两把锁来保证线程安全。
2 链表节点的数据结构
static class Node<E> {
//存储当前节点的元素
E item;
//指向下一个链节点
Node<E> next;
//有参构造函数
Node(E x) { item = x; }
}
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);
}
4 阻塞式新增操作(put方法)
这里提下上锁的方式
public void put(E e) throws InterruptedException {
//非空校验
if (e == null) throw new NullPointerException();
int c = -1;
//初始化一个新的节点
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
//获取当前时间队列中节点的总数
final AtomicInteger count = this.count;
/*
* 三种上锁方法对比:
* 1)lock()方法:成功则获取锁,失败则使线程排队,甚至阻塞。
* 2)lockInterruptibly()方法成功则获取锁,失败则使线程排队,甚至阻塞。跟lock比较最大得区别是能响应异常,因为这个方法是抛出异常得。
* 3)tryLock()方法成功则获取锁,失败则返回false,不再执行获取锁操作。也就当前线程不存在排队或者阻塞。
putLock.lockInterruptibly();
try {
//如果队列中节点的总数达到队列的容量,则线程进入等待状态,让出CPU
while (count.get() == capacity) {
notFull.await();
}
//将node节点添加到链表头部
enqueue(node);
//队列中节点的总数加1
c = count.getAndIncrement();
//count.getAndIncrement()返回的是count未自增前的值
//c+1表示count自增后的数量如果小于容量,说明可以唤醒notFull上等待的线程
if (c + 1 < capacity)
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
//count未自增前的值若为0,说明自增前队列为空,此时可能有线程在notEmpty上等待
//自增后count的值大于1,说明队列有值,可以唤醒在notEmpty上等待的线程
if (c == 0)
signalNotEmpty();
}
//将新增节点插入到链表尾部
private void enqueue(Node<E> node) {
last = last.next = node;
}
//唤醒在notEmpty上等待的线程
// condition.signal操作是唤醒等待最久的节点
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
5 非阻塞式新增操作(offer)
offer方法跟put类似,最大区别是offer添加时候如果队列满了,则返回false,表示添加失败,结束当前操作;而put会使当前线程进入阻塞状态,直到添加成功。
public boolean offer(E e) {
//非空检验
if (e == null) throw new NullPointerException();
//获取当前时间队列中节点的总数
final AtomicInteger count = this.count;
//如果队列中排队节点的总数达到队列的容量,则返回false,表示添加失败
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
//再次检查队列的大小,如果不小于容量则不进入条件语句
if (count.get() < capacity) {
//将node节点添加到链表头部
enqueue(node);
//队列中节点的总数加1
c = count.getAndIncrement();
//c+1表示count自增后的数量如果小于容量,说明可以唤醒notFull上等待的线程
if (c + 1 < capacity)
notFull.signal();
}
} finally {
//释放锁
putLock.unlock();
}
//自增后count的值大于1,说明队列有值,可以唤醒在notEmpty上等待的线程
if (c == 0)
signalNotEmpty();
//如果c小于0,说明没有添加成功,返回false
return c >= 0;
}
6 阻塞式取出操作(take方法)
阻塞式获取头节点元素,(这里的返回值实际是h.next中的值,因为头节点中包含的item永远为空)。如果队列为空,则发生阻塞。
这里 while (count.get() == 0) { notEmpty.await(); }
阻塞代码使用while循环,可以换成if吗?答案式不可以。因为添加操作的上锁方式采用lockInterruptibly(),是可以被中断,所以需要通过while循环做保证。
public E take() throws InterruptedException {
E x;
int c = -1;
//获取当前时间队列中节点的总数
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//上锁
takeLock.lockInterruptibly();
try {
//判断队列是否为空,若为空则线程进入等待状态;当有新的元素进入队列时,会唤醒此线程
while (count.get() == 0) {
notEmpty.await();
}
//将头节点出队列
x = dequeue();
//队列总数减1操作
c = count.getAndDecrement();
//队列总数count减1操作前的数量大于1,说明队列中至少有2个元素,执行减1操作后至少还剩下1个
if (c > 1)
//唤醒在notEmpt上等待的线程
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
//队列总数count减1操作前的数量等于队列容量,说明减1操作后就小于队列容量,在notFull上等待的线程可以被唤醒
if (c == capacity)
//唤醒在notFull上等待的线程
signalNotFull();
return x;
}
//将头节点出队列,但返回值式head.next中包含元素x
//将head的下一个节点first变为头节点,并且把first的item清空
这样头节点中item永远为null
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;
}
//唤醒在notFull上等待的线程
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
添加操作是操作尾节点,取出操作是操作尾节点,两者互不影响,因此效率比较高。
7 非阻塞式取出操作(poll方法)
poll方法跟take类似,最大区别是poll获取元素时如果队列为空,则返回null,表示获取失败,结束当前操作;而take会使当前线程进入阻塞状态,直到获取成功。
public E poll() {
//队列中节点数
final AtomicInteger count = this.count;
//队列中节点数为0
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操作
c = count.getAndDecrement();
//队列总数count减1操作前的数量大于1,说明队列中至少有2个元素,执行减1操作后至少还剩下1个
if (c > 1)
//唤醒在notEmpt上等待的线程
notEmpty.signal();
}
} finally {
//释放锁
takeLock.unlock();
}
//队列总数count减1操作前的数量等于队列容量,说明减1操作后就小于队列容量,在notFull上等待的线程可以被唤醒
if (c == capacity)
signalNotFull();
return x;
}
8 非阻塞式查询操作(peak方法)
非阻塞式获取头节点的元素(这里的返回值实际是h.next中的值,因为头节点中包含的item永远为空)
public E peek() {
//判断队列是否为空,为空则返回null
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
//上锁
takeLock.lock();
try {
//头节点中包含的item永远为空,所以是判断头节点的下一个节点的item值
Node<E> first = head.next;
//为空则分返回null
if (first == null)
return null;
else
//不为空则返回item值
return first.item;
} finally {
//是否锁
takeLock.unlock();
}
}
9 获取队列的大小(size方法)
public int size() {
//返回当前队列的大小
return count.get();
}
10 参考文献
1)JDK7在线文档
https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
2) JDK8在线文档
https://docs.oracle.com/javase/8/docs/api/
3) Bruce Eckel. Java编程思想,第4版,2007,机械工业出版社
4)方腾飞,魏鹏,程晓明. Java并发编程的艺术,第1版,2015年,机械工业出版社