BlockingQueue,是java.util.concurrent 包提供的用于解决并发生产者 - 消费者问题的最有用的类,它的特性是在任意时刻只有一个线程可以进行take或者put操作,并且BlockingQueue提供了超时return null的机制,在许多生产场景里都可以看到这个工具的身影。
队列类型
- 无限队列 (unbounded queue )几乎可以无限增长
- 有限队列 (bounded queue ) ,定义了最大容量
队列数据结构
队列实质就是一种存储数据的结构 :
通常用链表或者数组实现
一般而言队列具备FIFO先进先出的特性,当然也有双向队列 (Deque)优先级队列
主要操作:入队(EnQueue)与出队(Dequeue)
常见的4种阻塞队列
ArrayBlockingQueue 由数组支持的有界队列
BlockingQueue API
方法 | 说明 |
---|---|
add() | 如果插入成功则返回true,否则抛出IllegalStateException |
put() | 将指定的元素插入队列,如果队列满了,那么会阻塞直到空间插入为止 |
offer() | 如果插入成功则返回true,否则返回false |
offer(E e ,long timeout ,TimeUnit unit ) | 尝试将元素插入队列,如果队列已满,那么会阻塞直到有空间插入 |
take() | 获取队列的头部元素并将其删除,如果队列为空,则阻塞并等待 |
poll(long timeout,TimeUnit unit ) | 检索并删除队列的头部,如果必要,等待指定的时间,如果还没有空间,则返回null |
ArrayBlockingQueue
队列基于数组实现,容量大小在创建ArrayBlockingQueue对象时已定义好
数据结构如下图
队列创建:
应用场景
在线程池中有比较多的应用,生产者消费者场景
工作原理
基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞
接下来,直接看ArrayBlockingQueue的构造函数。
public ArrayBlockingQueue(int capacity) { this(capacity, false); } 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(); }
发现ArrayBlockingQueue竞争是用ReentrantLock和Condition来实现阻塞和唤醒的。
接下来,我们来看向阻塞队列中添加元素的实现。
public boolean offer(E e) { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lock(); try { // 如果数组已满,直接返回false if (count == items.length) return false; else { insert(e); return true; } } finally { lock.unlock(); } } public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; // 使用了中断锁 lock.lockInterruptibly(); try { // 如果数组中存储元素的长度等于数组的长度 // 将当前线程加入到条件队列 while (count == items.length) notFull.await(); insert(e); } finally { lock.unlock(); } } private void insert(E x) { items[putIndex] = x; // putIndex = putIndex + 1 ,如果putIndex 等于数组的长度,则从头再来 putIndex = inc(putIndex); ++count; // 通知条件队列中等待take()或poll()的线程来数组中取数据 notEmpty.signal(); } final int inc(int i) { return (++i == items.length) ? 0 : i; }
上面的逻辑太简单了,直接向数组元素中添加元素即可 ,只是值得区分一下,如果queue中的元素已满,put()方法会等待,而offer()方法不会等待。下面看take()和poll()方法 。
public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { return (count == 0) ? null : extract(); } finally { lock.unlock(); } } public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { // 如果数组元素的长度为0,则等待 while (count == 0) notEmpty.await(); return extract(); } finally { lock.unlock(); } } private E extract() { final Object[] items = this.items; E x = this.<E>cast(items[takeIndex]); // 清空takeIndex的值 items[takeIndex] = null; // 如果takeIndex等待数组的长度,则takeIndex = 0 ,从头再来,否则takeIndex = takeIndex + 1 takeIndex = inc(takeIndex); --count; // 唤醒在条件队列中等待向queue加入元素的线程 notFull.signal(); return x; }
大家需要注意的是当queue为空时,take()方法会等待,而poll()方法不会等待,不知道大家发现规率没有,put(),take() 这些方法名有t的则等待,联想一下wait单词,是不是也有t,而offer(),poll()这些方法名有o的不会等待。这些都是对queue增加删除元素,因此使用了ReentrantLock的lock()与unlock()方法,所以不用担心线程问题。
接下来,我们来看一下remove()方法
public boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); try { for (int i = takeIndex, k = count; k > 0; i = inc(i), k--) { // 遍历整个数组,找到要删除元素的索引 if (o.equals(items[i])) { removeAt(i); return true; } } return false; } finally { lock.unlock(); } } void removeAt(int i) { final Object[] items = this.items; // 如果要删除元素的索引正好是takeIndex,则模拟extract()方法实现即可 if (i == takeIndex) { items[takeIndex] = null; takeIndex = inc(takeIndex); } else { // i != takeIndex,i ~ putIndex 所有的元素前移一位即可 // 也就是items[i] = items[i + 1 ],并且将 items[putIndex] = null // putIndex = putIndex -1 即可 for (;;) { int nexti = inc(i); if (nexti != putIndex) { items[i] = items[nexti]; i = nexti; } else { items[i] = null; putIndex = i; break; } } } --count; // 唤醒所有在条件队列等待向队列中put()元素的一个线程 notFull.signal(); }
ArrayBlockingQueue物理上是一个线性数组实现,却逻辑上是一个环状数组,当count >0时,每次put()或offer()元素之前,items[putIndex]肯定没有值,但每次take()或poll()元素时,items[takeIndex]一定有值,同时putIndex每次追着takeIndex跑,永远不会越过putIndex跑到前面去,也像夸父逐日一样,永远也追不上。我觉得用下面这图来理解,更加生动,
这一点需要注意,大家发现没有,即使是简单的生产者消费者的实现Doug Lea也是写得如此性能之高,每次put()或take()事实上基本没有性能开销,相当于精确的向数组中添加元素,或删除元素,这一点值得我们学习,既然理解了,我们来看一个面试题。
public class ArrayBlockingQueueTest { public static void main(String[] args) throws Exception { final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(3); queue.put(1); queue.put(2); queue.put(3); Thread t1 = new Thread(new Runnable() { @Override public void run() { try { queue.put(4); System.out.println("1"); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { queue.put(5); System.out.println("2"); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); Thread.sleep(200); t2.start(); Thread.sleep(200); queue.take(); } } 答案输出 : 只能是 1
如果不明白 ,一定要去看之前的ReentrantLock源码解析及ReentrantReadWriteLock源码解析 这篇博客 ,毫无疑问,先将1,2,3 put进入ArrayBlockingQueue中,此时线程1启动,put 4 到ArrayBlockingQueue,但此时数组已满,只能将当前线程加入到条件队列中,过200毫秒,线程2启动,此时发现ArrayBlockingQueue仍然已满,则只能将当前线程2加入到条件队列中,再过200毫秒,此时主线程从队列中take()一个元素,会调用signal()通知notFull条件队列中的线程进入同步队列,只要抢到锁,即可向ArrayBlockingQueue中添加元素,因此线程2将永远都在条件队列中等待,无法put元素到ArrayBlockingQueue中,但在本例中,只要修改一点点,如果将 t2.start();后的Thread.sleep(200);注释掉,结果将可能是1,也可能是2,因为线程2的wait()方法可能和主线程take()方法同时执行,take()方法通过signal()方法唤醒线程1从条件队列到同步队列中,线程1并随时等待抢锁,但ReentrantLock默认是非公平锁,线程2和转移到同步队列中的线程1可能存在锁竞争,因此输出的结果就不确定了。
在看LinkedBlockingQueue的过程中,发现一个问题,在LinkedBlockingQueue用到了putLock和takeLock两把锁,这样put,offer 就只需要获取putLock锁即可 ,take ,poll()就只需要获取takeLock锁即可 ,那么ArrayBlockingQueue这样做是不是性能更好呢?答案是肯定的,性能会更加好,但Doug Lea为什么不这么做呢?聪明的小伙伴有知道原因的不?假如put 和poll用不同的锁,假如用不同锁实现 ,线程1在10:59:59时,向ArrayBlockingQueue队列中put一个元素,在11:00:00 ,线程2来poll()一个元素,但此时线程1 put元素还没有完成,count 还没有 + 1 ,而线程2发现count值是0,直接返回false,如果用同一把锁实现,线程2在获取锁时,发现线程1获得了锁,需要等待,直到线程1释放锁,此时count == 1 ,而线程2发现count > 0 ,则从队列中取出元素,在ArrayBlockingQueue中使用两把锁能提升性能,但给程序逻辑上带来的不正确结果,按道理线程2后调用offer方法,而线程1是先调用put方法,那么线程2肯定能获取值队列中的元素,如果使用两把锁,则获取不到,性能再怎样优化,也需要保证程序正确的语义,我猜这是Doug Lea不用两把锁来实现的原因吧。
接下来,我们来看LinkedBlockingQueue的源码实现。
LinkedBlockingQueue
是一个基于链表的无界队列(理论上有界)
BlockingQueue blockingQueue = new LinkedBlockingQueue<> ();
上面这段代码中,blockingQueue 的容量将设置为 Integer.MAX_VALUE 。
向无限队列添加元素的所有操作都将永远不会阻塞,[注意这里不是说不会加锁保证线程安全],因此它可以增长到非常大的容量。使用无限 BlockingQueue 设计生产者 - 消费者模型时最重要的是 消费者应该能够像生产者向队列添加消息一样快地消费消息 。否则,内存可能会填满,然后就会得到一个 OutOfMemory 异常。
我们先看LinkedBlockingQueue的构造函数。
static class Node<E> { E item; // 指向next节点 Node<E> next; // 存储插入元素的值 Node(E x) { item = x; } } /** The capacity bound, or Integer.MAX_VALUE if none */ private final int capacity; /** Current number of elements */ private final AtomicInteger count = new AtomicInteger(0); /** * Head of linked list. * Invariant: head.item == null */ private transient Node<E> head; /** * Tail of linked list. * Invariant: last.next == null */ private transient Node<E> last; /** Lock held by take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock(); /** Wait queue for waiting takes */ private final Condition notEmpty = takeLock.newCondition(); /** Lock held by put, offer, etc */ private final ReentrantLock putLock = new ReentrantLock(); /** Wait queue for waiting puts */ private final Condition notFull = putLock.newCondition(); public LinkedBlockingQueue() { // 默认LinkedBlockingQueue的最大容量是2147483647 this(Integer.MAX_VALUE); } public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; // 队列初始化,头,尾节点指向一个item为空的节点,和同步队列很像 last = head = new Node<E>(null); }
接下来,我们来看put方法
public void put(E e) throws InterruptedException { 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); // 队列中的元素个数 + 1 c = count.getAndIncrement(); // 如果 c + 1 少于最大容量,唤醒当前条件队列的线程来插入元素 if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } // 队列中有元素了,则唤醒条件队列中等待take()元素的线程可以来获取数据了 // 因为在take() ,poll()方法中会自己唤醒notFull中条件队列中的线程,因此作为生产者,只唤醒一次 // 避免无用的唤醒 ,导致线程park(),影响程序的性能 if (c == 0) signalNotEmpty(); } private void enqueue(Node<E> node) { //将当前节点插入队列的尾节点 last = last.next = node; } private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { // 通知等待队列中,需要take()元素的线程来队列中取数据 notEmpty.signal(); } finally { takeLock.unlock(); } }
接下来,我们看take()方法实现。
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { // 如果队列中元素个数为0,则当前线程阻塞 while (count.get() == 0) { notEmpty.await(); } // 从头节点中移除元素 x = dequeue(); c = count.getAndDecrement(); // 如果有等待take() 或poll()的线程,唤醒它 if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } // 如果队列中元素减少已经达到阈值,唤醒生产者线程可以向队列中添加元素 if (c == capacity) signalNotFull(); return x; } private E dequeue() { // assert takeLock.isHeldByCurrentThread(); // assert head.item == null; // 整个过程就是头节点后移一位,并返回头节点的存储元素的值 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(); } }
再来看看peek()方法。
public E peek() { if (count.get() == 0) return null; final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { Node<E> first = head.next; if (first == null) return null; else return first.item; } finally { takeLock.unlock(); } }
peek()方法就很简单了,直接获取头节点的next节点的item值即可,和take()方法的区别是,take()方法会删除节点,但peek()方法不会删除节点 。
public boolean remove(Object o) { if (o == null) return false; // putLock 和takeLock 加锁 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 { // putLock 和takeLock 解锁 fullyUnlock(); } } 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(); } void fullyLock() { putLock.lock(); takeLock.lock(); } void fullyUnlock() { takeLock.unlock(); putLock.unlock(); }
在删除元素的过程中,对整个链表加锁和解锁,先遍历整个链表,找到链表中元素值和当前传入值相同的元素,并从链表中移除,但聪明的小伙伴有没有发现,如果链表中连续插入两个相同的值,remove()时,只会移除第一个,第二个仍然在链表节点中。
我们看LinkedBlockingQueue遍历 。
private class Itr implements Iterator<E> { 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; } private Node<E> nextNode(Node<E> p) { //在调用next()方法的过程中,当前节点可能被其他线程take()或poll()掉,这时需要跳过这些被获取这些被移除的节点 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 { // 如果调用了hasNext ,current 不为空,而调用next方法时current为空,则会抛出NoSuchElementException if (current == null) throw new NoSuchElementException(); E x = currentElement; lastRet = current; current = nextNode(current); currentElement = (current == null) ? null : current.item; return x; } finally { fullyUnlock(); } } }
遍历时实际上是先调用其hashNext()方法,如果返回true,再调用next()方法,获取返回值。我们也知道HashMap是不允许在遍历过程中调用remove()方法的,但是队列中允许不?我们来看一个例子。
public class LinkedBlockingQueueTest { public static void main(String[] args) throws Exception { LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); Integer i1 = new Integer(1); Integer i2 = new Integer(2); Integer i3 = new Integer(3); queue.put(i1); queue.put(i2); queue.put(i3); queue.put(i1); Iterator<Integer> it = queue.iterator(); int i = 0; while (it.hasNext()) { Integer item = it.next(); System.out.println(item); if(i == queue.size() -1 ){ queue.remove(item); } i++; } System.out.println("======================"); it = queue.iterator(); while (it.hasNext()) { Integer item = it.next(); System.out.println(item); } } } 答案输出 : 1 2 3 1 ====================== 2 3 1 发现神奇没有,本来是移除队列中最后一个item等于1的节点,却移除的是队列中第一个item = 1 的节点。那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) { // 从队列head节点开始 向队列tail 节点遍历 ,只要找到Node的item值和当前正在遍历Node相等的值,则移除它 if (o.equals(p.item)) { unlink(p, trail); return true; } } return false; } finally { fullyUnlock(); } }
在上述例子中,队列最后一个节点的item值是1 ,此时要移除它,此时队列并不是直接移除当前节点,而是重新遍历整个队列,找到一个值和当前item值相等的元素,并移除它。当然上面的例子,我们改一下。加粗代码为修改后的代码
public class LinkedBlockingQueueTest { public static void main(String[] args) throws Exception { LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); Integer i1 = new Integer(1); Integer i2 = new Integer(2); Integer i3 = new Integer(3); queue.put(i1); queue.put(i2); queue.put(i3); queue.put(i1); Iterator<Integer> it = queue.iterator(); int i = 0; while (it.hasNext()) { Integer item = it.next(); System.out.println(item); if(i == queue.size() -1 ){ //queue.remove(item); it.remove(); } i++; } System.out.println("======================"); it = queue.iterator(); while (it.hasNext()) { Integer item = it.next(); System.out.println(item); } } } 结果输出 : 1 2 3 1 ====================== 1 2 3 结果就不一样了,我们看it的remove方法如何实现 。 public void remove() { if (lastRet == null) throw new IllegalStateException(); fullyLock(); try { Node<E> node = lastRet; lastRet = null; // 从队列head节点开始 向队列tail 节点遍历 ,只要找到Node和当前正在遍历Node相等的值,则移除它 for (Node<E> trail = head, p = trail.next; p != null; trail = p, p = p.next) { if (p == node) { unlink(p, trail); break; } } } finally { fullyUnlock(); } } 源码清楚的看出,queue的remove()比较的是Node的item值,而迭代器遍历则比较Node值,这点区分,大家需要注意 。
我相信LinkedBlockingQueue队列还是很好理解的,无非是用一个链表来实现对数据的添加和删除,同时链表的默认长度是Integer.MAX_VALUE值,其他的,好像也没有什么知识点了。
PriorityBlockingQueue
我也是参考PriorityQueue用法与介绍这篇博客的,有兴趣看原文 。
队列是遵循先进先出(First-In-First-Out)模式的,PriorityQueue类在Java1.5中引入并作为 Java Collections Framework 的一部分。
优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。
优先队列不允许空值,而且不支持non-comparable(不可比较)的对象,比如用户自定义的类。优先队列要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。
优先队列的头是基于自然排序或者Comparator排序的最小元素。如果有多个对象拥有同样的排序,那么就可能随机地取其中任意一个。当我们获取队列时,返回队列的头对象。
优先队列的大小是不受限制的,但在创建时可以指定初始大小。当我们向优先队列增加元素的时候,队列大小会自动增加。
PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境。
二、实现原理
Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。
上图中我们给每个元素按照层序遍历的方式进行了编号,如果你足够细心,会发现父节点和子节点的编号是有联系的,更确切的说父子节点的编号之间有如下关系:
-
leftNo = parentNo*2+1
-
rightNo = parentNo*2+2
-
parentNo = (nodeNo-1)/2
通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。
PriorityQueue的peek()和element操作是常数时间,add(),offer(), 无参数的remove()以及poll()方法的时间复杂度都是log(N)。
add()&offer()
add(E e)和offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false。对于PriorityQueue这两个方法其实没什么差别。
新加入的元素可能会破坏小顶堆的性质,因此需要进行必要的调整。
接下来,我们看向优先级队列中添加元素实现过程 。
public boolean add(E e) { return offer(e); } public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) // 如果当头存储元素的数组下标大于队列的长度,则进行扩容 grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else // 先将元素插入到叶子节点,然后将其与父亲节点比较,如果优先级大于父亲节点,则交换 // 直到根节点为止 siftUp(i, e); return true; } private void grow(int minCapacity) { int oldCapacity = queue.length; // 如果旧队列的长度小于64 ,则扩容为原来两倍,如果大于64,则扩容为原来1.5倍 int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // 队列最大长度为Integer.MAX_VALUE if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } private void siftUp(int k, E x) { // 如果用户自己指定了比较器,则使用自己的比较器进行比较 if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); } private void siftUpComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { // 获取queue[k]的父亲节点索引 // 之前我们看到,当前节点的左右子节点的下标分别为leftNo = parentNo*2+1,和rightNo = parentNo*2+2 // 根据子节点推算父亲节点的下标为 parent = (lefNo -2 )/2 = (rightNo-2)/2 = (k - 1) /2 = (k - 1) >>> 1 // 本来用除2更加好理解,但是为了性能,这里作者使用了 无符号右移 >>> int parent = (k - 1) >>> 1; Object e = queue[parent]; // 如果当前key 的优先级小于父亲节点的优先级,则直接返回 if (key.compareTo((E) e) >= 0) break; // 如果当前节点的优先级大于父亲节点的优先级,则当前节点与父亲节点交换位置 queue[k] = e; k = parent; } queue[k] = key; }
在上述代码中需要注意,key对应的值越小,则优先级越高。
上述代码中,扩容函数grow()类似于ArrayList里的grow()函数,就是再申请一个更大的数组,并将原数组的元素复制过去,这里不再赘述。需要注意的是siftUp(int k, E x)方法,该方法用于插入元素x并维持堆的特性。新加入的元素x可能会破坏小顶堆的性质,因此需要进行调整。调整的过程为:从k指定的位置开始,将x逐层与当前点的parent进行比较并交换,直到满足x >= queue[parent]为止。注意这里的比较可以是元素的自然顺序,也可以是依靠比较器的顺序。
element()和peek()
element()和peek()的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回null。根据小顶堆的性质,堆顶那个元素就是全局最小的那个;由于堆用数组表示,根据下标关系,0下标处的那个元素既是堆顶元素。所以直接返回数组0下标处的那个元素即可。
public E element() { E x = peek(); if (x != null) return x; else throw new NoSuchElementException(); } public E peek() { if (size == 0) return null; return (E) queue[0]; }
remove()和poll()
remove()和poll()方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。
public E remove() { E x = poll(); if (x != null) return x; else throw new NoSuchElementException(); } public E poll() { if (size == 0) return null; int s = --size; modCount++; E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) siftDown(0, x); return result; } private void siftDown(int k, E x) { // 如果用户自身指定了比较器,则使用指定的比较器 if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // loop while a non-leaf while (k < half) { // 当前节点的左孩子在队列中的索引为 2k + 1 // 右孩子在队列中的索引为2k + 2 int child = (k << 1) + 1; // assume left child is least Object c = queue[child]; int right = child + 1; // k < half ,则当前节点肯定存在子节点,但可能不存在右子节点,因此需要加上right < size 的判断 // 如果当前节点存在左右子节点,则比较其优先级大小 ,比较出优先级小的存储到c中 if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) // 如果右子节点的优先级比左子节点的优先级低,则记录到c中 c = queue[child = right]; // 再将当前节点与左右子节点中优先级比较低的子节点进行比较 ,如果key的优先级比子节点低,则交换 // 依此类推,直到叶子节点 if (key.compareTo((E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = key; }
上述代码首先记录0下标处的元素,并用最后一个元素替换0下标位置的元素,之后调用siftDown()方法对堆进行调整,最后返回原来0下标处的那个元素(也就是最小的那个元素)。重点是siftDown(int k, E x)方法,该方法的作用是从k指定的位置开始,将x逐层向下与当前点的左右孩子中较小的那个交换,直到x小于或等于左右孩子中的任何一个为止。在这里需要注意的是,值越大,表示优先级越小。
remove(Object o)
remove(Object o)方法用于删除队列中跟o相等的某一个元素(如果有多个相等,只删除一个),该方法不是Queue接口内的方法,而是Collection接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。具体来说,remove(Object o)可以分为2种情况:1. 删除的是最后一个元素。直接删除即可,不需要调整。2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用一次siftDown()即可。
public boolean remove(Object o) { // 遍历整个队列,找到当前要删除元素在队列中的索引 int i = indexOf(o); if (i == -1) return false; else { // 移除索引处的节点 removeAt(i); return true; } } private int indexOf(Object o) { if (o != null) { for (int i = 0; i < size; i++) if (o.equals(queue[i])) return i; } return -1; } private E removeAt(int i) { assert i >= 0 && i < size; modCount++; int s = --size; // 如果删除的是最后一个元素 if (s == i) // removed last element queue[i] = null; else { E moved = (E) queue[s]; queue[s] = null; // 先将当前节点与左右子节点比较优先级,如果小于,则与叶子节点交换位置 siftDown(i, moved); // 如果当前节点并没有与其叶子节点交换过位置,则需要与其父亲节点比较优先级了,如果大于 ,则交换 if (queue[i] == moved) { // 当前节点与父亲节点比较优先级,如果大于,则与父亲节点交换位置 siftUp(i, moved); if (queue[i] != moved) return moved; } } return null; }
在上述分析中,如果与子节点比较优先级后并没有交换位置,此时需要与其父亲节点比较优先级。如果优先级大于父亲节点,则需要交换位置,什么情况下会出现这种情况呢?
我相信此时此刻,你肯定理解了PriorityBlockingQueue的实现原理了,用一个数组来模拟一颗树的操作,但树要满足以下几个条件。
- 左子节点在数组中的索引为 leftChildIndex = parentIndex * 2 + 1
- 右子节点在数组中的索引为rightChildIndex = parentIndex * 2 + 2
- 父亲节点的索引为 parentIndex =( leftChildIndex -1 )/ 2 = (rightChildIndex -1 ) /2 = ( leftChildIndex -1 ) >>> 1 = (rightChildIndex -1 ) >>> 1
- 父亲节点的优先级肯定大于左右子节点的优先级,换句话说,queue[parent].compareTo(queue[child]) < 0
DelayQueue
延迟队列用到了优先级队列来实现,当然也使用了ReentrantLock,我们看他的属性,这些属性有什么用呢?
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> { private transient final ReentrantLock lock = new ReentrantLock(); private final PriorityQueue<E> q = new PriorityQueue<E>(); private Thread leader = null; private final Condition available = lock.newCondition(); }
继续看代码 。
add()&put()&offer()方法
add() put() offer() 三个方法的语义一样。
public boolean add(E e) { return offer(e); } public void put(E e) { offer(e); } public boolean offer(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { // 将当前节点加入到优先级队列中 q.offer(e); // 如果当前节点是优先级队列的根节点,则通知正在等待的take()线程来获取元素 if (q.peek() == e) { leader = null; available.signal(); } return true; } finally { lock.unlock(); } }
为什么当前新加入的节点是队列中优先级最高的节点时,需要通知正在take()方法中等待的线程呢?后面再来分析,先看简单的poll()方法。
public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { E first = q.peek(); // 如果队列中第0个节点的值为空,或时间没有到,则返回空 if (first == null || first.getDelay(TimeUnit.NANOSECONDS) > 0) return null; else // 获取优先级最高的节点并返回 return q.poll(); } finally { lock.unlock(); } }
接下来,我们来看take()方法的实现。take()方法如果获取不到元素线程会阻塞。
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { E first = q.peek(); if (first == null) // 如果当前队列中元素为空,则进入条件队列中等待 available.await(); else { // 获取队列的首节点的延迟时间 long delay = first.getDelay(TimeUnit.NANOSECONDS); if (delay <= 0) // 如果小于0,则从优先级队列中获取首节点 return q.poll(); // 如果有其他线程在等待take() 元素了,则当前线程进行条件队列中等待 else if (leader != null) available.await(); else { Thread thisThread = Thread.currentThread(); leader = thisThread; try { // 等待delay 纳秒,自动唤醒 available.awaitNanos(delay); } finally { if (leader == thisThread) leader = null; } } } } } finally { // 如果leader == null ,而队列中有元素,可能有其他线程在等待take() 元素,需要唤醒它 if (leader == null && q.peek() != null) available.signal(); lock.unlock(); } }
到这里,我们再回头来看offer()方法,为什么offer()方法中会有下面逻辑呢
if (q.peek() == e) { leader = null; available.signal(); }
假如A线程take()时,发现队列中有节点,等待的最小时间为10秒,因此A线程调用available.awaitNanos(delay)等待10秒后自动唤醒,此时B线程调用了offer()方法,加入了一个节点,只需要等待5秒,因此B线程加入的节点是优先级队列中的队头节点,此时B线程需要通知A线程来获取数据,当B线程加入元素后,A线程就不需要等待10秒才能获取到元素,可能只需要5秒就能获取到元素,因此B线程有提前通知A线程的义务。
延迟队列利用了优先级队列来实现,因此延迟队列的实现逻辑变得很简单,我在面试的时候,偶尔会问如果用HashMap来实现Redis的过期失效的功能,怎样实现呢?大部分面试都会说,用定时任务,第隔一秒,将HashMap遍历一次,将过期失效的元素从HashMap中移除掉,这个答案是让我非常失望的,当你学习过延迟队列后,我相信你已经有答案,那我们就来实现一下吧。
public class DelayQueueTest { private static HashMap<String, Object> redis = new HashMap<>(); public static DelayQueue<Node> delay = new DelayQueue(); public static void main(String[] args) throws Exception { invalidKey(); set("key1","value1",1); set("key2","value2",3); set("key3","value3",5); set("key4","value4",6); for(int i = 0;i < 1000;i ++){ System.out.println("size = " + redis.size()); Thread.sleep(1000); } } public static Object get(String key) { return redis.get(key); } public static void set(String key, Object value, long seconds) { redis.put(key, value); delay.offer(new Node(key, addSeconds(new Date(), (int) seconds).getTime())); } public static void invalidKey() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Node node = delay.take(); System.out.println("key = " + node.getItem() + " 被移除掉 " + (System.currentTimeMillis() - node.getTime())); redis.remove(node.getItem()); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } public static Date addSeconds(final Date date, int seconds) { Calendar cal = GregorianCalendar.getInstance(); cal.setTime(date); cal.add(Calendar.SECOND, seconds); return new Date(cal.getTime().getTime()); } static class Node implements Delayed { private Object item; private long time ; public Node(Object item, long time) { this.item = item; this.time = time; } @Override public long getDelay(TimeUnit unit) { return time - System.currentTimeMillis(); } @Override public int compareTo(Delayed o) { return time > ((Node) o).getTime() ? 1 : time == ((Node) o).getTime() ? 0 : -1; } public Object getItem() { return item; } public long getTime() { return time; } } } 结果输出 : size = 4 key = key1 被移除掉 0 size = 3 size = 3 key = key2 被移除掉 0 size = 2 size = 2 key = key3 被移除掉 0 size = 1 key = key4 被移除掉 0 size = 0 size = 0 size = 0 size = 0
上面这个例子实现原理也很简单,在set()元素时,将key 加入到hashMap中,同时将key和有效时间构造成Node加入到延迟队列中,凡是能从延迟队列中取到元素,则这个节点就是过期节点,直接将其从HashMap中移除即可,是不是理解优先级队列后,做类redis缓存失效代码就很简单了,不然,你也只能用定时任务来实现,显然定时任务有太多的缺点。
SynchronousQueue
SynchronousQueue是一个同步阻塞队列,每一个 put操作都必须等待一个take操作。每一个take操作也必须等待一个put操作。SynchronousQueue是没有容量的,无法存储元素节点信息,不能通过peek方法获取元素,peek方法会直接返回null。由于没有元素,所以不能被迭代,它的iterator方法会返回一个空的迭代器Collections.emptyIterator();。
SynchronousQueue比较适合线程通信、传递信息、状态切换等应用场景,一个线程必须等待另一个线程传递某些信息给他才可以继续执行。
SynchronousQueue这个队列不常用,但是线程池中有用到该队列,所以也分析一下。Executors.newCachedThreadPool()方法中使用到了SynchronousQueue
TransferQueue
同步队列的使用和其他队列一样,都需要向里面添加元素,删除元素,先来看向队列中添加元素 。话不多说,直接看代码 。
public void put(E o) throws InterruptedException { if (o == null) throw new NullPointerException(); if (transferer.transfer(o, false, 0) == null) { Thread.interrupted(); throw new InterruptedException(); } } public boolean offer(E o, long timeout, TimeUnit unit) throws InterruptedException { if (o == null) throw new NullPointerException(); if (transferer.transfer(o, true, unit.toNanos(timeout)) != null) return true; if (!Thread.interrupted()) return false; throw new InterruptedException(); } public boolean offer(E e) { if (e == null) throw new NullPointerException(); return transferer.transfer(e, true, 0) != null; }
我们发现了一个规率,SynchronousQueue队列中,不允许添加空值,发现最终都调用了transfer方法,transfer()方法有3个参数,第一个参数是需要添加到队列的数据,第2个参数,表示是否等待,第3个参数表示等待的纳秒值 。我们进入tansfer()方法。因为take() ,poll()方法都是调用transfer()方法来实现的,这里先分析transfer方法中的put()和offer()的部分。不过在分析transfer()方法之前,我们先来看一下TransferQueue的内部结构 。
static final class TransferQueue extends Transferer { static final class QNode { // 当前节点的next节点 volatile QNode next; // next node in queue // put进来的数据 volatile Object item; // CAS'ed to or from null // 调用put()或offer()方法的线程 volatile Thread waiter; // to control park/unpark // 当前节点是否是存储数据的节点 final boolean isData; QNode(Object item, boolean isData) { this.item = item; this.isData = isData; } boolean casNext(QNode cmp, QNode val) { return next == cmp && UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); } boolean casItem(Object cmp, Object val) { return item == cmp && UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); } void tryCancel(Object cmp) { UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this); } boolean isCancelled() { return item == this; } boolean isOffList() { return next == this; } // Unsafe mechanics 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 = QNode.class; itemOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("item")); nextOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("next")); } catch (Exception e) { throw new Error(e); } } } // TransferQueue指向队列的头节点 transient volatile QNode head; // TransferQueue指向队列的尾节点 transient volatile QNode tail; // 记录当前需要被清除但此时并不能清除的节点 transient volatile QNode cleanMe; // TransferQueue的构造函数中初始化head 和tail TransferQueue() { QNode h = new QNode(null, false); // initialize to dummy node. head = h; tail = h; } void advanceHead(QNode h, QNode nh) { if (h == head && UNSAFE.compareAndSwapObject(this, headOffset, h, nh)) h.next = h; // forget old next } void advanceTail(QNode t, QNode nt) { if (tail == t) UNSAFE.compareAndSwapObject(this, tailOffset, t, nt); } boolean casCleanMe(QNode cmp, QNode val) { return cleanMe == cmp && UNSAFE.compareAndSwapObject(this, cleanMeOffset, cmp, val); }
看了上面的代码,结构是不是很像ReentrantLock的同步队列。其实不瞒你说,TransferQueue很像 并发编程之CountDownLatch&CyclicBarrier&Semaphore&Exchanger原理 这篇博客中的Exchanger的实现原理,目的就是和其他线程交换数据,只不过take() ,poll()方法用一个null值来和put()或offer()的线程交换数据,不同的是Exchanger用一个数组来存储数据,但TransferQueue用链表来存储数据。百闻不如一见,我们进入transfer()方法中一看究竟。
Object transfer(Object e, boolean timed, long nanos) { QNode s = null; // 是否有数据,如果是put()或offer()方法调用时,e !=null ,isData = true // 如果是take()或poll()方法调用时,e == null ,isData = false boolean isData = (e != null); for (;;) { QNode t = tail; QNode h = head; // 如果TransferQueue没有初始化完成,请先等待完成初始化 if (t == null || h == null) // saw uninitialized value continue; // spin // 只要是第一次进入transfer()方法,无论是put,offer() 还是 take() ,poll()方法都可能进入下面的代码 // 但有一点注意 ,如果当前尾节点是 take()或poll() 加入的节点,并且h != t ,则以后所有的take()或poll()方法都会走下面代码 // 如果当前尾节点是put或offer() 加入的节点,并且 h != t ,则所有的put或offer() 方法都会进入下面代码块。 // 只要h == t ,上面两种情况将重新分配,可能之前是take()或poll()方法调用才能进入下面代码块,但 // 不断的有生产者生产消息,所有等待take()的线程都获取消息并返回了,此时h == t ,而put 或offer()方法继续调用, // 此时可能就是put 或offer的线程进入下面代码块等待。 // 作者下面的代码实现了一种思想,生产和消费两者,谁先调用,谁就等待 if (h == t || t.isData == isData) { // empty or same-mode QNode tn = t.next; // 并发情况下,有新的节点加入尾节点等待 if (t != tail) // inconsistent read continue; if (tn != null) { // 在并发情况下,其他线程调用了t.casNext(null, s),但还没有调用advanceTail(t, s) // 当前线程帮助它将 tn 设置为尾节点 advanceTail(t, tn); continue; } // offer(E e) 或 poll() 方法不会等待 // 如果在将当前线程加入等待队列尾部过程中有其他线程已经加入了队列尾部,或其他线程调用了 t.casNext(null, s)方法, // 则当前线程直接返回 if (timed && nanos <= 0) // can't wait return null; // 如果节点没有构建,则构建节点, // s !=null 的情况也是存在的,在并发情况下 ,当前线程调用t.casNext(null, s) 方法,同时其他线程也调用了t.casNext(null, s) // 当前线程CAS操作失败,则重新循环 if (s == null) s = new QNode(e, isData); // 将当前节点置为尾节点的next 节点 ,同样使用CAS 操作 if (!t.casNext(null, s)) // failed to link in continue; // 将当前节点置为队列的尾节点 advanceTail(t, s); // swing tail and wait // 等待值被交换 Object x = awaitFulfill(s, e, timed, nanos); if (x == s) { // wait was cancelled // 如果当前线程被中断,则清理当前节点 clean(t, s); return null; } // next == this , 如果s 不是孤立的节点 if (!s.isOffList()) { // not already unlinked advanceHead(t, s); // unlink if head if (x != null) // and forget fields s.item = s; s.waiter = null; } return (x != null) ? x : e; } else { // 如果头,尾节点有变化,或m已经为空,则新一轮循环 QNode m = h.next; if (t != tail || m == null || h != head) continue; // 获取当前m存储的值 Object x = m.item; // isData == (x != null) 出现情况 // 假如进入当前代码块isDate = true , 则x != null 成立,m 是从队列中取出的节点,如果isDate = true ,理论上m.item 为null // 存在两种情况 ,m.item !=null // 1. 在m节点所在线程被中断或时间到了,到达指定时间没有其他线程与它交换数据,此时 m 将被tryCancel 了, 在tryCancel()方法 中 // m.item = m ,此时m.item 将不为空。 // 2. 如果其他线程已经调用了 m.casItem(x, e) 方法,如果isData = true,则 e 肯定有值,此时m.item将不为空 // 反过来 isData= false ,只有一种情况,其他线程执行了 m.casItem(x, e),因为isData = false , 所以e == null ,此时 x != null 将为false // 最终也会执行advanceHead(h, m) 方法,将m节点设置为头节点,并且h节点从队列中移除被 jvm 回收 // 在isData == (x != null) 分析中,有一种情况漏掉了, 如果 isData = false ,而m 节点被tryCancel()了 // 此时x == m ,则也会调用 advanceHead(h, m) 方法 // 如果上面两种情况都不满足,则将m.item = e ,当然这里也是进行CAS操作, // m.casItem(x, e) 也会出现成功与失败,如果成功,则将m 设置为头节点即可,如果失败,则存在抢锁操作,当前线程需要重新找下一个线程 // 交换数据 (如果有线程等待则交换数据) 或 等待( 如果head == t ,只能 park()当前线程进入阻塞) if (isData == (x != null) || // m already fulfilled x == m || // m cancelled !m.casItem(x, e)) { // lost CAS advanceHead(h, m); // dequeue and retry continue; } // 如果当前线程m.casItem(x, e) 操作成功,则将m节点设置为头节点 advanceHead(h, m); // 唤醒m节点所在线程 LockSupport.unpark(m.waiter); return (x != null) ? x : e; } } }
如果生产过剩或消费处于饥饿中,则当前线程进入等待。
Object awaitFulfill(QNode s, Object e, boolean timed, long nanos) { // 记录当前时间 ,如果 timed = true ,并且System.nanoTime() - lastTime <= 0 // 并且还没有其他线程与当前线程交换数据,则当前线程将调用tryCancel()方法,删除当前节点 并 取消等待 long lastTime = timed ? System.nanoTime() : 0; Thread w = Thread.currentThread(); // spins为空循环次数,如果s 为第一个节点,则不做空循环等待 // 如果队列中已经有其他线程在等待,则分4种情况 // 先看timed = true 的情况,timed = true ,则当前调用的方法为offer()方法或poll()方法 // 1. 如果CPU数量 < 2 ,则 spins = 0 // 2. 如果CPU数量 >= 2 ,则spins = 32 // 如果调用的是put() 或take()方法 ,timed = false // 3.如果CPU数量 < 2 ,则spins = 0 // 4.如果CPU数量 >= 2 ,则spins = 16 * 32 = 512 // 从上面设计来看,如果CPU数量小于2 ,害怕CPU忙不过来,则空循环等待的次数将为0 ,减少性能开销 // 如果CPU数量>=2 ,如果有时间限制的方法,则空循环的次数为32,如果循环的次数过多,则可能会导致 // System.nanoTime() - lastTime 的值和0相差较大,会导致返回的时间不准确,结果可能也不准确,因此spins的值设置比较小 // 如果没有时间限制的方法调用,为了降低park()阻塞线程的可能性,因此将空循环等待的次数设置为512,这样 // 尽可能的提升性能 ,我相信到这里,可以看出作者的细腻心思了 int spins = ((head.next == s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); for (;;) { // 如果线程被设置了中断标识 if (w.isInterrupted()) // 取消当前等待中的节点 s.item = s s.tryCancel(e); Object x = s.item; // x != e 有两种情况 // 1 .线程被中断 ,s.tryCancel()方法调用,则s.item = s ,肯定s.item != e 了 // 2. 线程已经和其他线程交换数据,s.e = other.e ,肯定x != e ,上面两种情况出现任意一种,则 返回 e.item if (x != e) return x; if (timed) { // 如果offer() poll()方法时间超时,则调用s.tryCancel(e); 设置 s.item = s long now = System.nanoTime(); nanos -= now - lastTime; lastTime = now; if (nanos <= 0) { s.tryCancel(e); continue; } } // 在线程调用park()之前做垂死挣扎,进行循环检测 s.item != e if (spins > 0) --spins; // 如果上面条件都不满足,只能将当前线程加入到Node的waiter中保存park()当前线程 else if (s.waiter == null) s.waiter = w; else if (!timed) // 如果是put或take方法,则直接park()线程 LockSupport.park(this); else if (nanos > spinForTimeoutThreshold) // 如果是offer或poll方法,parkNanos(),如果nanos纳秒后还没有其他线程与当前线程交换数据 // 当前线程将自动唤醒并调用tryCancel()方法,取消当前节点 LockSupport.parkNanos(this, nanos); } }
接下来,我们来看clean方法,清除被取消的节点 。
void clean(QNode pred, QNode s) { // 清空s 节点保存的线程引用 s.waiter = null; // 如果pre 节点已经从队列中移除,如pred.next = pred ,则不需要处理 while (pred.next == s) { QNode h = head; QNode hn = h.next; // 如果头节点的next节点已经取消,则将当前头节点的next节点设置为头节点 // 直到头节点的next节点不为取消节点 if (hn != null && hn.isCancelled()) { advanceHead(h, hn); continue; } QNode t = tail; // 如果此时队列为空,则直接返回 if (t == h) return; QNode tn = t.next; // 如果此时有新的节点插入节点,则重新开始 if (t != tail) continue; // 如果其他线程调用了t.casNext(null, s) 方法,当前线程帮助其将tn节点设置为尾节点 ,并重新开始 if (tn != null) { advanceTail(t, tn); continue; } // 如果s 不是尾节点 if (s != t) { QNode sn = s.next; // 下面分为两种情况 // 1. sn == s ,则s节点已经被设置为头节点 ,直接返回 // 2. pred.casNext(s, sn) 设置成功,也直接返回 ,如果pred.casNext(s, sn) == false , // 则只有一种情况,pred节点被设置为头节点 if (sn == s || pred.casNext(s, sn)) return; } // 进入下面代码分两种情况 , // 1. s 是尾节点 // 2. s 不是尾节点 ,pred 变为了头节点 // 对于上面两种情况是不能立即删除s节点的 QNode dp = cleanMe; // 如果cleanMe 为空,则将pred保存起来即可,留着下次删除 if (dp != null) { // d 节点是要被删除的节点 QNode d = dp.next; QNode dn; // 想了很久,一直不明白什么情况下会d == null ? 如果小伙伴知道,给我博客下方留言,我来更新博客 if (d == null || // 如果dp 节点的 next 节点被设置为头节点,dp 节点已经从队列中移除,dp.next = dp ,则d == dp d == dp || // 在并发情况下 其中一下线程执行了下面的代码 dp.casNext(d, dn) ,此时 dp.next 节点不是取消节点了 !d.isCancelled() || ( // 如果d 节点不是尾节点,则d节点可以删除了 d != t && // 如果dn = d.next 节点,进一步保证d 节点不是尾节点 (dn = d.next) != null && // d 节点没有从队列中移除 dn != d && // 将dp 节点的next节点指向 d节点的next 节点,d 节点从队列中移除 dp.casNext(d, dn))) // 如果上面出现任何一种情况 ,说明 d 节点已经从队列中移除了,就不需要保存dp 节点到cleanMe中 casCleanMe(dp, null); // 如果dp != pred ,则此次循环是帮之前的pred清除next节点,而本次需要清除的节点还没有处理,再次循环 if (dp == pred) return; // 保证要清除节点的前驱节点,以便下次清除 } else if (casCleanMe(null, pred)) return; } }
可能有小伙伴不懂,为什么要清除的节点是最后一个节点时,不能直接清除 。而需要保存前驱节点,等下次有节点要删除时,再来帮助清除上一次需要清除的节点 。
我们在回头来看transfer()方法 。
Object transfer(Object e, boolean timed, long nanos) { QNode s = null; // constructed/reused as needed boolean isData = (e != null); for (;;) { QNode t = tail; QNode h = head; if (t == null || h == null) // saw uninitialized value continue; // spin if (h == t || t.isData == isData) { // empty or same-mode ... 代码块1 } else { // complementary-mode ... 代码块2 } }
假设如果s == t 时是可以被删除的,线程1 调用put()方法,线程2调用take()方法同时进入了transfer()方法,先假设s.isData = true ,此时线程1 判断 (h == t || t.isData == isData) ,发现t.isData == isData = true ,进入代码块1, 若此时s节点被删除,线程2调用(h == t || t.isData == isData) ,发现 h == t ,此时线程2的take()方法也会进入代码块 1等待,显然逻辑不正确,如果删除s节点,在多线程情况下会存在并发问题,因此当s节点是尾节点时,在clean()方法中不能立即删除,只能将需要删除的节点的前驱节点保存到cleanMe中。
我相信TransferQueue原理你已经清楚了,还是提供一下建议,看源码一定要去看每一行代码,当时作者写这行代码的意图是什么,如果只看一个大概,看再多源码也不能从源码中理解精髓。
从网上找了一张图片,我觉得还是画得很好的,可供参考。
TransferStack
非公平模式TransferStack
我们之前分析TransferQueue源码时,发现一个规率,先到先得,每次消费数据的take()和poll()方法时,总是从队头的next节点开始取数据,并唤醒对方线程,那么时间来得越早take()或poll()的线程,唤醒得越快,当然put和offer也一样,我们称为公平模式,下面我们来看非公平模式 。
static final class TransferStack extends Transferer { // 消费者模式 static final int REQUEST = 0; // 产生者模式 static final int DATA = 1; static final int FULFILLING = 2; static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; } static final class SNode { volatile SNode next; // next node in stack volatile SNode match; // the node matched to this volatile Thread waiter; // to control park/unpark Object item; // data; or null for REQUESTs int mode; SNode(Object item) { this.item = item; } boolean casHead(SNode cmp, SNode val) { return cmp == next && UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); } boolean tryMatch(SNode s) { if (match == null && UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) { Thread w = waiter; if (w != null) { // 如果w 不为空,则唤醒w线程 waiter = null; LockSupport.unpark(w); } return true; } return match == s; } void tryCancel() { UNSAFE.compareAndSwapObject(this, matchOffset, null, this); } boolean isCancelled() { return match == this; } private static final sun.misc.Unsafe UNSAFE; private static final long matchOffset; private static final long nextOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class k = SNode.class; matchOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("match")); nextOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("next")); } catch (Exception e) { throw new Error(e); } } } volatile SNode head; boolean casHead(SNode h, SNode nh) { return h == head && UNSAFE.compareAndSwapObject(this, headOffset, h, nh); } static SNode snode(SNode s, Object e, SNode next, int mode) { if (s == null) s = new SNode(e); s.mode = mode; s.next = next; return s; }
上面的代码只是一些数据准备 ,接下来,我们看transfer()方法的内部实现,再来推出上面的这些属性和方法的用途。
Object transfer(Object e, boolean timed, long nanos) { SNode s = null; // e == null 表明是take()或poll()方法调用,而e !=null ,表明是put()或offer()方法调用 ,因此 // REQUEST 模式表示消费者获取数据,DATA 模式表示生产者生产数据 int mode = (e == null) ? REQUEST : DATA; for (;;) { SNode h = head; // 如果头节点为空或当前模式== 头节点模式,如线程1 调用了put()方法,此时头节点的模式为DATA // 此时另外一个线程再调用put()方法,而此时头节点的模式为DATA,则当前线程模式依然是 DATA,则当前线程进入下面代码块 // 进行等待 if (h == null || h.mode == mode) { // offer(E e) 或poll()方法调用,则不等待 if (timed && nanos <= 0) { // can't wait // 如果头节点不为空,但头节点已经被中断或超时,则将头节点的next节点设置为头节点 if (h != null && h.isCancelled()) casHead(h, h.next); // pop cancelled node else // 如果队列中取消的节点都被移除了,则当前线程返回空即可 return null; // 将新建的s节点作为头节点,h.next -> s } else if (casHead(h, s = snode(s, e, h, mode))) { // 当前s节点所在线程进入阻塞 SNode m = awaitFulfill(s, timed, nanos); // 如果等待超时或线程被中断 ,则清除s节点 if (m == s) { // wait was cancelled clean(s); return null; } // 如果头节点不为空,并且头节点指向自己,则将当前头节点更换为s.next节点 // 其实这个操作本来由和自己交换数据的线程来做的,这里也是出于好心帮助它而已 if ((h = head) != null && h.next == s) casHead(h, s.next); // 如果当前线程调用的是 take(),poll()方法,则返回m.item // 如果是调用put() 或 offer()方法,则返回线程本身传入的数据 e ,如果不返回, // 在put 或 offer方法中将抛出InterruptedException异常 return (mode == REQUEST) ? m.item : s.item; } // 如果队头不为空 ,且下面两种情况 会进入else if (!isFulfilling(h.mode)) 代码块 // 1. 如果队头模式是REQUEST,当前线程调用了put()或offer()方法,当前线程请求模式为DATA // 2. 如果队头模式是DATA,当前线程调用了take()或offer()方法,当前线程的请求模式为REQUEST // 上面两种情况可以与队头交换数据 } else if (!isFulfilling(h.mode)) { // 如果队头节点被中断或超时,则将h.next置为队头节点 if (h.isCancelled()) casHead(h, h.next); // 将当前节点设置为队头节点,且模式为 FULFILLING|mode // 如果当前e == null , mode = 0 ,则FULFILLING|mode = 2 | 0 = 2 // 如果当前e != null , mode = 1 ,则 FULFILLING|mode = 2 | 1 = 3 // 之前的队头节点为当前节点s的next节点 ,大家需要注意 ,同一时刻,只可能有一个线程进入下面的 // else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) 代码块 // 假如 线程1,线程2 两个线程同时进入代码else if条件判断,分别构建A ,B 两个节点都指向头节点,同时进行CAS操作 // 将 A,B 两个节点设置为头节点,但只可能有一个成功,假如A节点被设置为头节点成功,B节点设置头节点失败 // A 节点所在线程1将进入else if 代码块,而B 节点所在线程2,则再次重新进入循环,但重新循环时发现此时头节点的模式为 2 或 3 // !isFulfilling(h.mode) == false ,则会进入else 代码块,帮助当前线程1进行数据交换和线程唤醒 // 再多线程原理也是一样,因此在同一时间,只有一个线程会进入下面的else if 代码块 else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { for (;;) { // 什么情况下会再现m为的情况呢? 纵观代码,当线程第一次 for 循环时,m 肯定不为空 // 刚刚都设置了s.next = h ,第一次进入时,m 肯定等于之前的头节点。 // 只可能是m.tryMatch(s)调用返回false, 在tryMatch()方法中,只要m.match = s ,不管是否是当前线程CAS操作成功还是失败,都会返回true // 因此,只可能m.match != s 时,才会出现m.tryMatch(s) = false的情况,不知道大家有没有忽略了,如果m节点被中断或超时, // m将调用tryCancel()方法,将m.match = m ,出现这种情况m.tryMatch(s)肯定会返回false // 如果m.在调用tryMatch方法前,m节点被中断或超时,此时会调用s.casNext(m,mn),如果mn为空, // 则会走下面代码块 SNode m = s.next; if (m == null) { casHead(s, null); s = null; break; } SNode mn = m.next; // CAS操作将m.match 指向s,如果CAS操作成功,则返回true,即使CAS操作失败,但m.match == s ,也会返回true if (m.tryMatch(s)) { // 将当前头节点由s 指向 s.next.next , 将s 和s.next 将被移除掉 casHead(s, mn); // 如果是调用了take()或poll()方法,则返回m.item // 如果是put()或offer()方法,返回自己传入的数据 return (mode == REQUEST) ? m.item : s.item; } else // 如果m 中断或超时,将s的next指向s.next.next s.casNext(m, mn); } } } else { // 如果当前线程进入时,当前h.mode = 2或3,则当前线程会帮助头节点进行match匹配及线程唤醒 // 如果不帮它,当前线程将干等着,不如帮其他线程完成数据交换 SNode m = h.next; // 如果h.next 节点被中断或超时,会出现m == null 的情况 if (m == null) // 如果m 为空,表明链表中没有其他元素,则将头节点置空 casHead(h, null); else { SNode mn = m.next; // 设置 m.match = s if (m.tryMatch(h)) // 将mn设置为头节点 casHead(h, mn); else // 如果m 中断或超时,将h.next 指向mn h.casNext(m, mn); } } } }
接下来,我们进入awaitFulfill()方法,看它帮我做了哪些事情 。
SNode awaitFulfill(SNode s, boolean timed, long nanos) { // 记录当前时间的纳秒值,如果System.nanoTime() - lastTime < 0 ,还没有其他线程和当前线程交换数据 // 则当前节点将从队列中移除 long lastTime = timed ? System.nanoTime() : 0; Thread w = Thread.currentThread(); SNode h = head; // 如果头节点不为空且不为s ,且 h.mode 是DATA或REQUEST 模式 ,则spins == 0 // 那上面这段代码的意思是? 假如线程1,线程2,线程3 同时调用put方法,添加的结点分别是A ,B ,C // 此时线程2对应的B节点发现头节点是C,B 节点的spins将为0,如果此时有其他线程调用take()方法 // 则也只可能与线程3的C节点交换数据,而不会与B节点交换数据,与其做无用的等待,不如park() 当前线程 // 如果当前节点是头节点或头节点的模式为 2 或3 , FULFILLING | REQUEST = = 2 | 0 = 2 ,FULFILLING | DATA = 3 , // 我们称之为互补模式 // 1.如果当前节点是头节点,如果当前节点模式为DATA,则可能很快有其他线程调用take()方法来获取数据,因此应该spins 大于0 // 2.如果当前头节点的模式为2 或 3,则已经有线程来和队列中的节点交换数据,避免无用的线程开销,所以应该 spins 大于0 // 如果需要等待,spins 大于 0 ,大于0的逻辑如下 , // 先看timed = true 的情况,timed = true ,则当前调用的方法为offer()方法或poll()方法 // 1. 如果CPU数量 < 2 ,则 spins = 0 // 2. 如果CPU数量 >= 2 ,则spins = 32 // 如果调用的是put() 或take()方法 ,timed = false // 3.如果CPU数量 < 2 ,则spins = 0 // 4.如果CPU数量 >= 2 ,则spins = 16 * 32 = 512 // 我相信spins的由来你已经知道了,如果确切知道不会有线程来和当前线程交换数据,则spins == 0 ,不需要做无用的自旋 int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0); for (;;) { if (w.isInterrupted()) // 如果线程被中断,则将s.match -> s s.tryCancel(); // s.match !=null ,则返回,可能是s节点被中断或超时,也可能是与其他线程交换match线程 SNode m = s.match; if (m != null) return m; if (timed) { // 如果超时,则取消s节点 long now = System.nanoTime(); nanos -= now - lastTime; lastTime = now; if (nanos <= 0) { s.tryCancel(); continue; } } // 如果spins > 0 ,则进行自旋等待 if (spins > 0) // 如果s == 头节点,或 头节点为空,或s的模式为DATA ,则spins = spins -1 ,否则 spins = 0 spins = shouldSpin(s) ? (spins-1) : 0; // 如果s的线程为空,则设置s.waiter为当前线程 else if (s.waiter == null) s.waiter = w; // 如果没有设置时间限制,则park(this),如果设置了时间限制,则parkNanos(this,nanos) else if (!timed) LockSupport.park(this); else if (nanos > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanos); } }
如果线程被中断或超时,需要将s节点移除,接下来看clean方法的实现。
void clean(SNode s) { // 清空item和waiter s.item = null; // forget item s.waiter = null; // forget thread SNode past = s.next; // 如果s.next 是取消节点,则past = s.next.next节点 if (past != null && past.isCancelled()) past = past.next; SNode p; // 清除掉head节点之后连续的isCancelled节点,如下图中的A,B,C节点 while ((p = head) != null && p != past && p.isCancelled()) casHead(p, p.next); // 清除掉从head ~ s.next 节点之间所有isCancelled的节点,因为 S 节点肯定位于head ~ S.next节点之间 // 所以 当p == past时,S 节点肯定被清除了 ,在这里用到了CAS设置p.next节点,目的就是保证并发操作的正确性 while (p != null && p != past) { SNode n = p.next; if (n != null && n.isCancelled()) p.casNext(n, n.next); else p = n; } }
经过上面的分析,我相信大部分人已经基本已经理解了TransferStack的实现原理 ,我们最后来举个例子,加深大家的理解 。假如红色节点为take()或offer() 取数据线程构建的节点,绿色节点为put()或offer() 向队列中加数据构建的节点 。
- 线程1调用put()方法进入transfer()方法,加粗代码为走的代码
Object transfer(Object e, boolean timed, long nanos) { SNode s = null; int mode = (e == null) ? REQUEST : DATA; for (;;) { SNode h = head; if (h == null || h.mode == mode) { if (timed && nanos <= 0) { if (h != null && h.isCancelled()) casHead(h, h.next); else return null; } else if (casHead(h, s = snode(s, e, h, mode))) { SNode m = awaitFulfill(s, timed, nanos); if (m == s) { clean(s); return null; } if ((h = head) != null && h.next == s) casHead(h, s.next); return (mode == REQUEST) ? m.item : s.item; } } else if (!isFulfilling(h.mode)) { if (h.isCancelled()) casHead(h, h.next); else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { for (;;) { // loop until matched or waiters disappear SNode m = s.next; if (m == null) { casHead(s, null); s = null; break; } SNode mn = m.next; if (m.tryMatch(s)) { casHead(s, mn); return (mode == REQUEST) ? m.item : s.item; } else s.casNext(m, mn); } } } else { SNode m = h.next; if (m == null) casHead(h, null); else { SNode mn = m.next; if (m.tryMatch(h)) casHead(h, mn); else h.casNext(m, mn); } } } }
- 此时有线程2,线程3 两个线程同时调用了take()方法,两个线程会同时走下面加粗代码 。
Object transfer(Object e, boolean timed, long nanos) { SNode s = null; int mode = (e == null) ? REQUEST : DATA; for (;;) { SNode h = head; if (h == null || h.mode == mode) { if (timed && nanos <= 0) { if (h != null && h.isCancelled()) casHead(h, h.next); else return null; } else if (casHead(h, s = snode(s, e, h, mode))) { SNode m = awaitFulfill(s, timed, nanos); if (m == s) { clean(s); return null; } if ((h = head) != null && h.next == s) casHead(h, s.next); return (mode == REQUEST) ? m.item : s.item; } } else if (!isFulfilling(h.mode)) { if (h.isCancelled()) casHead(h, h.next); else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { for (;;) { // loop until matched or waiters disappear SNode m = s.next; if (m == null) { casHead(s, null); s = null; break; } SNode mn = m.next; if (m.tryMatch(s)) { casHead(s, mn); return (mode == REQUEST) ? m.item : s.item; } else s.casNext(m, mn); } } } else { SNode m = h.next; if (m == null) casHead(h, null); else { SNode mn = m.next; if (m.tryMatch(h)) casHead(h, mn); else h.casNext(m, mn); } } } }
- 假如线程2 ,线程3 创建两个新节点分别为B,C 。假如B节点CAS 设置头节点成功。C 节点设置头节点失败。
此时线程2 会走下面加粗代码
Object transfer(Object e, boolean timed, long nanos) { SNode s = null; int mode = (e == null) ? REQUEST : DATA; for (;;) { SNode h = head; if (h == null || h.mode == mode) { if (timed && nanos <= 0) { if (h != null && h.isCancelled()) casHead(h, h.next); else return null; } else if (casHead(h, s = snode(s, e, h, mode))) { SNode m = awaitFulfill(s, timed, nanos); if (m == s) { clean(s); return null; } if ((h = head) != null && h.next == s) casHead(h, s.next); return (mode == REQUEST) ? m.item : s.item; } } else if (!isFulfilling(h.mode)) { if (h.isCancelled()) casHead(h, h.next); else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { for (;;) { SNode m = s.next; if (m == null) { casHead(s, null); s = null; break; } SNode mn = m.next; if (m.tryMatch(s)) { casHead(s, mn); return (mode == REQUEST) ? m.item : s.item; } else s.casNext(m, mn); } } } else { SNode m = h.next; if (m == null) casHead(h, null); else { SNode mn = m.next; if (m.tryMatch(h)) casHead(h, mn); else h.casNext(m, mn); } } } }
- 线程3因为抢锁失败,只能继续循环,而此时头节点的模式为复合模式,因此!isFulfilling(h.mode) = false ,则会走下面代码 。
Object transfer(Object e, boolean timed, long nanos) { SNode s = null; int mode = (e == null) ? REQUEST : DATA; for (;;) { SNode h = head; if (h == null || h.mode == mode) { if (timed && nanos <= 0) { if (h != null && h.isCancelled()) casHead(h, h.next); else return null; } else if (casHead(h, s = snode(s, e, h, mode))) { SNode m = awaitFulfill(s, timed, nanos); if (m == s) { clean(s); return null; } if ((h = head) != null && h.next == s) casHead(h, s.next); return (mode == REQUEST) ? m.item : s.item; } } else if (!isFulfilling(h.mode)) { if (h.isCancelled()) casHead(h, h.next); else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { for (;;) { // loop until matched or waiters disappear SNode m = s.next; if (m == null) { casHead(s, null); s = null; break; } SNode mn = m.next; if (m.tryMatch(s)) { casHead(s, mn); return (mode == REQUEST) ? m.item : s.item; } else s.casNext(m, mn); } } } else { SNode m = h.next; if (m == null) casHead(h, null); else { SNode mn = m.next; if (m.tryMatch(h)) casHead(h, mn); else h.casNext(m, mn); } } } }
此时线程3干等着也是干等着,不如帮线程2去完成数据交换与线程唤醒 ,因此线程3此时以一个帮助者的心态去执行代码 。
若此时队列中没有其他可交换的节点,在线程3帮助线程完成数据交换后,线程3将进入等待。
C 变为新的头节点。
如果此时有线程4调用take()方法来获取数据,线程4构建的节点为D 。
此时有线程6 调用put方法从队列中加入数据。线程6构建的节点为E
此时线程6会走线程2的逻辑,与next节点交换数据。
我相信经过这个例子,你对TransferStack肯定有了更深的理解。
到这里我相信你对ArrayBlockingQueue&LinkedBlockingQueue&DelayQueue&SynchronousQueue&PriorityBlockingQueue源码肯定有了不同层次的理解,建议读者在看源码时一定要去分析作者写每一行代码的意图,这样你才能真正的得到源码的精髓,可能我讲得也有遗漏或表述有问题,大家看到了,给我留言。在这里先行谢了。
这里也就告一段落,下一篇,我们来分析线程池源码 。