并发容器——BlockingQueue相关类

开发 后端

java.util.concurrent提供了多种并发容器,总体上来说有4类

Queue类:BlockingQueue ConcurrentLinkedQueue

Map类:ConcurrentMap

Set类:ConcurrentSkipListSet CopyOnWriteArraySet

List类:CopyOnWriteArrayList

接下来一系列文章,我会对每一类的源码进行分析,试图让它们的实现机制完全暴露在大家面前。这篇主要是BlockingQueue及其相关类。

先给出结构图:

 

 

下面我按这样的顺序来展开:

1、BlockingQueue

2、ArrayBlockingQueue 2.1 添加新元素的方法:add/put/offer

2.2 该类的几个实例变量:takeIndex/putIndex/count/

2.3 Condition实现

3、LinkedBlockingQueue

4、PriorityBlockingQueue

5、DelayQueue

6、BlockingDque+LinkedBlockingQueue

其中前两个分析的尽量详细,为了方便大家看,基本贴出了所有相关源码。后面几个就用尽量用文字论述,如果看得吃力,建议对着jdk的源码看。

1、BlockingQueue

BlockingQueue继承了Queue,Queu是先入先出(FIFO),BlockingQueue是JDK 5.0新引入的。

根据队列null/full时的表现,BlockingQueue的方法分为以下几类:

 

 

至于为什么要使用并发容器,一个典型的例子就是生产者-消费者的例子,为了精简本文篇幅,放到附件中见附件:“生产者-消费者 测试.rar”。

另外,BlockingQueue接口定义的所有方法实现都是线程安全的,它的实现类里面都会用锁和其他控制并发的手段保证这种线程安全,但是这些类同时也实现了Collection接口(主要是AbstractQueue实现),所以会出现BlockingQueue的实现类也能同时使用Conllection接口方法,而这时会出现的问题就是像addAll,containsAll,retainAll和removeAll这类批量方法的实现不保证线程安全,举个例子就是addAll 10个items到一个ArrayBlockingQueue,可能中途失败但是却有几个item已经被放进这个队列里面了。

2、ArrayBlockingQueue

ArrayBlockingQueue创建的时候需要指定容量capacity(可以存储的最大的元素个数,因为它不会自动扩容)以及是否为公平锁(fair参数)。

在创建ArrayBlockingQueue的时候默认创建的是非公平锁,不过我们可以在它的构造函数里指定。这里调用ReentrantLock的构造函数创建锁的时候,调用了:

public ReentrantLock(boolean fair) {

sync = (fair)? new FairSync() : new NonfairSync();

}

FairSync/ NonfairSync是ReentrantLock的内部类:

线程按顺序请求获得公平锁,而一个非公平锁可以闯入,如果锁的状态可用,请求非公平锁的线程可在等待队列中向前跳跃,获得该锁。内部锁synchronized没有提供确定的公平性保证。

分三点来讲这个类:

2.1 添加新元素的方法:add/put/offer

2.2 该类的几个实例变量:takeIndex/putIndex/count/

2.3 Condition实现

2.1 添加新元素的方法:add/put/offer

首先,谈到添加元素的方法,首先得分析以下该类同步机制中用到的锁:

Java代码

  1. lock = new ReentrantLock(fair);     
  2. notEmpty = lock.newCondition();//Condition Variable 1     
  3. notFull =  lock.newCondition();//Condition Variable 2    

 

这三个都是该类的实例变量,只有一个锁lock,然后lock实例化出两个Condition,notEmpty/noFull分别用来协调多线程的读写操作。

Java代码

  1. 1、     
  2. public boolean offer(E e) {     
  3.         if (e == nullthrow new NullPointerException();     
  4.         final ReentrantLock lock = this.lock;//每个对象对应一个显示的锁     
  5.         lock.lock();//请求锁直到获得锁(不可以被interrupte)     
  6.         try {     
  7.             if (count == items.length)//如果队列已经满了     
  8.                 return false;     
  9.             else {     
  10.                 insert(e);     
  11.                 return true;     
  12.             }     
  13.         } finally {     
  14.             lock.unlock();//     
  15.         }     
  16. }     
  17. 看insert方法:     
  18. private void insert(E x) {     
  19.         items[putIndex] = x;     
  20.         //增加全局index的值。     
  21.         /*    
  22.         Inc方法体内部:    
  23.         final int inc(int i) {    
  24.         return (++i == items.length)? 0 : i;    
  25.             }    
  26.         这里可以看出ArrayBlockingQueue采用从前到后向内部数组插入的方式插入新元素的。如果插完了,putIndex可能重新变为0(在已经执行了移除操作的前提下,否则在之前的判断中队列为满)    
  27.         */    
  28.         putIndex = inc(putIndex);      
  29.         ++count;     
  30.         notEmpty.signal();//wake up one waiting thread     
  31. }    

 

Java代码

  1. public void put(E e) throws InterruptedException {     
  2.         if (e == nullthrow new NullPointerException();     
  3.         final E[] items = this.items;     
  4.         final ReentrantLock lock = this.lock;     
  5.         lock.lockInterruptibly();//请求锁直到得到锁或者变为interrupted     
  6.         try {     
  7.             try {     
  8.                 while (count == items.length)//如果满了,当前线程进入noFull对应的等waiting状态     
  9.                     notFull.await();     
  10.             } catch (InterruptedException ie) {     
  11.                 notFull.signal(); // propagate to non-interrupted thread     
  12.                 throw ie;     
  13.             }     
  14.             insert(e);     
  15.         } finally {     
  16.             lock.unlock();     
  17.         }     
  18. }    

 

Java代码

  1. public boolean offer(E e, long timeout, TimeUnit unit)     
  2.         throws InterruptedException {     
  3.     
  4.         if (e == nullthrow new NullPointerException();     
  5.     long nanos = unit.toNanos(timeout);     
  6.         final ReentrantLock lock = this.lock;     
  7.         lock.lockInterruptibly();     
  8.         try {     
  9.             for (;;) {     
  10.                 if (count != items.length) {     
  11.                     insert(e);     
  12.                     return true;     
  13.                 }     
  14.                 if (nanos <= 0)     
  15.                     return false;     
  16.                 try {     
  17.                 //如果没有被 signal/interruptes,需要等待nanos时间才返回     
  18.                     nanos = notFull.awaitNanos(nanos);     
  19.                 } catch (InterruptedException ie) {     
  20.                     notFull.signal(); // propagate to non-interrupted thread     
  21.                     throw ie;     
  22.                 }     
  23.             }     
  24.         } finally {     
  25.             lock.unlock();     
  26.         }     
  27.     }    

 

Java代码

  1. public boolean add(E e) {     
  2.     return super.add(e);     
  3.     }     
  4. 父类:     
  5. public boolean add(E e) {     
  6.         if (offer(e))     
  7.             return true;     
  8.         else    
  9.             throw new IllegalStateException("Queue full");     
  10.     }    

 

2.2 该类的几个实例变量:takeIndex/putIndex/count

Java代码

  1. 用三个数字来维护这个队列中的数据变更:     
  2. /** items index for next take, poll or remove */    
  3.     private int takeIndex;     
  4.     /** items index for next put, offer, or add. */    
  5.     private int putIndex;     
  6.     /** Number of items in the queue */    
  7.     private int count;    

 

提取元素的三个方法take/poll/remove内部都调用了这个方法:

Java代码

  1. private E extract() {     
  2.         final E[] items = this.items;     
  3.         E x = items[takeIndex];     
  4.         items[takeIndex] = null;//移除已经被提取出的元素     
  5.         takeIndex = inc(takeIndex);//策略和添加元素时相同     
  6.         --count;     
  7.         notFull.signal();//提醒其他在notFull这个Condition上waiting的线程可以尝试工作了     
  8.         return x;     
  9.     }   

 

从这个方法里可见,tabkeIndex维护一个可以提取/移除元素的索引位置,因为takeIndex是从0递增的,所以这个类是FIFO队列。

putIndex维护一个可以插入的元素的位置索引。

count显然是维护队列中已经存在的元素总数。

2.3 Condition实现

Condition现在的实现只有java.util.concurrent.locks.AbstractQueueSynchoronizer内部的ConditionObject,并且通过ReentranLock的newCondition()方法暴露出来,这是因为Condition的await()/sinal()一般在lock.lock()与lock.unlock()之间执行,当执行condition.await()方法时,它会首先释放掉本线程持有的锁,然后自己进入等待队列。直到sinal(),唤醒后又会重新试图去拿到锁,拿到后执行await()下的代码,其中释放当前锁和得到当前锁都需要ReentranLock的tryAcquire(int arg)方法来判定,并且享受ReentranLock的重进入特性。

Java代码

  1. public final void await() throws InterruptedException {     
  2.             if (Thread.interrupted())     
  3.                 throw new InterruptedException();     
  4.            //加一个新的condition等待节点     
  5.  Node node = addConditionWaiter();     
  6. //释放自己的锁     
  7.             int savedState = fullyRelease(node);      
  8.             int interruptMode = 0;     
  9.             while (!isOnSyncQueue(node)) {     
  10.             //如果当前线程 等待状态时CONDITION,park住当前线程,等待condition的signal来解除     
  11.                 LockSupport.park(this);     
  12.                 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)     
  13.                     break;     
  14.             }     
  15.             if (acquireQueued(node, savedState) && interruptMode != THROW_IE)     
  16.                 interruptMode = REINTERRUPT;     
  17.             if (node.nextWaiter != null)     
  18.                 unlinkCancelledWaiters();     
  19.             if (interruptMode != 0)     
  20.                 reportInterruptAfterWait(interruptMode);     
  21.         }   

 

3、LinkedBlockingQueue

单向链表结构的队列。如果不指定容量默认为Integer.MAX_VALUE。通过putLock和takeLock两个锁进行同步,两个锁分别实例化notFull和notEmpty两个Condtion,用来协调多线程的存取动作。其中某些方法(如remove,toArray,toString,clear等)的同步需要同时获得这两个锁,并且总是先putLock.lock紧接着takeLock.lock(在同一方法fullyLock中),这样的顺序是为了避免可能出现的死锁情况(我也想不明白为什么会是这样?)

4、PriorityBlockingQueue

看它的三个属性,就基本能看懂这个类了:

Java代码

  1. private final PriorityQueue q;     
  2.     private final ReentrantLock lock = new ReentrantLock(true);     
  3.     private final Condition notEmpty = lock.newCondition();   

 

q说明,本类内部数据结构是PriorityQueue,至于PriorityQueue怎么排序看我之前一篇文章:http://jiadongkai-sina-com.iteye.com/blog/825683

lock说明本类使用一个lock来同步读写等操作。

notEmpty协调队列是否有新元素提供,而队列满了以后会调用PriorityQueue的grow方法来扩容。

5、DelayQueue

Delayed接口继承自Comparable,我们插入的E元素都要实现这个接口。

DelayQueue的设计目的间API文档:

An unbounded blocking queue of Delayed elements, in which an element can only be taken when its delay has expired. The head of the queue is that Delayed element whose delay expired furthest in the past. If no delay has expired there is no head and poll will returnnull. Expiration occurs when an element's getDelay(TimeUnit.NANOSECONDS) method returns a value less than or equal to zero. Even though unexpired elements cannot be removed using take or poll, they are otherwise treated as normal elements. For example, the size method returns the count of both expired and unexpired elements. This queue does not permit null elements.

因为DelayQueue构造函数了里限定死不允许传入comparator(之前的PriorityBlockingQueue中没有限定死),即只能在compare方法里定义优先级的比较规则。再看上面这段英文,“The head of the queue is that Delayed element whose delay expired furthest in the past.”说明compare方法实现的时候要保证最先加入的元素最早结束延时。而 “Expiration occurs when an element's getDelay(TimeUnit.NANOSECONDS) method returns a value less than or equal to zero.”说明getDelay方法的实现必须保证延时到了返回的值变为<=0的int。

上面这段英文中,还说明了:在poll/take的时候,队列中元素会判定这个elment有没有达到超时时间,如果没有达到,poll返回null,而take进入等待状态。但是,除了这两个方法,队列中的元素会被当做正常的元素来对待。例如,size方法返回所有元素的数量,而不管它们有没有达到超时时间。而协调的Condition available只对take和poll是有意义的。

另外需要补充的是,在ScheduledThreadPoolExecutor中工作队列类型是它的内部类DelayedWorkQueue,而DelayedWorkQueue的Task容器是DelayQueue类型,而ScheduledFutureTask作为Delay的实现类作为Runnable的封装后的Task类。也就是说ScheduledThreadPoolExecutor是通过DelayQueue优先级判定规则来执行任务的。

6、BlockingDque+LinkedBlockingQueue

BlockingDque为阻塞双端队列接口,实现类有LinkedBlockingDque。双端队列特别之处是它首尾都可以操作。LinkedBlockingDque不同于LinkedBlockingQueue,它只用一个lock来维护读写操作,并由这个lock实例化出两个Condition notEmpty及notFull,而LinkedBlockingQueue读和写分别维护一个lock。

【编辑推荐】

  1. Java Web应用开发中的一些概念
  2. Tomcat 7 应用实测:声明式Servlet 3.0
  3. 探秘Servlet 3.0中的Web安全改进
  4. 简化Web应用开发 Servlet 3.0特性详解
  5. Servlet 3.0的异步处理
责任编辑:金贺 来源: ITEYE博客
相关推荐

2023-07-03 09:59:00

并发编程并发容器

2020-06-29 07:52:17

Java工具类开发

2023-06-30 08:27:20

2022-07-04 11:39:21

并发容器同步容器机制

2023-12-07 08:13:58

Java开发

2020-07-01 07:52:07

Java并发容器

2009-08-05 18:39:54

C#异常类

2010-02-06 15:49:31

删除C++容器值

2011-07-13 14:58:53

STL容器

2010-01-05 16:15:05

.NET Framew

2022-10-12 07:53:46

并发编程同步工具

2011-06-24 14:17:58

Qt 容器类 QVector

2010-02-01 17:31:06

C++类成员

2009-12-24 15:42:01

ADO类库

2009-09-01 16:14:08

C# Socket类

2012-02-22 14:14:43

Java

2015-10-13 11:01:42

AzureCon微软容器技术

2009-12-21 16:24:24

WCF新到工厂

2023-07-04 13:36:00

同步工具类Phaser

2023-07-05 08:18:54

Atomic类乐观锁悲观锁
点赞
收藏

51CTO技术栈公众号