一。概述
LinkedBlockingQueue是cocurrent包下的一个线程安全的阻塞队列,与ArrayBlockingQueue比,LinkedBlockingQueue队列用链表实现,通常用于生产者、消费者模型。
二。LinkedBlockingQueue的数据结构
1. 链表节点
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
2. 链表指针
LinkedBlockingQueue链表提供了头指针和尾指针。
1)头指针:用来管理队列出队,如take(), poll(), peek()
2)尾指针:用来管理队列入队,如put(), offer()
private transient Node head; /* 头结点, 头节点不保存数据信息 /
private transient Node last; / 尾节点, 尾节点保存最新入队的数据信息 */
3. 链表容量的大小
LinkedBlockingQueue是有大小限制的,如果队列已经满了,则无法再向其中添加元素。
private final int capacity; /* 队列容量一般使用中,构造LinkedBlockingQueue时,需要传入当前队列大小,如果不传入,默认是Integer.MAX_VALUE */
private final AtomicInteger count = new AtomicInteger(0); // 队列当前大小
这里的count是AtomicInteger类型,而不是int类型,因为LinkedBlockingQueue的读和写是通过两把锁来控制并发操作。如果用int类型,会有线程安全问题。
4. 控制并发的lock和condition
LinkedBlockingQueue中,读和写操作分别由两把锁控制,两把锁分别管理head节点和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(); / 写锁对应的条件 */
三。关键代码解读
1. 入队的public方法
入队的方法有两种:一种阻塞的方式,一种是非阻塞的方式。
1)put(): 阻塞算法,直到队列有空余时,才能为队列加入新元素。
2)offer():非阻塞算法,如果队列已满,立即返回或等待一会再返回,通过返回值true或false,标记本次入队操作是否成功。
put的源码:
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.await();
}
enqueue(e);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); /* 如果完成当前入队操作后,队列依然有剩余的空间,那么再唤醒另一个等待入队的线程 */
} finally {
putLock.unlock();
}
if (c == 0) /* 如果入队前,队列大小为空,那么唤醒一个等待出队的线程 */
signalNotEmpty();
}
offer的源码:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
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) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
2. 出队的public方法
出队与入队的方法类似,也分为阻塞与非阻塞方法。其中:
1)take()为阻塞算法,直到有数据可取时,在取出数据,否则阻塞等待。
2)poll()为非阻塞算法,如果队列为空,立即返回或等待一会再返回,通过返回值true或false来标记是否取到数据。
3)peek():只返回队列中的第一个元素,既不出队,也不阻塞,如果没有元素,就返回null.
take方法源代码:
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();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal(); /* 如果完成当前入队操作后,队列依然有剩余的元素,那么再唤醒另一个等待出队的线程 */
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull(); /* 如果出队前,队列是满的,那么出队后,队列就空了,需要通知一个等待入队的线程 */
return x;
}
3. 线程安全的迭代器
LinkedBlockingQueue的迭代器Itr,是线程安全的,在获取元素之前,会对上述读锁和写锁同时加锁,为了防止死锁,先加写锁,再加读锁。
void fullyLock() {
putLock.lock(); // 先加写锁
takeLock.lock(); // 再加读锁
}
void fullyUnlock() {
takeLock.unlock(); /* 先解锁读锁 */
putLock.unlock(); /* 再解锁写锁 */
}
迭代器的源码:
private class Itr implements Iterator<E> {
/*
* Basic weakly-consistent iterator. At all times hold the next
* item to hand out so that if hasNext() reports true, we will
* still have it to return even if lost race with a take etc.
*/
private Node<E> current;
private Node<E> lastRet;
private E currentElement;
Itr() {
fullyLock();
try {
current = head.next;
if (current != null)
currentElement = current.item;
} finally {
fullyUnlock();
}
}
public boolean hasNext() {
return current != null;
}
/**
* Returns the next live successor of p, or null if no such.
*
* Unlike other traversal methods, iterators need to handle both:
* - dequeued nodes (p.next == p)
* - (possibly multiple) interior removed nodes (p.item == null)
*/
private Node<E> nextNode(Node<E> p) {
for (;;) {
Node<E> s = p.next;
if (s == p)
return head.next;
if (s == null || s.item != null)
return s;
p = s;
}
}
public E next() {
fullyLock();
try {
if (current == null)
throw new NoSuchElementException();
E x = currentElement;
lastRet = current;
current = nextNode(current);
currentElement = (current == null) ? null : current.item;
return x;
} finally {
fullyUnlock();
}
}
public void remove() {
if (lastRet == null)
throw new IllegalStateException();
fullyLock();
try {
Node<E> node = lastRet;
lastRet = null;
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (p == node) {
unlink(p, trail);
break;
}
}
} finally {
fullyUnlock();
}
}
}
可以看到,迭代器中保存了一下内容:
private Node<E> current; /* 迭代器的下一个位置 */
private Node<E> lastRet; /* 当前迭代器的位置 */
private E currentElement; /* 当前需要返回的元素内容 */
如果我们自己实现这个迭代器,会觉得只要一个指向当前位置的指针就行了,何必那么麻烦呢?JDK的开发人员这样设计,是为了应付多线程的问题:
1)首先,通过currrentElement保存了当前需要返回的内容,这样可以保证在当前节点被其他线程删除的情况下,迭代器的next()方法仍能返回当前指向的内容。
2)在迭代器中,如果调用了remove()方法,删除了当前对象,那么,lastRet就派上用场了。可以通过再次遍历列表,找到需要删除的对象,并将其删除,同事为了防止remove()方法被调用两次,在删除时,会将lastRet设置为null, 这样当一个线程在执行删除时,其他线程不会再次执行删除了。
3)current保存了迭代器的下一个指向的位置,调用hasNext()时,可以立即知道是否还有空余的对象,更重要的是,如果在迭代器创建后,其他线程多次调用了出队的方法,可能导致lastRet和current都变成悬挂的指针了,这时,需要判断current的next是否为自己,就可以知道自己是否已经被出队,是否需要重定位current的位置。
- 对锁的精巧使用
LinkedBlockingQueue将读和写分离,可以让读写操作在互不干扰的情况下,完成各自的功能,提高并发的吞吐量。因为,LinkedBlockingQueue的读写分别在头尾进行,所以可以做到互不干涉。
有空需要自己仔细琢磨自己实现一个ConcurrentHashMap和LinkedBlockingQueue,里面有很多值得学习的多线程的底层知识。