一、类介绍
基于链表实现的FIFO阻塞队列实现类。
二、属性介绍
//链表节点
static class Node<E> {
E item;
/**
* 当前节点的后继节点
*/
Node<E> next;
Node(E x) { item = x; }
}
/** 队列容量,没指定时容量为 Integer.MAX_VALUE */
private final int capacity;
/** 队列内当前元素个数,保证原子性的增减元素 */
private final AtomicInteger count = new AtomicInteger();
/**
* 不是链表的头结点!不是链表的头结点!不是链表的头结点!
* Head.next = 链表头结点
*/
transient Node<E> head;
/**
* 链表尾节点
*/
private transient Node<E> last;
/** 获取元素时的锁 */
private final ReentrantLock takeLock = new ReentrantLock();
/** takeLock关联的Condition */
private final Condition notEmpty = takeLock.newCondition();
/** 保存元素时的锁。 */
private final ReentrantLock putLock = new ReentrantLock();
/** putLock关联的Condition */
private final Condition notFull = putLock.newCondition();
三、方法介绍
1、构造器
// 无参构造器,队列容量是Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* 创建一个指定容量的队列
* 初始化head和tail节点
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
/**
* 创建容量为Integer.MAX_VALUE的队列,按传入的集合的遍历顺序,把集合中元素依次入队列。
*/
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、写数据:写数据入队列有put()方法、offer()方法以及其重载的超时版本方法。以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;
//加写锁(可中断)
putLock.lockInterruptibly();
try {
//如果队列元素满了,则进行等待,直到not full(链表未满)。这时需要等到取数据操作发生后,发出链表未满的通知。
while (count.get() == capacity) {
notFull.await();
}
//元素入队列,直接添加到链表末尾
enqueue(node);
//先取数据,再加1
c = count.getAndIncrement();
//存入数据后,链表仍然未满,发出链表未满的通知
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
//注意,c是添加元素前的队列元素数量,当c=0时,添加元素后,队列的元素数量为1,此时发出队列非空的通知。
if (c == 0)
signalNotEmpty();
}
//新元素添加到链表尾部
private void enqueue(Node<E> node) {
last = last.next = node;
}
3、读数据:读数据有take()方法、poll()方法和pick()方法以及他们的重载的超时版本。以take方法为例说明。读数据的方法,在队列为空的情况下,会进入阻塞状态。
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
//加读锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//队列是空的,则进行等待,直到队列非空。在上面的put()方法的末尾,在添加元素后,会发出队列非空的通知,此时,这里的阻塞将会结束。
while (count.get() == 0) {
notEmpty.await();
}
//取队列头结点,并删除队列的头结点。
x = dequeue();
//先获取队列元素数量,再减1
c = count.getAndDecrement();
//如果队列元素数量大于1,说明此次取元素后,队列中仍然至少有1个元素,队列非空,发出非空通知。
//为么事c != capacity - 1,而是c = capacity? 我觉得是方法末尾仍然用到了c == capacity进行判断有关,不然,还得把c加1,再与capacity进行比较。
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
//队列元素再减1之前就等于队列最大容量,此时,可能又某个写数据操作,正阻塞着(队列已满,等待发出队列未满通知),因此在元素数量减1后,需要及时发出队列未满通知,让阻塞的写线程继续工作。
//这点,也能看出采用两个锁的好处了。
if (c == capacity)
signalNotFull();
return x;
}
//元素出队列,这里只要了解Head并不是队列的第一个元素就好理解了。
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;
}
4、删除元素:删除元素就是普通的链表操作了
//迭代,删除指定对象。
public boolean remove(Object o) {
if (o == null) return false;
//同时加读锁和写锁
fullyLock();
try {
//迭代链表,找到要删除的元素
for (Node<E> trail = head, p = trail.next; p != null; trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
fullyUnlock();
}
}
//利用前置节点,删除当前节点
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;
trail.next = p.next;
if (last == p)
last = trail;
if (count.getAndDecrement() == capacity)
notFull.signal();
}
5、方法总结:
1)Head并不是队列的首节点;
2)读数据时,若数据为空,会触发读锁等待,需要写数据操作发生后发出队列非空通知;写数据时,如果队列已满,会触发写锁等待,需要读数据操作发生后发出队列未满通知。
四、链表操作图
五、思考
1、takeLock和putLock:因为入队列是操作队尾的元素,而取元素是操作队首的元素,用两个锁,保证线程安全的同时,也提高了性能。