1、LinkedBlockingQueue 介绍
LinkedBlockingQueue 也是接口BlockingQueue的一个实现类,与 ArrrayBlockingQueue基于
数组实现不同的是,LinkedBlockingQueue是基于单项链表实现的,在LinkedBlockingQueue
内部维护了一个单向链表来存储数据;链表原则上是无边界的,但LinkedBlockingQueue维护
了一个常量 capacity 表示队列的容量,new 创建 LinkedBlockingQueue 时若不指定 capacity
的值,capacity 默认是 Integer.MAX_VALUE
LinkedBlockingQueue 结构如下:
java
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private static final long serialVersionUID = -6903933977591709194L;
//存放数据的节点
//从这里可以发现,LinkedBlockingQueue采用单向链表来存储数据
/**
* Linked list node class
*/
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
private final int capacity;//队列容量
/**
* 使用 AtomicInteger 来记录 数据的个数
* todo 问题:在ArrayBlockingQueue采用int 类型来记录数据个数,但在
* 该类中为什么使用 AtomicInteger 来记录数据个数?
* 因为 ArrayBlockingQueue 是通过一个锁来保证数据的 入队/出队,可以通过锁来
* 保证int数据的原子性;而LinkedBlockingQueue 的 入队/出队 采用不同的锁,但
* count 在 入队/出队 都需要操作,所以要想保证 count需要CAS来保证原子性
*
*/
private final AtomicInteger count = new AtomicInteger();
transient Node<E> head;//队列头节点
private transient Node<E> last;//队列尾节点
private final ReentrantLock takeLock = new ReentrantLock();//取队列数据的锁
private final Condition notEmpty = takeLock.newCondition(); //取数据的Condition
private final ReentrantLock putLock = new ReentrantLock();//向队列添加数据的锁
private final Condition notFull = putLock.newCondition();//入队列的Condition
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();
try {
int n = 0;
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();
}
}
}
2、LinkedBlockingQueue 使用示例
LinkedBlockingQueue常用方法也是 BlockingQueue定义的那几个方法,使用方式与
ArrayBlockingQueue差不多,只是每个方法的具体实现不同而已,、;
LinkedBlockingQueue 使用示例如下:
java
public class LinkedBlockingQueueDemo01 {
public static void main(String[] args) throws InterruptedException {
//若创建队列时不指定队列容量大小,则默认大小是 Integer.MAX_VALUE
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>(3);
//add 添加失败抛出异常
queue.add("1");
queue.add("2");
queue.add("3");
queue.add("4");
//put 添加失败,则一直阻塞,直到队列有空位数据添加成功
queue.put("4");
//添加成功返回true,添加失败返回false
boolean b = queue.offer("5");
System.out.println(b);
//带超时时间的添加
b = queue.offer("6",5, TimeUnit.SECONDS);
System.out.println(b);
//从队列中取数据
//remove 若队列中没有数据,则抛出异常
String s = queue.remove();
//poll 若队列为空,则返回null
s = queue.poll();
//带超时时间的取数据,若队列为空,则线程阻塞,若阻塞超过超时时间之后队列中还没有数据,则返回null
s= queue.poll(5,TimeUnit.SECONDS);
//take 从队列中取数据,若队列为空,则一直阻塞,直到队列中有数据
s = queue.take();
}
}
3、LinkedBlockingQueue常用方法解析
3.1、add(E e) 方法
该方法作用是向队列中添加数据,若添加失败,则直接抛出异常;
方法代码如下:
3.2、offer(E e) 方法
该方法作用也是向队列中添加数据,若添加成则返回true,添加失败返回false
offer方法代码如下:
java
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
//引用成员变量,获取队列数量
final AtomicInteger count = this.count;
//队列数据个数是否等于队列限制长度(队列容量)
if (count.get() == capacity)
return false;
//作为标记存在
//todo 使用数值类型作为标识的特点
int c = -1;
//将存储的数据封装成Node
Node<E> node = new Node<E>(e);
//生产者锁
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
//再次判断,查看队列是否还有空间
//双重检查
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
//判断队列是否满了
if (c + 1 < capacity)
//通知其他阻塞的生产者线程
//这里生产者和消费者不是互斥的,但消费者之间是互斥的
notFull.signal();
}
} finally {
putLock.unlock();
}
//如果c==0,表示添加数据之前队列元素个数为0,这时可能会出现消费者全在阻塞状态
//所以,添加数据之后需要唤醒消费者
if (c == 0)
//唤醒消费者
signalNotEmpty();
//c>=0表示添加成功
return c >= 0;
}
//添加数据
private void enqueue(Node<E> node) {
last = last.next = node;
}
3.3、singnalNotEmpty()、signalNotNull()、enqueue()
signalNotEmpty(): 唤醒消费者线程
signalNotFull(): 唤醒生产者线程
enqueue(): 入队列
java
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
//只有持有锁后才能调用 wait/signal
//所以这里要先获取读锁
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
//要想执行Condition 的方法,必须先获取相关的锁
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
3.4、offer(E e, long timeout, TimeUnit unit)
该方法功能也是向队列添加数据,若添加失败则会阻塞,若超过了超时时间还没添加成功
则表示添加失败,返回false
java
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 {
//阻塞,直到超时时间为0
while (count.get() == capacity) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
//入队
enqueue(new Node<E>(e));
c = count.getAndIncrement();
//队列未满,通知其他生产者线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
//此时所有消费者线程可能都在阻塞,所有生产数据后需要唤醒消费者线程
if (c == 0)
signalNotEmpty();
return true;
}
3.5、put(E e) 方法
该方法功能也是向队列中添加数据,若添加失败,则一直阻塞,直到队列中有空位可以
添加成功;若线程被中断,则直接抛出异常退出
java
public void put(E e) throws InterruptedException {
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 (count.get() == capacity) {
notFull.await();
}
//向队列中添加数据
enqueue(node);
//更新队列数据个数
c = count.getAndIncrement();
//队列还没有满,通知其他生产者线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
//此时所有消费者线程可能都在阻塞,所有生产数据后需要唤醒消费者线程
if (c == 0)
signalNotEmpty();
}
3.6、remove() 方法
该方法是从队列取数据,若队列为空,则抛出异常;
remove方法代码如下:
3.7、poll() 方法
该方法功能是删除队列头元素(第一个进入队列的元素),若队列为空,则返回null;
poll方法代码如下:
java
/**
* 从队列中取数据,若队列为空则返回null
* @return
*/
public E poll() {
final AtomicInteger count = this.count;
//若队列为空,则返回null
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();
//CAS队列元素个数减1
//先获取再减1
c = count.getAndDecrement();
//队列元素多余1,当前线程消费后继续唤醒其他消费者线程
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
//c获取的当前消费者线程消费之前的线程,若 c == capacity 表示队列满了,此刻当前线程
//消费后可能会出现所有生产者线程都处于"阻塞等待" 状态,所以需要唤醒生产者线程
if (c == capacity)
signalNotFull();
//返回消费的元素
return x;
}
3.8、poll(long timeout, TimeUnit unit)
该方法是带有超时时间的获取队列的第一个元素;若队列为空,则当前线程会阻塞,直到
超过了超时时间 timeout 后,若队列还是为空,则返回null
java
/**
* 删除队列第一个元素,并返回
* 若队列为空,当前线程会阻塞,直到时间超过 timeout,若队列还是为空,则返回null
* 若线程被中断,则直接抛出中断异常,并退出
*
* @param timeout
* @param unit
* @return
* @throws InterruptedException
*/
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;
//阻塞,并返回剩余阻塞时间
nanos = notEmpty.awaitNanos(nanos);
}
//取数据
x = dequeue();
//CAS,队列元素个数减1
//先获取再减1
c = count.getAndDecrement();
//队列元素有多个,则通知其他消费者线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
//此刻,生产者线程可能全处于"阻塞等待" 状态,通知唤醒生产者线程
if (c == capacity)
signalNotFull();
return x;
}
3.9、take() 方法
该方法功能也是删除并返回队列的第一个元素,若队列为空,则一直阻塞,直到队列
不为空,或着线程被中断,异常退出
java
/**
* 删除并返回队列的第一个元素,若队列为空,则一直阻塞;
* 若线程被中断,则直接抛出异常,退出
*
* @return
* @throws InterruptedException
*/
public E take() throws InterruptedException {
E x;
/**
* todo 问题:这里c为什么设置为-1?
* c=-1 作为标记存在,使用数值类型作为标识的特点
*/
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//可中断的线程锁,若线程被中断则抛出异常
takeLock.lockInterruptibly();
try {
//队列为空,则阻塞
while (count.get() == 0) {
notEmpty.await();
}
//取数据
x = dequeue();
//更新队列数据
c = count.getAndDecrement();
//队列不为空,通知其他消费者线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
/**
* todo 注意:
* 需要注意 c == capacity 这个判断,c == capacity 表示之前队列满了,当前消费了一个元素后,
* 但此时可能存在 生产者线程全是 "阻塞" 状态,所以消费数据之后需要唤醒一个生产者线程
*/
if (c == capacity)
signalNotFull();
return x;
}
3.10、peek() 方法
该方法功能是查看(返回)队列中的第一个元素,但并不会把该元素从队列中删除。
java
/**
* 查看队列头元素,但并不会删除头元素
* @return
*/
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();
}
}
3.11、signalNotFull()、dequeue()
signalNotFull():唤醒阻塞等待中的生产者线程
dequeue():删除并返回队列的第一个元素
java
/**
* 唤醒阻塞中的生产者线程
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
//要想执行Condition 的方法,必须先获取相关的锁
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
/**
* 删除链表的第一个元素并返回
*/
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head; //头节点(头节点是一个虚拟节点)
Node<E> first = h.next;//第一个头节点
//删除头节点,即修改节点的next指向(或 h.next=null也是一样)
//让下一个节点作为头节点
h.next = h; // help GC,
//更新头结点
head = first;
E x = first.item;
//删除节点数据,让其作为虚拟头节点
first.item = null;
return x;
}