Queue,阻塞队列

LinkedBlockingQueue:

源码:

/** 阻塞队列的大小,默认为Integer.MAX_VALUE */ private final int capacity; /** 当前阻塞队列中的元素个数 */ private final AtomicInteger count = new AtomicInteger();

* 阻塞队列的头结点 transient Node<E> head; * 阻塞队列的尾节点 private transient Node<E> last; /** 获取并移除元素时使用的锁,如take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock();

特性:

1:它如果不指定容量,默认为Integer.MAX_VALUE(默认值时:如果存在添加速度大于删除速度时候,有可能会内存溢出),也就是无界队列。所以为了避免队列过大造成机器负载或者内存爆满的情况出现。

2:每个添加到LinkedBlockingQueue队列中的数据都将被封装成Node节点,添加的链表队列中,其中head和last分别指向队列的头结点和尾结点。

3:inkedBlockingQueue内部分别使用了takeLock 和 putLock 对并发进行控制,也就是说,添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量

入队方法

void put(E e)特点:

1:队列已满,阻塞等待。

2:队列未满,创建一个node节点放入队列中,如果放完以后队列还有剩余空间,继续唤醒下一个添加线程进行添加。如果放之前队列中没有元素,放完以后要唤醒消费线程进行消费。

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(); } // 把node放入队列中 enqueue(node); c = count.getAndIncrement(); // 再次判断队列是否有可用空间,如果有唤醒下一个线程进行添加操作 if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } // 如果队列中有一条数据,唤醒消费线程进行消费 if (c == 0) signalNotEmpty(); }

private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } } private void signalNotFull() { final ReentrantLock putLock = this.putLock; putLock.lock(); try { notFull.signal(); } finally { putLock.unlock(); } }

boolean offer(E e)特点:

当队列没有可用元素的时候,不同于put方法的阻塞等待,offer方法直接方法false。

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 { // 队列有可用空间,放入node节点,判断放入元素后是否还有可用空间, // 如果有,唤醒下一个添加线程进行添加操作。 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; }

boolean offer(E e, long timeout, TimeUnit unit)特点:

对offer方法进行了阻塞超时处理,使用了Condition的awaitNanos来进行超时等待,这里为什么要用while循环?因为awaitNanos方法是可中断的,为了防止在等待过程中线程被中断,这里使用while循环进行等待过程中中断的处理,继续等待剩下需等待的时间

出队方法

E take()特点:

1:队列为空,阻塞等待。

2:队列不为空,从队首获取并移除一个元素,如果消费后还有元素在队列中,继续唤醒下一个消费线程进行元素移除。如果放之前队列是满元素的情况,移除完后要唤醒生产线程进行添加元素。

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; }

E poll():

public E poll() { final AtomicInteger count = this.count; if (count.get() == 0) return null; E x = null; int c = -1; final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { if (count.get() > 0) { x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }

E poll(long timeout, TimeUnit unit);

remove():删除元素方法

public boolean remove(Object o) { if (o == null) return false; // 两个lock全部上锁 fullyLock(); try { // 从head开始遍历元素,直到最后一个元素 for (Node<E> trail = head, p = trail.next; p != null; trail = p, p = p.next) { // 如果找到相等的元素,调用unlink方法删除元素 if (o.equals(p.item)) { unlink(p, trail); return true; } } return false; } finally { // 两个lock全部解锁 fullyUnlock(); } }

---------------------------------------------------------------------------------------------------------------------------

ArrayBlockingQueue:

入队方法:

1:add: 内部实际上获取的offer方法,当Queue已经满了时,抛出一个IllegalStateException(“Queue full”)异常,不会阻塞。

2:offer:队列未满时,返回true;队列满时返回false。非阻塞立即返回。

//ArrayBlockingQueue#offer,队列未满时返回true,满时返回false public boolean offer(E e) {   checkNotNull(e);//检查入队元素是否为空   final ReentrantLock lock = this.lock;   lock.lock();//获得锁,线程安全   try {     if (count == items.length)//队列满时,不阻塞等待,直接返回false       return false;     else {       insert(e);//队列未满,直接插入       return true;     }   } finally {     lock.unlock();   } }

offer(e, time, unit):设定等待的时间,如果在指定时间内还不能往队列中插入数据则返回false,插入成功返回true。  

3:put:队列未满时,直接插入没有返回值;队列满时会阻塞等待,一直等到队列未满时再插入。

//ArrayBlockingQueue#put

public void put(E e) throws InterruptedException {

  checkNotNull(e); //同样检查插入元素是否为空

  final ReentrantLock lock = this.lock;

  lock.lockInterruptibly(); //这里并没有调用lock方法,而是调用了可被中断的lockInterruptibly,该方法可被线程中断返回,lock不能被中断返回。

  try {

    while (count == items.length)

      notFull.await(); //当队列满时,使非满等待队列休眠

    insert(e); //此时表示队列非满,故插入元素,同时在该方法里唤醒非空等待队列

  } finally {

    lock.unlock();

  }

}

出队方法:

1:remove():队列不为空时,返回队首值并移除;队列为空时抛出NoSuchElementException()异常

//AbstractQueue#remove,这也是一个模板方法,定义删除队列元素的算法骨架,队列中元素 时返回具体元素,元素为空时抛出异常,具体实现poll由子类实现,

public E remove() {

  E x = poll(); //poll方法由Queue接口定义

  if (x != null)

    return x;

  else

    throw new NoSuchElementException();

}

2:poll():队列不为空时返回队首值并移除;队列为空时返回null。非阻塞立即返回。

//ArrayBlockingQueue#poll,队列中有元素时返回元素,不为空时返回null public E poll() {   final ReentrantLock lock = this.lock;   lock.lock();   try {     return (count == 0) ? null : extract();   } finally {     lock.unlock();   } }

//ArrayBlockingQueue#extract private E extract() {   final Object[] items = this.items;   E x = this.<E>cast(items[takeIndex]); //移除队首元素   items[takeIndex] = null; //将队列数组中的第一个元素置为null,便于GC回收   takeIndex = inc(takeIndex);   --count;   notFull.signal();//唤醒非满等待队列线程   return x; }

poll(time, unit):设定等待的时间,如果在指定时间内队列还未孔则返回null,不为空则返回队首值 

3:take(e):队列不为空返回队首值并移除;当队列为空时会阻塞等待,一直等到队列不为空时再返回队首值。 

//ArrayBlockQueue#take public E take() throws InterruptedException {   final ReentrantLock lock = this.lock();   lock.lockInterrupted(); //这里并没有调用lock方法,而是调用了可被中断的lockInterruptibly,该方法可被线程中断返回,lock不能被中断返回。   try {     while (count == 0) //队列元素为空       notEmpty.await(); //非空等待队列休眠     return extract(); //此时表示队列非空,故删除元素,同时在里唤醒非满等待队列   } finally {     lock.unlock();   } }

小扩展:

public int size() {   final ReentrantLock lock = this.lock;   lock.lock();   try {     return count;   } finally {     lock.unlock();   } }

ArrayBlockingQueue队列的size方法,是直接返回的count变量,它不像ConcurrentLinkedQueue,ConcurrentLinkedQueue的size则是每次会遍历这个队列,故ArrayBlockingQueue的size方法比ConcurrentLinkedQueue的size方法效率高。

size()方法比isEmpty()耗时更长,原因是size() 是要遍历一遍集合的.

SynchronousQueue

1:是无界的,是一种无缓冲的等待队列,它的每个插入操作都要等待其他线程相应的移除操作,反过来也一样。SynchronousQueue 像是生产者和消费者的会合通道,它比较适合“切换”或“传递”这种场景:一个线程必须同步等待另外一个线程把相关信息/时间/任务传递给它;

2:SynchronousQueue 内部没有容量,所以不能通过peek方法获取头部元素;也不能单独插入元素,可以简单理解为它的插入和移除是“一对”对称的操作。为了兼容 Collection 的某些操作(例如contains),SynchronousQueue扮演了一个空集合的角色。

SynchronousQueue 的一个典型应用场景是在线程池中,Executors.newCachedThreadPool() 就使用了它,这个构造使线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

3:SynchronousQueue 为等待过程中的生产者或消费者线程提供可选的公平策略(默认非公平模式)。非公平模式通过栈(LIFO)实现,公平模式通过队列(FIFO)实现。使用的数据结构是双重队列(Dual queue)和双重栈(Dual stack),FIFO通常用于支持更高的吞吐量,LIFO则支持更高的线程局部存储(TLS)。

SynchronousQueue 的阻塞算法可以归结为以下几点:

1:使用了双重队列(Dual queue)和双重栈(Dual stack)存储数据,队列中的每个节点都可以是一个生产者或是消费者。

2:已取消节点引用指向自身,避免垃圾保留和内存损耗

3:通过自旋和 LockSupport 的 park/unpark 实现阻塞,在高争用环境下,自旋可以显著提高吞吐量。

SynchronousQueue 有三个内部类

1:Transferer:内部抽象类,只有一个transfer方法。SynchronousQueue的put和take被统一为一个方法(就是这个transfer方法),因为在双重队列/栈数据结构中,put和take操作是对称的,所以几乎所有代码都可以合并。

源码:

public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); if (transferer.transfer(e, false, 0) == null) { Thread.interrupted(); throw new InterruptedException(); } }

2:TransferStack:继承了内部抽象类 Transferer,实现了transfer方法,用于非公平模式下的队列操作,数据按照LIFO的顺序。内部通过单向链表 SNode 实现的双重栈。

源码:E transfer(E e, boolean timed, long nanos) { SNode s = null; // constructed/reused as needed //根据所传元素判断为生产or消费 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()) //head已经被匹配,修改head继续循环 casHead(h, h.next); // pop cancelled node else return null; } else if (casHead(h, s = snode(s, e, h, mode))) { //构建新的节点s,放到栈顶 //等待s节点被匹配,返回s.match节点m SNode m = awaitFulfill(s, timed, nanos); //s.match==s( 等待被取消) if (m == s) { // wait was cancelled clean(s); //清除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)) { //head节点还没有被匹配,尝试匹配 try to fulfill if (h.isCancelled()) // already cancelled //head已经被匹配,修改head继续循环 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 //cas成功后s的match节点就是s.next,即m 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)) {//尝试匹配,唤醒m节点的线程 casHead(s, mn); //弹出匹配成功的两个节点,替换head pop both s and m return (E) ((mode == REQUEST) ? m.item : s.item); } else // lost match s.casNext(m, mn); //匹配失败,删除m节点,重新循环 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:如果栈为空或者已经包含了一个相同的 mode,此时分两种情况:如果是非计时操作(offer、poll)或者已经超时,直接返回null;其他情况下就把当前节点压进栈顶等待匹配(通过awaitFulfill方法),匹配成功后返回匹配节点的 item,如果节点取消等待就调用clean方法(后面单独讲解)清除取消等待的节点,并返回 null。

2:如果栈顶节点(head)还没有被匹配(通过isFulfilling方法判断),则把当前节点压入栈顶,并尝试与head节点进行匹配,匹配成功后从栈中弹出这两个节点,并返回匹配节点的数据。isFulfilling源码如下:

/** Returns true if m has fulfilling bit set. */ static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; }

3:如果栈顶节点(head)已经持有另外一个数据节点,说明栈顶节点正在匹配,则帮助此节点进行匹配操作,然后继续从第一步开始循环。

3:TransferQueue:继承了内部抽象类 Transferer,实现了transfer方法,用于公平模式下的队列操作,数据按照FIFO的顺序。内部通过单向链表 QNode 实现的双重队列。

源码:E transfer(E e, boolean timed, long nanos) { QNode s = null; // constructed/reused as needed boolean isData = (e != null);//判断put or take 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 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; //推进tail advanceTail(t, s); // swing tail and wait //等待匹配,并返回匹配节点的item,如果取消等待则返回该节点s Object x = awaitFulfill(s, e, timed, nanos); if (x == s) { // wait was cancelled clean(t, s); //等待被取消,清除s节点 return null; } if (!s.isOffList()) { // s节点尚未出列 not already unlinked advanceHead(t, s); // unlink if head if (x != null) // and forget fields s.item = s;//item指向自身 s.waiter = null; } return (x != null) ? (E)x : e; //take } 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.item=m, m cancelled !m.casItem(x, e)) { // 匹配,CAS修改item为给定元素e lost CAS advanceHead(h, m); // 推进head,继续向后查找 dequeue and retry continue; } advanceHead(h, m); //匹配成功,head出列 successfully fulfilled LockSupport.unpark(m.waiter); //唤醒被匹配节点m的线程 return (x != null) ? (E)x : e; } } }

说明:基本算法是循环尝试以下两个动作中的其中一个:

1:若队列为空或者队列中的尾节点(tail)和自己的模式相同,则把当前节点添加到队列尾,调用awaitFulfill等待节点被匹配。匹配成功后返回匹配节点的 item,如果等待节点被中断或等待超时返回null。在此期间会不断检查tail节点,如果tail节点被其他线程修改,则向后推进tail继续循环尝试。

注:TransferQueue 的 awaitFulfill方法与 TransferStack.awaitFulfill算法一致,后面就不再讲解了。

2:如果当前操作模式与尾节点(tail)不同,说明可以进行匹配,则从队列头节点head开始向后查找一个互补节点进行匹配,尝试通过CAS修改互补节点的item字段为给定元素e,匹配成功后向后推进head,并唤醒被匹配节点的waiter线程,最后返回匹配节点的item。

栈/队列节点清除的对比(clean方法)

1:对于队列来说,如果节点被取消,我们几乎总是可以以 O1 的时间复杂度移除节点。但是如果节点在队尾,它必须等待后面节点的取消。

2:对于栈来说,我们可能需要 O(n) 的时间复杂度去遍历整个栈,然后确定节点可被移除,但这可以与访问栈的其他线程并行运行。

---------------------------------------------------------------------------------------------------------------------------

总结:

LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。它和ArrayBlockingQueue的不同点在于:

  • 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
  • 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
  • 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高
  • 效并发地处理大批量数据的时,对于GC可能存在较大影响。
  • 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

发布了50 篇原创文章 · 获赞 2 · 访问量 2342

猜你喜欢

转载自blog.csdn.net/eafun_888/article/details/93044980