1.ArrayBlockingQueue
·属性
/** The queued items */ final Object[] items; /** items index for next take, poll, peek or remove */ int takeIndex; /** items index for next put, offer, or add */ int putIndex; /** Number of elements in the queue */ int count; /** Main lock guarding all access */ final ReentrantLock lock; /** Condition for waiting takes */ private final Condition notEmpty; /** Condition for waiting puts */ private final Condition notFull; 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(); }
可以看出来该类是使用ReentrantLock和两个条件队列来保证多线程安全的。并且只有一把锁,也就意味着同一时刻只能有一个线程进来操作。
结构如下:
·put()
相当于生产者
public void put(E e) throws InterruptedException { checkNotNull(e); //插入的元素不能为null final ReentrantLock lock = this.lock; lock.lockInterruptibly();//先获取可中断锁 try { while (count == items.length) //如果数组满了,那么不能进行放的操作,去条件队列等待 notFull.await(); enqueue(e); //放到数组中去 } 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) //如果放入该元素后等于数组的容量,那么让put下标指向0,可以看出这里是一个循环数组 putIndex = 0; count++; //数量+1 notEmpty.signal();//唤醒因为没有元素而阻塞的消费者线程(take线程) }
·take()
相当于消费者
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); //拿锁 try { while (count == 0) //如果数组中没有元素,那么不能拿,去条件队列中等待 notEmpty.await(); return dequeue(); //否则返回拿到的值 } finally { lock.unlock(); } } private E dequeue() { // assert lock.getHoldCount() == 1; // assert items[takeIndex] != null; final Object[] items = this.items; //拿到数组 @SuppressWarnings("unchecked") E x = (E) items[takeIndex];//拿到对应下标的值 items[takeIndex] = null; //已经被拿走了,这个坑位就不再需要保存当前值了 if (++takeIndex == items.length) //如果已经拿到最后一个了,那么让takeIndex回到数组开始位置 takeIndex = 0; count--; //数量-1 //...忽略即可 notFull.signal(); //唤醒因为队列满了而不能放的生产者线程(因为我这里已经消费了,所以你可以生产了) return x; //返回拿到的值 }
·remove()
public boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); //上锁 try { if (count > 0) { //如果数组中没有元素,那还移除什么东西?直接返回false final int putIndex = this.putIndex; int i = takeIndex; do { if (o.equals(items[i])) { //通过equals来判断是否包含,如果有则移除 removeAt(i); //这个方法就不看了,我们可以想想在数组中删除一个元素是如何删除的?如果是最后一个元素那么就不用移动直接删除即可,否则必须要移动,逻辑比较简单,我就不赘述了 return true; } if (++i == items.length)//如果到达数组尾部,那么将i从数组开始继续找(因为这是个循环数组,可能出现takeIndex在putIndex的后面) i = 0; } while (i != putIndex); //直到追上putIndex为止 } return false; } finally { lock.unlock(); } }
可以看到基于数组实现的BlockIngQueue是由一把大锁实现的,生产者和消费者只能有一个进行,那么如果生产者生产了两个,这个时候生产者没有释放锁,消费者还是要等着,这样效率是否有点低下?
2.LinkedBlockingQueue
·属性
static class Node<E> { E item; //用于保存值 Node<E> next; //用于保存下一个节点 } private final int capacity; private final AtomicInteger count = new AtomicInteger(); transient Node<E> head; private transient Node<E> 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(); public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }
可以看到链表实现的阻塞队列和数组实现的阻塞队列的不同,在这里锁分为了put锁(生产者锁)和take锁(消费者锁),也就意味着消费者和生产者可以并发执行。并且在创建的时候就初始化了伪节点(head和tail),并且伪节点的是不存放数据的(item=null),
初始化的状态如下:
·put()
生产者线程
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException();//不能放null的值 int c = -1; // 临时变量c 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();//注意这个方法是返回原来的数量,也就是此时c=原来队列中有多少个元素 if (c + 1 < capacity) //如果当前线程插入之后还小于容量,那么唤醒我的兄弟(因为队列满了无法插入而去条件队列中阻塞的生产者线程) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) //如果放入元素之前队列是空的,那么唤醒因为队列为空而无法take()的消费者线程(因为此时我已经放入了一个元素了) signalNotEmpty(); } private void enqueue(Node<E> node) { last = last.next = node; //可以看出来是尾插法 }
·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(); //返回count之前的值 if (c > 1) //如果之前还有不止一个元素,那么唤醒我的兄弟起来消费(因为队列为空而无法消费去等待队列阻塞的线程) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) //如果在拿元素之前的队列是满的,那么唤醒因为队列满而无法放入元素的生产者线程(因为此时我已经消费了一个元素了) signalNotFull(); return x; } private E dequeue() { //可以看到是头拿的 Node<E> h = head; //拿到头节点 Node<E> first = h.next; //拿到头节点的后一个节点 h.next = h; // help GC 将伪节点的next设置为自己 head = first;//更换头节点 E x = first.item;//需要返回的值 first.item = null;// return x; }
·remove()
public boolean remove(Object o) { if (o == null) return false; fullyLock(); //拿到两把锁 try { for (Node<E> trail = head, p = trail.next; p != null; //单链表的移除操作,没什么好讲的了(唯一需要注意的点是:如果在移除之前队列是满了,那么肯定最后会有个唤醒因为队列满而不能put()的生产者线程,也即 notFull.signal()操作) trail = p, p = p.next) { if (o.equals(p.item)) { unlink(p, trail); return true; } } return false; } finally { fullyUnlock(); } }
·contains
查询队列中是否包含某个元素。可想而知,如果我在这里查找,而生产者在生产,消费者在消费,这明显不符合常理,所以要执行查找方法就必须拿到两把锁才能执行。否则没有意义。
public boolean contains(Object o) { if (o == null) return false; fullyLock(); //拿到两把锁 try { //可以看出时间复杂度是O(n) for (Node<E> p = head.next; p != null; p = p.next) //从第二个节点开始找(伪节点不存放数据) if (o.equals(p.item)) return true; return false; } finally { fullyUnlock(); } } void fullyUnlock() { //拿到生产者和消费者两把锁 takeLock.unlock(); putLock.unlock(); }
思考:
1.如果初始节点只有1个伪节点,那么生产者和消费者同时操作的时候是否会有冲突?
2.生产者和消费者同时操作链表是否存在问题?
·1.不会,因为存在count时,put和take操作在最开始的时候都会判断count值,如果take线程先判断count==0,那么就会去睡眠,然后put线程放了一个元素,并且在最后会判断if(c==0)那么唤醒take线程,所以不会出现问题。
·2.不存在问题,因为put是尾插,take是头拿。所以两个线程互不影响。
3.LinkedBlockingDeque
有单向链表的LinkedBlockingQueue,那么肯定有双向链表的。
·属性
static final class Node<E> { //可以看出是由双向链表组成的队列 E item; Node<E> prev; Node<E> next; } transient Node<E> first; transient Node<E> last; private transient int count; private final int capacity; final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); private final Condition notFull = lock.newCondition(); //构造方法 public LinkedBlockingDeque(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; }
可以看到该类只有一把锁,也就意味着,只要拿到了这把锁,那么就是接下来的操作就是单线程的了,也就没有并发,所以性能肯定是没有LinkedBlockingQueue好的。接下来看一些源码分析
·putFirst()
从队列头部放入元素
public void putFirst(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); //不允许放null的值 Node<E> node = new Node<E>(e); //可以看到这里没有伪节点,每个节点都是动态创建的 final ReentrantLock lock = this.lock; lock.lock(); //拿到锁 try { while (!linkFirst(node)) //linkFirst()是真正插入节点的方法,如果返回false(插入失败),那么意味着队列满了,则需要去条件队列中等待 notFull.await(); } finally { lock.unlock(); } } //linkFirst() private boolean linkFirst(Node<E> node) { if (count >= capacity) //如果队列满了,返回false return false; Node<E> f = first; //拿到头节点 node.next = f;//将头节点设置为要插入节点的next(也即头插法) first = node;//修改头节点 if (last == null) //如果是第一个节点,那么将last也指向该节点 last = node; else f.prev = node;//修改原来头节点的前驱节点 ++count; //数量+1 notEmpty.signal(); //唤醒因为队列为null无法消费而去阻塞的消费者线程 return true; }
·putLast()
从队列尾部插入元素
逻辑较为简单,当拿到锁以后就变为了操作双向链表了,在双向链表的尾部插入一个节点,这个操作很简单就不再赘述了
public void putLast(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); Node<E> node = new Node<E>(e); final ReentrantLock lock = this.lock; lock.lock(); try { while (!linkLast(node)) notFull.await(); } finally { lock.unlock(); } } // private boolean linkLast(Node<E> node) { // assert lock.isHeldByCurrentThread(); if (count >= capacity) return false; Node<E> l = last; node.prev = l; last = node; if (first == null) first = node; else l.next = node; ++count; notEmpty.signal(); return true; }
·takeFirst()
从队列头部拿元素,上锁后变为双向链表中从头部移除一个节点,不再赘述。
public E takeFirst() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lock(); //上锁 try { E x; while ( (x = unlinkFirst()) == null) notEmpty.await(); return x; } finally { lock.unlock(); } } // private E unlinkFirst() { Node<E> f = first; if (f == null) return null; Node<E> n = f.next; E item = f.item; f.item = null; f.next = f; // help GC first = n; if (n == null) last = null; else n.prev = null; --count; notFull.signal(); return item; }
从Node节点的数据结构可以看出,只有一把锁,所以只要拿到锁就变成单线程操作了,就是对双向链表进行增删改查了,逻辑比较简单,就不再赘述了。
4.LinkedTransferQueue
基于链接节点的无界的TransferQueue。 这个队列相对于任何给定的生产者订购元素FIFO(先进先出)。 队列的头部是那些已经排队的元素是一些生产者的最长时间。 队列的尾部是那些已经在队列上的元素是一些生产者的最短时间。
·示例
new Thread(() -> { try { log.debug("start--"); Object take = linkedTransferQueue.take(); //如果队列中没有元素,那么会阻塞 log.debug("{}", take); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); Thread.sleep(5000); linkedTransferQueue.transfer(1);//放入一个元素并且唤醒阻塞线程 } //运行结果 2024-01-25 10:45:25 [Thread-0] - start-- //可以看出这里被阻塞了 2024-01-25 10:45:30 [Thread-0] - 1 //拿到了数据 从这个简单的示例中可以看出该类实现了生产者消费者模型,也即数据交换模型。
·属性
public LinkedTransferQueue() { //空的构造函数 } private static final int FRONT_SPINS = 1 << 7; =2^7 private static final int CHAINED_SPINS = FRONT_SPINS >>> 1; 2^6 static final int SWEEP_THRESHOLD = 32; static final class Node { /* 判断是生产数据的节点还是消费数据的节点, false表示消费 true表示生产 */ final boolean isData; /* 数据保存的地方 对于生产节点,item保存数据 对于消费节点,item保存从生产节点中获得的数据 */ volatile Object item; volatile Node next; //下一个节点 volatile Thread waiter; //要被阻塞的线程对象保存在这里,留给唤醒我的线程使用 final boolean isMatched() { //判断是否匹配 Object x = item; //拿到节点的item值 /* 如果是生产节点,那么isData=true,初始item为生产的值,当被匹配成功后,item为消费者的null值(数据交换) 那么此时x==null为true isData为true,所以为true,那么代表着生产者匹配成功了 对于消费节点,isData=false,匹配成功后,那么item为生产者的值(数据交换),那么此时x!=null也即false, isData=false,即false=flase,代表消费者匹配成功。 那么第一个x==this代表着数据指向自己(这是什么意思?后续再继续分析) */ return (x == this) || ((x == null) == isData); } transient volatile Node head; private transient volatile Node tail; //对被取消的节点进行清理失败的票数计数 private transient volatile int sweepVotes; private static final int NOW = 0; // for untimed poll, tryTransfer private static final int ASYNC = 1; // for offer, put, add //异步非阻塞操作 private static final int SYNC = 2; // for transfer, take //同步阻塞操作 private static final int TIMED = 3; // for timed poll, tryTransfer }
可以看到该类竟然没有锁,并且也没有伪节点,而是一个单链表,那么意味着生产者和消费者作用在同一条链表上,那么看看到底是如何工作的。
·transfer()
//生产者放入数据 public void transfer(E e) throws InterruptedException { /* e代表要传递的数据 true代表当前有数据要传递 SYNC代表同步等待 0代表没有超时 说明这是一个生产者线程 */ if (xfer(e, true, SYNC, 0) != null) { Thread.interrupted(); // failure possible only due to interrupt throw new InterruptedException(); } } /* 注意:如果生产者和消费者匹配成功了,那么数据将会互换,也即item的值将会交换(这也是我们想要的结果), 1.所以下面在判断item!=null ==isData时,对于生产者而言,原来的item=数据,匹配成功后为null,而isData=true, 所以item!=null ==isData为(false == true即如果被匹配了,那么此判断为false,也即生产者被匹配,同理消费者也是如此) 2.当线程被删除后,如何标识自己要被删除了?只能用一个东西来标识自己,在该类中使用的是item指向自己来标识自己要被删除了,因为无论是生产者还是消费者都要和item打交道 */ //xfer() private E xfer(E e, boolean haveData, int how, long nanos) { if (haveData && (e == null)) throw new NullPointerException(); //如果为生产线程,那么数据不能为null Node s = null; //临时节点 // the node to append, if needed retry: //标记,Java中很少使用 for (;;) { // restart on append race for (Node h = head, p = h; p != null;) { // 从头节点开始找到并且匹配节点 boolean isData = p.isData; //拿到当前节点的类型(是生产者还是消费者) Object item = p.item; //拿到要交换的数据 /* item!=p代表自己没被取消 (item != null) == isData 生产者item如果没被匹配,那么就不为null,则为true,生产者的isData本来就为true,所以这个判断为true时就代表没被匹配,如果没被匹配就继续往下执行, */ if (item != p && (item != null) == isData) { // unmatched //如果是同一种类型的,那么结束循环 if (isData == haveData) // can't match break; /* p节点和当前节点的类型相反(一个是生产者一个是消费者),那么cas尝试匹配, 如果p是消费线程,那么则将p的item置为e(e是生产者的数据), 如果p为生产线程,那么此时e为null,则将p的item置为null,代表匹配成功了, 如果cas成功,那么需要将p节点断开,继续下面的操作 */ if (p.casItem(item, e)) { // match //q默认是匹配成功的节点p,并且q不能是头节点 for (Node q = p; q != h;) { Node n = q.next; //取q的后继节点// update by 2 unless singleton /* head == h 头节点没有变化 并且 如果q是最后一个节点,那么修改q为head 否则修改q的后继节点为头节点,因为是尾插, 所以越后面的节点越不容易被取消,下次匹配时也是先匹配头节点 */ if (head == h && casHead(h, n == null ? q : n)) { //断开next指针,将next指向自己 h.forgetNext(); break; } // advance and retry if ((h = head) == null || (q = h.next) == null || !q.isMatched()) break; // unless slack < 2 } //唤醒p节点的等待线程 LockSupport.unpark(p.waiter); //将值返回即可 return LinkedTransferQueue.<E>cast(item); } } //执行到这里,上面的p节点已经被匹配,去p的后继节点,如果p.next=p(p=n)的话,表明p已经被移除, 此时将p设置为新的头节点并且继续循环,否则继续尝试匹配p.next Node n = p.next; p = (p != n) ? n : (h = head); // Use head if p offlist } if (how != NOW) { // No matches available if (s == null) s = new Node(e, haveData); //初始化等待节点 Node pred = tryAppend(s, haveData);//将节点插入到链表中 if (pred == null)//插入链表失败,重试 continue retry; // lost race vs opposite mode if (how != ASYNC) //准备阻塞 return awaitMatch(s, pred, e, (how == TIMED), nanos); } return e; // not waiting } }
·tryAppend()
private Node tryAppend(Node s, boolean haveData) { //haveData代表线程的种类(是生产线程还是消费线程) for (Node t = tail, p = t;;) { // move p to last node and append Node n, u; // temps for reads of next & tail if (p == null && (p = head) == null) { if (casHead(null, s)) return s; // initialize } else if (p.cannotPrecede(haveData)) return null; // lost race vs opposite mode else if ((n = p.next) != null) // not last; keep traversing p = p != t && t != (u = tail) ? (t = u) : // stale tail (p != n) ? n : null; // restart if off list else if (!p.casNext(null, s)) p = p.next; // re-read on CAS failure else { if (p != t) { // update if slack now >= 2 while ((tail != t || !casTail(t, s)) && (t = tail) != null && (s = t.next) != null && // advance and retry (s = s.next) != null && s != t); } return p; } } }
该方法的总体逻辑如下:
·awaitMatch()
看下是如何阻塞线程的
//s是当前节点 pred是前一个节点 e是要交换的数据 private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) { final long deadline = timed ? System.nanoTime() + nanos : 0L; Thread w = Thread.currentThread();//获取当前执行线程 int spins = -1; // 自旋次数 ThreadLocalRandom randomYields = null; // bound if needed for (;;) { Object item = s.item; //当前节点的数据 if (item != e) { //说明当前节点已经匹配成功 // matched // assert item != s; //将item指向自己 s.forgetContents(); // avoid garbage return LinkedTransferQueue.<E>cast(item); } //如果线程被中断,返回原始值 //或者超时,则通过cas将当前节点的item设置为自己,注意这里的cas,如果在超时和被匹配中,如果先匹配成功,那么即使超时也不算 if ((w.isInterrupted() || (timed && nanos <= 0)) && s.casItem(e, s)) { // cancel unsplice(pred, s); //如果是,那么将s节点从链表中移除 return e; } //初始化自旋次数 if (spins < 0) { // establish spins at/near front if ((spins = spinsFor(pred, s.isData)) > 0) randomYields = ThreadLocalRandom.current(); } else if (spins > 0) { // spin 开始自旋 --spins; if (randomYields.nextInt(CHAINED_SPINS) == 0) //用来释放cpu资源的随机数,自旋优化 Thread.yield(); // occasionally yield } else if (s.waiter == null) { // 设置等待线程 s.waiter = w; // request unpark then recheck } else if (timed) { nanos = deadline - System.nanoTime(); if (nanos > 0L) LockSupport.parkNanos(this, nanos); } else { //阻塞 LockSupport.park(this); } } }
·unsplice()
final void unsplice(Node pred, Node s) { //s是当前节点,是需要被移除的节点,pred是前驱节点 s.forgetContents(); // 将s的item指向自己 //s不是头节点并且s是pred的后继节点 if (pred != null && pred != s && pred.next == s) { Node n = s.next; //获取s的后继节点 if (n == null || //后继节点为null ,意味着s是最后一个节点,则直接进入,否则意味着s不是最后一个节点 //或者 后继节点不等于s, 则CAS将前驱节点的next节点设置为s的next节点,这条语句就能将s节点从链表中移除了 //并且判断前驱节点是否被匹配。如果没有被匹配,那么直接返回 (n != s && pred.casNext(s, n) && pred.isMatched())) { for (;;) { // check if at, or could be, head Node h = head; //拿到最新的头节点 //如果前驱节点是头节点或者s是头节点或者此时链表为null,直接返回 if (h == pred || h == s || h == null) return; // at head or list empty if (!h.isMatched())//如果头节点没有被匹配,直接结束循环 break; Node hn = h.next; //拿到头节点的后续节点 if (hn == null) //链表中只有头节点,此时队列为null,直接返回 return; // now empty if (hn != h && casHead(h, hn)) h.forgetNext(); // advance head } if (pred.next != pred && s.next != s) { // recheck if offlist for (;;) { // sweep now if enough votes int v = sweepVotes; if (v < SWEEP_THRESHOLD) { if (casSweepVotes(v, v + 1)) break; } else if (casSweepVotes(v, 0)) { sweep(); break; } } } } } }
5.SynchronousQueue
·简介
A blocking queue其中每个插入操作必须等待另一个线程相应的删除操作,反之亦然。 同步队列没有任何内部容量,甚至没有一个容量。 你不能peek
在同步队列,因为一个元素,当您尝试删除它才存在; 您无法插入元素(使用任何方法),除非另有线程正在尝试删除它; 你不能迭代,因为没有什么可以迭代。 队列的头部是第一个排队的插入线程尝试添加到队列中的元素; 如果没有这样排队的线程,那么没有元素可用于删除,并且poll()
将返回null
。 为了其他Collection
方法(例如contains
)的目的, SynchronousQueue
充当空集合。 此队列不允许null
元素。
·简单示例:
SynchronousQueue<Object> synchronousQueue = new SynchronousQueue<>(); new Thread(() -> { Object ret = null; try { log.debug("生产者等待匹配中····"); synchronousQueue.put(1); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); Thread.sleep(3000); new Thread(() -> { Object ret = null; try { log.debug("消费者准备消费····"); ret = synchronousQueue.take(); log.debug("ret={}", ret); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); //结果: 2024-01-26 12:19:42 [Thread-0] - 生产者等待匹配中···· 2024-01-26 12:19:45 [Thread-1] - 消费者准备消费···· 2024-01-26 12:19:45 [Thread-0] - 匹配成功 2024-01-26 12:19:45 [Thread-1] - ret=1 //将上面2个线程互换得出的结果 2024-01-26 12:19:04 [Thread-0] - 消费者准备消费···· 2024-01-26 12:19:07 [Thread-1] - 生产者等待匹配中···· 2024-01-26 12:19:07 [Thread-1] - 匹配成功 2024-01-26 12:19:07 [Thread-0] - ret=1 可以看到线程必须匹配才能执行,如果生产者发现没有人等待消费,那么阻塞,同样消费者发现没有人生产,一样阻塞
·构造函数
public SynchronousQueue() { this(false); } public SynchronousQueue(boolean fair) { transferer = fair ? new TransferQueue<E>() : new TransferStack<E>(); }
可以看到该类的内部有两种实现,公平的和非公平的,默认是非公平的。
·公平:TransferQueue实现的
·非公平:TransferStack实现的
·put(E e)
将指定的元素添加到此队列,等待另一个线程接收它。并且如果没有消费者等待着消费,那么阻塞
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); //不允许放null值 if (transferer.transfer(e, false, 0) == null) { //调用transferer.transfer() Thread.interrupted(); throw new InterruptedException(); } }
·take()
检索并删除此队列的头,等待另一个线程插入它。并且如果没有生产者等待着被取走,那么阻塞
public E take() throws InterruptedException { E e = transferer.transfer(null, false, 0); //可以看到都是调用transferer.transfer()方法 if (e != null) return e; Thread.interrupted(); throw new InterruptedException(); }
可以看到不管是生产者还是消费者都是调用transferer.transfer(),不过对于生产者而言,其数据不为null,而对于消费者而言其数据为null(这很容易理解)。
·transfer()公平实现
先看看由TransferQueue实现的transfer的工作原理
·属性
static final class QNode { volatile QNode next; // next node in queue 下一个节点,可以看出是由单链表实现的 volatile Object item; // CAS'ed to or from null 数据 volatile Thread waiter; // to control park/unpark 等待线程 final boolean isData; //消费者还是生产者的标识 QNode(Object item, boolean isData) { this.item = item; this.isData = isData; } } transient volatile QNode head; transient volatile QNode tail; transient volatile QNode cleanMe; TransferQueue() { //可以看到初始化的时候会创建一个伪节点(不存放数据) QNode h = new QNode(null, false); // initialize to dummy node. head = h; tail = h; }
E transfer(E e, boolean timed, long nanos) { QNode s = null; // constructed/reused as needed boolean isData = (e != null);//e!null说明是生产者,即生产者的IsData=true,消费者为flase for (;;) { QNode t = tail; QNode h = head; //将头节点和尾节点保存在线程栈上,注意Doug Lea写代码的风格就是保存旧值然后操作旧值, //后续操作再用旧值和新值比较来判断是否产生了并发 if (t == null || h == null) // saw uninitialized value continue; // spin if (h == t || t.isData == isData) { // empty or same-mode QNode tn = t.next; if (t != tail) // inconsistent read continue; if (tn != null) { // lagging tail advanceTail(t, tn); continue; } if (timed && nanos <= 0) // can't wait return null; if (s == null) s = new QNode(e, isData); 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; } 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) ? (E)x : e; } else { // complementary-mode QNode m = h.next; // node to fulfill if (t != tail || m == null || h != head) continue; // inconsistent read Object x = m.item; if (isData == (x != null) || // m already fulfilled x == m || // m cancelled !m.casItem(x, e)) { // lost CAS advanceHead(h, m); // dequeue and retry continue; } advanceHead(h, m); // successfully fulfilled LockSupport.unpark(m.waiter); return (x != null) ? (E)x : e; } } }
·源码分析:
如果图片模糊可以查看该链接:
·transfer()非公平实现
由TransferStack实现的
·属性
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 //思考一下:为什么这里不需要使用使用volatile修饰? Object item; // data; or null for REQUESTs int mode; } volatile SNode head;
·transfer()
E transfer(E e, boolean timed, long nanos) { /* * Basic algorithm is to loop trying one of three actions: * * 1. If apparently empty or already containing nodes of same * mode, try to push node on stack and wait for a match, * returning it, or null if cancelled. * * 2. If apparently containing node of complementary mode, * try to push a fulfilling node on to stack, match * with corresponding waiting node, pop both from * stack, and return matched item. The matching or * unlinking might not actually be necessary because of * other threads performing action 3: * * 3. If top of stack already holds another fulfilling node, * help it out by doing its match and/or pop * operations, and then continue. The code for helping * is essentially the same as for fulfilling, except * that it doesn't return the item. */ SNode s = null; // constructed/reused as needed int mode = (e == null) ? REQUEST : DATA; for (;;) { SNode h = head; if (h == null || h.mode == mode) { // empty or same-mode if (timed && nanos <= 0) { // can't wait if (h != null && h.isCancelled()) casHead(h, h.next); // pop cancelled node else return null; } else if (casHead(h, s = snode(s, e, h, mode))) { SNode m = awaitFulfill(s, timed, nanos); if (m == s) { // wait was cancelled clean(s); return null; } if ((h = head) != null && h.next == s) casHead(h, s.next); // help s's fulfiller return (E) ((mode == REQUEST) ? m.item : s.item); } } else if (!isFulfilling(h.mode)) { // try to fulfill if (h.isCancelled()) // already cancelled casHead(h, h.next); // pop and retry else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) { for (;;) { // loop until matched or waiters disappear SNode m = s.next; // m is s's match if (m == null) { // all waiters are gone casHead(s, null); // pop fulfill node s = null; // use new node next time break; // restart main loop } SNode mn = m.next; if (m.tryMatch(s)) { casHead(s, mn); // pop both s and m return (E) ((mode == REQUEST) ? m.item : s.item); } else // lost match s.casNext(m, mn); // help unlink } } } else { // help a fulfiller SNode m = h.next; // m is h's match if (m == null) // waiter is gone casHead(h, null); // pop fulfilling node else { SNode mn = m.next; if (m.tryMatch(h)) // help match casHead(h, mn); // pop both h and m else // lost match h.casNext(m, mn); // help unlink } } } }
·源码分析
·小总结:
1.如果栈为空或者栈顶线程和当前线程是相同模式的节点,那么为当前线程生成一个新的节点插入到栈中等待匹配
2.如果栈顶线程和当前线程的模式相反,那么尝试将一个fulfilling节点压入栈中,然后对这个节点进行匹配,匹配成功将这个节点和被匹配成功的节点一起从栈中弹出,当然如果有别的线程此时进来那么会帮助弹出
3.如果栈顶出现了一个正在匹配的节点,那么尝试帮助它完成匹配并且出栈,然后开始自己的匹配
可以看出Stack和Queue的区别了,Stack是栈顶进栈顶匹配,而Queue是队尾进,队头匹配。
6.PriorityBlockingQueue
一个无界的blocking queue使用与PriorityQueue类相同的排序规则,并提供阻塞检索操作。 虽然这个队列在逻辑上是无界的,但由于资源耗尽,尝试的添加可能会失败(导致OutOfMemoryError
)。 这个类不允许null
元素。 根据natural ordering的优先级队列也不允许插入不可比较的对象(这样做在ClassCastException
)。
·简单示例
priorityBlockingQueue.put(2); priorityBlockingQueue.put(1); Object ret = priorityBlockingQueue.take(); System.out.println("ret = " + ret); //ret=1
·属性
//默认数组的容量 private static final int DEFAULT_INITIAL_CAPACITY = 11; //最大的容量,想想为什么要-8?(数组对象的头部需要保存数组的长度) private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //保存元素的数组 private transient Object[] queue; //队列中元素的个数 private transient int size; //比较器 private transient Comparator<? super E> comparator; //互斥锁 private final ReentrantLock lock; //条件等待,因为是无界的,所以只有空,没有满 private final Condition notEmpty; //等待数组分配时的自旋锁使用的变量 private transient volatile int allocationSpinLock; //构造 public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.lock = new ReentrantLock(); //锁 this.notEmpty = lock.newCondition(); //条件队列 this.comparator = comparator; //比较器 this.queue = new Object[initialCapacity]; //数组实现的队列 }
·put()
public void put(E e) { offer(e); // never need to block } //-put()是调用offer()来完成的 public boolean offer(E e) { if (e == null) throw new NullPointerException(); //不允许放入null值 final ReentrantLock lock = this.lock; lock.lock(); //上锁,以下操作变为单线程操作 int n, cap; Object[] array; //队列满了,进行扩容,while()保证直到扩容成功 while ((n = size) >= (cap = (array = queue).length)) tryGrow; try { Comparator<? super E> cmp = comparator;//拿到比较器 if (cmp == null) //如果比较为null,则通过Comparable来比较 siftUpComparable(n, e, array); else siftUpUsingComparator(n, e, array, cmp); size = n + 1; //将队列中元素+1 notEmpty.signal();//唤醒因为没有元素take()而阻塞的线程 } finally { lock.unlock(); //解锁 } return true; }
·tryGrow()
private void tryGrow(Object[] array, int oldCap) { lock.unlock(); // 释放锁,不能因为扩容让其它操作无法执行 Object[] newArray = null; //获取自旋锁并且将其cas为1 if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) { try { //如果原数组的容量小于64,那么新数组的容量为64+2,否则为64+32 int newCap = oldCap + ((oldCap < 64) ? (oldCap + 2) : // grow faster if small (oldCap >> 1)); //如果扩容后超过最大容量 if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow //将原来的数组容量+1,如果原来容量就是最大值,那么+1会符号溢出 int minCap = oldCap + 1; if (minCap < 0 || minCap > MAX_ARRAY_SIZE) throw new OutOfMemoryError(); //抛出OOM newCap = MAX_ARRAY_SIZE;//将新容量设置为默认最大值 } //新容量大于老容量并且数组没有被修改引用,那么就创建一个新的数组 if (newCap > oldCap && queue == array) newArray = new Object[newCap]; } finally { allocationSpinLock = 0;//释放自旋锁 } } if (newArray == null) // 如果其它线程正在创建新数组,那么释放CPU资源,没必要做无意义的自旋 Thread.yield(); lock.lock();//操作原数组时必须上锁 //在原数组未改变的情况下进行数组复制并且替换原数组 if (newArray != null && queue == array) { queue = newArray; System.arraycopy(array, 0, newArray, 0, oldCap); } }
·take()
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly();//拿到可中断的锁 E result; try { while ( (result = dequeue()) == null) //如果此时队列中没有元素,那么去条件队列中阻塞 notEmpty.await(); } finally { lock.unlock(); } return result; }
该类的实现比较简单,上了锁之后就是单线程操作,对于入队出队的操作就是堆的操作,这里就不再赘述了。