非阻塞队列(ConcurrentLinkedQueue)
什么是非阻塞队列?这是一种无界的非阻塞队列,它的底层数据结构是通过单向链表实现的,只可以在队尾通过CAS算法添加元素,或者是在队尾通过CAS算法移除元素,它是一种线程安全的队列。
1.offer操作:offer操作是指通过CAS算法在队列末尾添加元素,checkNotNull(e)方法检测添加的元素是否为空,如果是空则会抛出异常。然后定义一个新节点,它的值就是传入的e,让t和p的指向tail节点,接下来获取p的下一个节点,如果下一个节点是空节点,那么就执行cas操作p.casNext(null, newNode),这个方法的含义是判断p节点的下一个节点是否为null,如果是bull就会把它修改为newNode节点,这个判断并更新的操作是原子性的。casTail(t, newNode);接下来需要把新加入的节点通过CAS算法设置为尾节点。下面的两个判断是基于多线程设计的,如果有两个线程同时调用p.casNext方法,只要线程1的节点newNode1可以被设置成功,而另一个线程在判断为null的时候返回了false,于是调用了下面的 p = (p != t && t != (t = tail)) ? t : q;方法,此时t节点表示尾节点了,p被赋值为它的下一个节点的引用q,于是就继续回到之前的循环判断,再次执行插入节点,最终两个新节点的添加都会设置成功,返回一个true。
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
}
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
p = (p != t && t != (t = tail)) ? t : q;
}
}
2.add操作:
add方法实际调用的是offer方法
public boolean add(E e) {
return offer(e);
}
3.poll操作
poll方法是指从队头移除一个元素,它的原子操作是体现在対头结点的值的修改上的,当然之后还有设置系的头结点的原子操作,看一下源码吧!其实思路很简单。restartFromHead是一个goto标记,在下面发现p==q的时候会调用跳转到这里,重新执行移除操作。Node h = head, p = h使得h和p获得头结点的引用,E item保存头结点的值,判断它的值是否为null,非null就可以通过CAS设置item为null,括号中的item是一个预期值,预期值与实际值相同,才能设置成功,返回true,继续下面的操作,更新头结点;如果队列为空,则返回null。如果设置失败且队列不为空,则会把p=q,然后回到之前的goto标志处,重新尝试移除。
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
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) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
4.peek操作
peek操作只要获取数据就可以了,不需要修改头结点,获取头节点的item值并返回,它可能是null,也可能是有数据的;在队列中item为空,并且队列不为空,则把p设置为q,重新尝试。
public E peek() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
5.size操作
处理size方法外,还调用了first方法和succ方法,first方法类似于peek方法,succ方法可以获取下一个节点的引用。这个获取size的方法是读取头结点,然后往后遍历,修改count值,最后得到size值并返回。
public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p))
if (p.item != null)
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE)
break;
return count;
}
Node<E> first() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
boolean hasItem = (p.item != null);
if (hasItem || (q = p.next) == null) {
updateHead(h, p);
return hasItem ? p : null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
final Node<E> succ(Node<E> p) {
Node<E> next = p.next;
return (p == next) ? head : next;
}
6.remove操作
remove方法是对poll方法的包装,poll方法会返回一个值,而remove方法中对这个返回值进行判断,如果为空就抛出异常。
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
7.contains操作
contains方法的过程和size方法有些类似,先获取头结点,然后从头往后遍历查找,找到了就会返回true。遍历结束还没有找到就返回false。
public boolean contains(Object o) {
if (o == null) return false;
for (Node<E> p = first(); p != null; p = succ(p)) {
E item = p.item;
if (item != null && o.equals(item))
return true;
}
return false;
}
小结:
基于CAS实现的非阻塞队列在并发下实现了对数据的添加和删除实现了高可靠性,同时也有比较高的效率,但是在获取size的时候很可能队列会被修改,这种情况下,size的返回结果是不准确的,在多线程下,通过死循环的设计,可以使得多线程在设置数据失败后还有机会自旋重试。相对阻塞算法,这种算法的特点是使用自旋带来的CPU消耗代替了阻塞带来的线程从用户态到内核态的损耗。
基于链表实现的阻塞队列(LinkedBlockingQueue)
什么是阻塞队列?阻塞队列是基于锁实现的,分别对入队和出队加锁,方法如下:
1.offer操作:
如下代码先判断要添加的元素是否为空,一旦为空就抛出异常。接下来判断队列是否已满,已满则丢弃该元素,返回false。添加的代码执行前先获取独占锁,在数量小于最大容量的时候执行 last = last.next = node;添加元素,再对原子变量加一,如果加一后还是没有满,则唤醒条件等待队列的下一个线程,继续执行添加操作。最后释放锁。
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.put操作:
该操作的lockInterruptibly()方法和上面的lock()方法的区别是该操作导致被阻塞的线程在沉睡中被别的线程设置了中断标志,就会抛出异常。也就是说,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 {
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.poll操作:
该操作的特点是需要调用takeLock.lock();和takeLock.unlock();方法。别的部分的思路和offer()方法十分类似。
4.peek操作:
peek()方法是获取队头的值,这也是需要加takelock锁的。
5.take操作:
该方法也是获取元素,但被它挂起的线程是可以被中断的,获取锁的方法是 takeLock.lockInterruptibly();
6.remove操作:
remove操作的时候不能入队也不能出队,它的特点就是加了双重锁。
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();
}
7.size操作:
这里直接获取volatile int value;相对非阻塞队列在size的获取上精度更高。关于为什么非阻塞队列为什么需要遍历数组而不是直接维护一个原子变量呢?因为CAS操作的添加和删除不是原子操作,只有比较并更新是原子操作,不能维护一个原子变量。
public int size() {
return count.get();
}
public final int get() {
return value;
}
小结:
该阻塞模型分别通过两把独占锁实现了入队和出队的原子性,在添加元素和删除元素上实现了生产消费模型。
基于数组实现的阻塞队列(ArrayBlockingQueue)
换为数组实现,和上述的链表实现十分相似,在最后我们在分析一下它们的区别。
先看一眼构造方法:
可以在初始化的时候指定大小,初始化的时候实例化一把全局的独占锁并获取两个条件变量(这和我之前一篇并发文章中实现的那个利用自己实现的不可重入独占锁来实现一个生产消费模型十分相似)。
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
1.offer操作:
直接加了独占锁。关注一下第二个方法的一个小细节, 在添加了数据后调用了notEmpty.signal();方法唤醒一个在等待中的线程,在notEmpty条件变量中的是消费者线程,被唤醒执行元素的出队。
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
2.put操作:
所有队列的put方法都几乎一致,都是可响应中断的添加操作。
3.poll操作:
加锁并移除数据,再调用notFull.signal();方法唤醒一个生产者线程。
4.take操作:
可以响应中断。
5.peek操作:
加锁获取队首的元素。
6.size操作:
加锁返回计数值,结果很精确。
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
小结:
在上述三个队列中,我们可以发现,基于全局独占锁实现的数组获取的size是最精确的。数组实现的阻塞队列实现了修改操作的原子性,在写少读多的情况下,这种形式的队列应用更加广泛。
带优先级的无阻塞队列(PriorityBlockQueue)
什么是带优先级的无阻塞队列?带优先级是指这个队列在出队的时候会优先处理那些优先级高的任务,该队列内部是维护有一个自平衡二叉树的,通过实现Comparator接口实现比较器,可以通过比较器实现大根堆和小根堆,该二叉树在低层是基于数组实现的,既然是数组且队列是无界队列,所以需要在合适的时候通过CAS算法扩容。
无界阻塞延迟队列(DelayQueue)
该队列的特点是该队列中的元素有过期时间,队列只会使得过期的元素出队,并且会优先出队最早过期的元素。这个队列其实是通过PriorityBlockQueue实现的,该队列实现了获取剩余过期时间的接口,还实现了比较的接口。这是一个有优先级的队列。