LinkedBlockingQueue
上章说到了ArrayBlockingQueue,现在来看看LinkedBlockingQueue.LinkedBlockingQueue也是一个阻塞的有界队列,其用法与ArrayBlockingQueue基本一致,只不过内部实现不一样,这里不再写demo咱们直接来看看起源码.首先咱们看看其关键属性和构造方法
//node节点,只有一个next,所以是单向链表
static class Node<E> {
//存储节点元素
E item;
//指向下一节点
Node<E> next;
Node(E x) { item = x; }
}
//容量
private final int capacity;
//当前元素数量
private final AtomicInteger count = new AtomicInteger(0);
//头节点
private transient Node<E> head;
//尾节点
private transient Node<E> last;
//take, poll等取出方法的锁
private final ReentrantLock takeLock = new ReentrantLock();
//取出元素方法等待条件
private final Condition notEmpty = takeLock.newCondition();
//put, offer等添加方法的锁
private final ReentrantLock putLock = new ReentrantLock();
//添加元素方法等待条件
private final Condition notFull = putLock.newCondition();
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
//头结点、尾节点都是Null
last = head = new Node<E>(null);
}
咱们先来看看其add()方法
//add方法定义与于父类AbastactQueue中,实际上是调用实现类中的offer()方法
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
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);
//加锁
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
//如果未满,则入列,元素数量+1
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
//唤醒在不为满(即put())条件等待的线程
notFull.signal();
}
} finally {
putLock.unlock();
//这里c默认是-1,经过enqueue入队,c=count.getAndIncrement(),如果变成0,说明链表队列原来是空的,现在有元素了
if (c == 0)
//唤醒在不为满(即take())条件等待的线程
signalNotEmpty();
return c >= 0;
}
private void enqueue(Node<E> node) {
//当前节点加到链中,并置为尾节点
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
与ArrayBlockingQueue一样,offer()方法并不阻塞.
再来看看put()和take()方法
public void put(E e) throws InterruptedException {
//元素为Null抛出异常
if (e == null) throw new NullPointerException();
int c = -1;
//构造节点
Node<E> node = new Node(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();
//队列未满,则唤醒所有put()上等待的队列
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
//唤醒在不为满(即take())条件等待的线程
signalNotEmpty();
}
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)
//如果还有元素,则唤醒在take()方法上等待的线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
//默认c为-1,经过take后c=count.getAndDecrement();说明原来队列是满的,take后不满,就可以唤醒notFull条件队列
if (c == capacity)
signalNotFull();
return x;
}
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;
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
再来看看remove()
//因为remove操作需要遍历整个链表,所以加2把锁遍历
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 fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
void unlink(Node<E> p, Node<E> trail) {
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
if (count.getAndDecrement() == capacity)
notFull.signal();
}
这个类很简单,相信大家也是要看就懂,在此不做过多分析,开始学习下一个
ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个非阻塞的无界的线程安全队列(基于cas实现),下面看看其是如何实现的
private static class Node<E> {
//存放元素
volatile E item;
//指向下一元素
volatile Node<E> next;
//构建node节点
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
//cas更新Item
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
//cas跟新next节点
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class k = Node.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
//头节点
private transient volatile Node<E> head;
//尾节点
private transient volatile Node<E> tail;
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
可以看到ConcurrentBlockingQueue并没有容量等相关属性,因此是一个无界队列
下面咱们看看其入列方法
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
checkNotNull(e);
//构建节点
final Node<E> newNode = new Node<E>(e);
//t为tail节点,p为尾节点,默认相等,采用自旋+cas方式,直到入队成功
for (Node<E> t = tail, p = t;;) {
//获得p的下一个节点
Node<E> q = p.next;
// 如果下一个节点是null,即p节点就是尾节点
if (q == null) {
//将入队节点newNode设置为当前队列尾节点p的next节点
if (p.casNext(null, newNode)) {
// 如果p.casNext有个线程成功了,p=newNode
// 比较 t (tail) 是不是 最后一个节点
if (p != t)
// 如果不等,就利用cas将,尾节点移到最后
// 如果失败了,那么说明有其他线程已经把tail移动过,也是OK的
casTail(t, newNode);
return true;
}
}
else if (p == q)
// 有可能刚好插入一个,然后P 就被删除了,那么 p==q
// 这时候在头结点需要从新定位。
p = (t != (t = tail)) ? t : head;
else
//p有next节点,表示p的next节点是尾节点,则需要重新更新p后将它指向next节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
从源代码我们看出入队过程中主要做了三件事情
- 定位出尾节点
- 使用CAS+自旋指令将入队节点设置成尾节点的next节点
- 重新定位tail节点
再看看出列方法
public E poll() {
// 设置起始点
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 利用cas 将第一个节点,设置未null
if (item != null && p.casItem(item, null)) {
// 和上面类似,p的next被删了,
// 然后然后判断一下,目的为了保证head的next不为空
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
// 有可能已经被另外线程先删除了下一个节点
// 那么需要先设定head 的位置,并返回null
updateHead(h, p);
return null;
}
else if (p == q)
// 这个一般是删完了(有点模糊)
continue restartFromHead;
else
// 和offer 类似,这历使用保证下一个节点有值,才能删除
p = q;
}
}
}
首先获取head节点的元素,并判断head节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走;如果不为空,则使用CAS的方式将head节点的引用设置成null,如果CAS成功,则直接返回head节点的元素;如果CAS不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取head节点。如果p节点的下一个节点为null,则说明这个队列为空(此时队列没有元素,只有一个伪结点p),则更新head节点。
再来看看size()方法
public int size() {
int count = 0;
//这里会遍历所有节点,效率较低
for (Node<E> p = first(); p != null; p = succ(p))
if (p.item != null){
if (++count == Integer.MAX_VALUE)
break;
}
return count;
}
size()方法会遍历所有节点,效率较低,因此如果想判断队列是否为空是可考虑使用empty()
public boolean isEmpty() {
return first() == null;
}