源码解读:堵塞式队列LinkedBlockQueue

一个基于链表构建的FIFO(fisrt in first out)阻塞式队列。

简介

类图

LinkedBlockingQueue是一个线程安全的阻塞队列,实现了先进先出等特性,是作为生产者消费者的首选,可以指定容量,也可以不指定,不指定的话默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法将一个对象放到队列尾部,在队列满的时候会阻塞直到有队列成员被消费,take方法从head取一个对象,在队列为空的时候会阻塞,直到有队列成员被放进来。

transient关键字

  • (1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法被访问。
  • (2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
  • (3)一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。

使用场景

  • (1)类中的字段值可以根据其它字段推导出来,如一个长方形类有三个属性长度、宽度、面积,面积不需要序列化。
  • (2) 一些安全性的信息,一般情况下是不能离开JVM的。
  • (3)如果类中使用了Logger实例,那么Logger实例也是不需要序列化的

成员变量

字段类型 字段名 备注
static class Node 元素封装类
int capacity 容器容量,默认为int的最大值
AtomicInteger count 当前容器中的元素数量
Node head,last 链表的开头与结尾元素
ReentrantLock takeLock,putLock 取锁与放锁
Condition notEmpty,notFull putLock/takeLock.newCondition()

构造方法

构建一个容量为int最大值的链表,可以发现容量只是赋给了capacity变量,并没有如ArratList那样生成一个连续的长度为容量值的字的内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;
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();
}
}

public void put(E e) throws InterruptedException {

素添加方法,在队列满后调用该方法的线程会被阻塞,直到有空余空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
final int c;
final 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条件满足后进入就绪
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}

与put犯法类似,在指定时间内仍未放入队列则报异常,实现关键是由notFull信号在指定时间内未获取到notify信号实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
final int c;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0L)
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;
}

public boolean offer(E e)

在队列末尾立即添加目标元素,与put方法相比,该方法不会导致调用线程阻塞。

public E take() throws InterruptedException

与put方法相对应,这是一个获取队头元素的线程安全方法,若队列中没有元素则会导致调用线程阻塞。

public E poll()

获取队头元素并删除,与put方法相比,该方法不会在没有元素时阻塞,而是直接返回null

public E peek()

获取但不删除队头元素

public int drainTo(Collection<? super E> c)

将列表中的元素放入到给定集合中,并从自身队列中删除这些元素。与for循环poll相比,该方法更为高效,并且不会在轮训过程中释放取锁。


参考资料