27. Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。
ArrayBlockingQueue介绍:
ArrayBlockingQueue是一个由数组支持的有界阻塞队列,继承自AbstractBlockingQueue,实现了BlockingQueue接口(Queue接口和Collection接口)。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。
此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。
ArrayBlockingQueue常用操作:add,offer,put,peek,pool,take,remove。
add、offer、put都是插入操作。peek,pool,take,remove是取出操作。他们之间的区别和关联:add: 内部是获取的offer方法,将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则抛出 IllegalStateException。不会阻塞。
offer:将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则返回 false。不会阻塞。
put:将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间,只要不被中断,就会插入数据到队列中。会阻塞,可以响应中断。
peek:获取但不移除此队列的头;如果此队列为空,则返回 null。不会阻塞。
pool:与offer对应,获取并移除此队列的头,如果此队列为空,则返回 null。
take:与put对应,获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。
remove:与add对应,从此队列中移除指定元素的单个实例(如果存在)。boolean remove(Object o) 返回true/false。remove() 在队列为空时会抛异常NoSuchElementException - if this queue is empty
LinkedBlockingQueue介绍:
一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。可选的容量范围构造方法参数作为防止队列过度扩展的一种方法。如果未指定容量,则它等于 Integer.MAX_VALUE。除非插入节点会使队列超出容量,否则每次插入后会动态地创建链接节点。
-----------------------------以下内容来自http://blog.csdn.net/qq_23359777/article/details/70146778-------------------------------------------------------------------
4.源码分析
下面从源码的角度来看,ArrayBlockingQueue的实现。JDK版本是1.8。
4.1 保存数据的结构
/** The queued items */ final Object[] items;可以看到,是一个Object的数组。
4.2全局锁
/** Main lock guarding all access */ final ReentrantLock lock;注视也说明了,这是一个掌管所有访问操作的锁。全局共享。都会使用这个锁。
4.3 add 和 offer
public boolean add(E e) { return super.add(e); } public boolean offer(E e) { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lock(); // 一直等到获取锁 try { if (count == items.length) //假如当前容纳的元素个数已经等于数组长度,那么返回false return false; else { enqueue(e); // 将元素插入到队列中,返回true return true; } } finally { lock.unlock(); //释放锁 } }把他们放在一起,实际上super.add(e)里面就是调用的offer方法,当offer返回false时,就抛出一个异常,否则返回true。我们直接分析offer方法。
他的实现逻辑是这样子的。
一直等待获取锁 - > 当获取到锁之后,比较当前的元素个数与数组长度,当相等时,那么队列已经满了,无法插入,返回false->否则进行入队操作,返回true。
4.4 put
public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); //可中断的获取锁 try { while (count == items.length) //当线程从等待中被唤醒时,会比较当前队列是否已经满了 notFull.await(); //notFull = lock.newCondition 表示队列不满这种状况,假如现场在插入的时候 enqueue(e); //当前队列已经满了时,则需要等到这种情况的发生。 } finally { //可以看出这是一种阻塞式的插入方式 lock.unlock(); } }
4.5 poll
如前文所说,poll方法与offer相互对应,见源码
public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { return (count == 0) ? null : dequeue(); //假如当前队列中的元素为空,返回null,否则返回出列的元素 } finally { lock.unlock(); } }
4.6 take
take方法和put方法相互对应,他一定要拿到一个元素,除非线程被中断。
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) //线程在刚进入 和 被唤醒时,会查看当前队列是否为空 notEmpty.await(); //notEmpty=lock.newCondition表示队列不为空的这种情况。假如一个线程进行take return dequeue(); //操作时,队列为空,则会一直等到到这种情况发生。 } finally { lock.unlock(); } }
4.7 peek
如前文所说,peek方法不会真正的从队列中删除元素,实际上只是取出头元素而已。
public E peek() { final ReentrantLock lock = this.lock; lock.lock(); try { return itemAt(takeIndex); // null when queue is empty
// 实际上 itemAt 方法就是 return (E) items[i];//也就是说 返回 数组中的第i个元素。 } finally { lock.unlock() ; }}
4.8 remove
remove方法实现在Abstract方法中,很容易看懂,里面就是走的poll方法。
public E remove() { E x = poll(); if (x != null) return x; else throw new NoSuchElementException(); }
4.9 enqueue
我们再来看看真正得到入队操作,不然光看上面的截图也不明白不是。
private void enqueue(E x) { //因为调用enqueue的方法都已经同步过了,这里就不需要在同步了 // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; //putIndex是下一个放至元素的坐标 if (++putIndex == items.length) //putIndex+1, 并且比较是否与数组长度相同,是的话,则从数组开头 putIndex = 0; //插入元素,这就是循环数组的奥秘了 count++; //当前元素总量+1 notEmpty.signal(); //给等到在数组非空的线程一个信号,唤醒他们。 }
4.10 dequeue
当然,我们也要看一下出对的操作
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; //将要取出的元素指向null 表示这个元素已经取出去了 if (++takeIndex == items.length) //takeIndex +1,同样的假如已经取到了数组的末尾,那么就要重新开始取 takeIndex = 0; //这就是循环数组 count--; if (itrs != null) itrs.elementDequeued(); //这里实现就比较麻烦,下次单独出一个吧,可以看看源码 notFull.signal(); //同样 需要给 等待数组不满这种情况的线程一个信号,唤醒他们。 return x; }
Queue接口参考
软件包 java.util.concurrent 的描述
在并发编程中很常用的实用工具类。此包包括了几个小的、已标准化的可扩展框架,以及一些提供有用功能的类,没有这些类,这些功能会很难实现或实现起来冗长乏味。下面简要描述主要的组件。另请参阅 locks 和 atomic 包。
执行程序
接口。Executor
是一个简单的标准化接口,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。根据所使用的具体 Executor 类的不同,可能在新创建的线程中,现有的任务执行线程中,或者调用
execute()
的线程中执行任务,并且可能顺序或并发执行。
ExecutorService
提供了多个完整的异步任务执行框架。ExecutorService 管理任务的排队和安排,并允许受控制的关闭。
ScheduledExecutorService
子接口及相关的接口添加了对延迟的和定期任务执行的支持。ExecutorService 提供了安排异步执行的方法,可执行由
Callable
表示的任何函数,结果类似于
Runnable
。
Future
返回函数的结果,允许确定执行是否完成,并提供取消执行的方法。
RunnableFuture
是拥有
run
方法的 Future,
run
方法执行时将设置其结果。
实现。类 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
提供可调的、灵活的线程池。Executors
类提供大多数 Executor 的常见类型和配置的工厂方法,以及使用它们的几种实用工具方法。其他基于 Executor 的实用工具包括具体类 FutureTask
,它提供 Future 的常见可扩展实现,以及 ExecutorCompletionService
,它有助于协调对异步任务组的处理。
队列
java.util.concurrentConcurrentLinkedQueue
类提供了高效的、可伸缩的、线程安全的非阻塞 FIFO 队列。java.util.concurrent 中的五个实现都支持扩展的
BlockingQueue
接口,该接口定义了 put 和 take 的阻塞版本:
LinkedBlockingQueue
、
ArrayBlockingQueue
、
SynchronousQueue
、
PriorityBlockingQueue
和
DelayQueue
。这些不同的类覆盖了生产者-使用者、消息传递、并行任务执行和相关并发设计的大多数常见使用的上下文。
BlockingDeque
接口扩展
BlockingQueue
,以支持 FIFO 和 LIFO(基于堆栈)操作。
LinkedBlockingDeque
类提供一个实现。
计时
TimeUnit
类为指定和控制基于超时的操作提供了多重粒度(包括纳秒级)。该包中的大多数类除了包含不确定的等待之外,还包含基于超时的操作。在使用超时的所有情况中,超时指定了在表明已超时前该方法应该等待的最少时间。在超时发生后,实现会“尽力”检测超时。但是,在检测超时与超时之后再次实际执行线程之间可能要经过不确定的时间。接受超时期参数的所有方法将小于等于 0 的值视为根本不会等待。要“永远”等待,可以使用
Long.MAX_VALUE
值。
同步器
四个类可协助实现常见的专用同步语句。Semaphore
是一个经典的并发工具。
CountDownLatch
是一个极其简单但又极其常用的实用工具,用于在保持给定数目的信号、事件或条件前阻塞执行。
CyclicBarrier
是一个可重置的多路同步点,在某些并行编程风格中很有用。
Exchanger
允许两个线程在 collection 点交换对象,它在多流水线设计中是有用的。
并发 Collection
除队列外,此包还提供了设计用于多线程上下文中的 Collection 实现:ConcurrentHashMap
、
ConcurrentSkipListMap
、
ConcurrentSkipListSet
、
CopyOnWriteArrayList
和
CopyOnWriteArraySet
。当期望许多线程访问一个给定 collection 时,
ConcurrentHashMap
通常优于同步的
HashMap
,
ConcurrentSkipListMap
通常优于同步的
TreeMap
。当期望的读数和遍历远远大于列表的更新数时,
CopyOnWriteArrayList
优于同步的
ArrayList
。
此包中与某些类一起使用的“Concurrent”前缀;是一种简写,表明与类似的“同步”类有所不同。例如,java.util.Hashtable 和 Collections.synchronizedMap(new HashMap()) 是同步的,但 ConcurrentHashMap
则是“并发的”。并发 collection 是线程安全的,但是不受单个排他锁的管理。在 ConcurrentHashMap 这一特定情况下,它可以安全地允许进行任意数目的并发读取,以及数目可调的并发写入。需要通过单个锁不允许对 collection 的所有访问时,“同步”类是很有用的,其代价是较差的可伸缩性。在期望多个线程访问公共 collection 的其他情况中,通常“并发”版本要更好一些。当 collection 是未共享的,或者仅保持其他锁时 collection 是可访问的情况下,非同步 collection 则要更好一些。
大多数并发 Collection 实现(包括大多数 Queue)与常规的 java.util 约定也不同,因为它们的迭代器提供了弱一致的,而不是快速失败的遍历。弱一致的迭代器是线程安全的,但是在迭代时没有必要冻结 collection,所以它不一定反映自迭代器创建以来的所有更新。
内存一致性属性
Java Language Specification 第 17 章定义了内存操作(如共享变量的读写)的 happen-before 关系。只有写入操作 happen-before 读取操作时,才保证一个线程写入的结果对另一个线程的读取是可视的。 synchronized
和 volatile
构造 happen-before 关系, Thread.start()
和 Thread.join()
方法形成 happen-before 关系。尤其是:
- 线程中的每个操作 happen-before 稍后按程序顺序传入的该线程中的每个操作。
- 一个解除锁监视器的(
synchronized
阻塞或方法退出)happen-before 相同监视器的每个后续锁(synchronized
阻塞或方法进入)。并且因为 happen-before 关系是可传递的,所以解除锁定之前的线程的所有操作 happen-before 锁定该监视器的任何线程后续的所有操作。 - 写入
volatile
字段 happen-before 每个后续读取相同字段。volatile
字段的读取和写入与进入和退出监视器具有相似的内存一致性效果,但不需要互斥锁。 - 在线程上调用
start
happen-before 已启动的线程中的任何线程。 - 线程中的所有操作 happen-before 从该线程上的
join
成功返回的任何其他线程。
java.util.concurrent
中所有类的方法及其子包扩展了这些对更高级别同步的保证。尤其是:
- 线程中将一个对象放入任何并发 collection 之前的操作 happen-before 从另一线程中的 collection 访问或移除该元素的后续操作。
- 线程中向
Executor
提交Runnable
之前的操作 happen-before 其执行开始。同样适用于向ExecutorService
提交Callables
。 - 异步计算(由
Future
表示)所采取的操作 happen-before 通过另一线程中Future.get()
获取结果后续的操作。 - “释放”同步储存方法(如
Lock.unlock
、Semaphore.release
和CountDownLatch.countDown
)之前的操作 happen-before 另一线程中相同同步储存对象成功“获取”方法(如Lock.lock
、Semaphore.acquire
、Condition.await
和CountDownLatch.await
)的后续操作。 - 对于通过
Exchanger
成功交换对象的每个线程对,每个线程中exchange()
之前的操作 happen-before 另一线程中对应exchange()
后续的操作。 - 调用
CyclicBarrier.await
之前的操作 happen-before 屏障操作所执行的操作,屏障操作所执行的操作 happen-before 从另一线程中对应await
成功返回的后续操作。
CountDownLatch介绍:
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
用给定的计数 初始化 CountDownLatch
。由于调用了 countDown()
方法,所以在当前计数到达零之前,await
方法会一直受阻塞。之后,会释放所有等待的线程,await
的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier
。
CountDownLatch
是一个通用同步工具,它有很多用途。将计数 1 初始化的 CountDownLatch
用作一个简单的开/关锁存器,或入口:在通过调用 countDown()
的线程打开入口前,所有调用 await
的线程都一直在入口处等待。用 N 初始化的 CountDownLatch
可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。
CountDownLatch
的一个有用特性是,它不要求调用 countDown
方法的线程等到计数到达零时才继续,而在所有线程都能通过之前,它只是阻止任何线程继续通过一个 await
。
示例用法: 下面给出了两个类,其中一组 worker 线程使用了两个倒计数锁存器:(可以详见代码一)
- 第一个类是一个启动信号,在 driver 为继续执行 worker 做好准备之前,它会阻止所有的 worker 继续执行。
- 第二个类是一个完成信号,它允许 driver 在完成所有 worker 之前一直等待。
class Driver { // ... void main() throws InterruptedException { CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(N); for (int i = 0; i < N; ++i) // create and start threads new Thread(new Worker(startSignal, doneSignal)).start(); doSomethingElse(); // don't let run yet startSignal.countDown(); // let all threads proceed doSomethingElse(); doneSignal.await(); // wait for all to finish } } class Worker implements Runnable { private final CountDownLatch startSignal; private final CountDownLatch doneSignal; Worker(CountDownLatch startSignal, CountDownLatch doneSignal) { this.startSignal = startSignal; this.doneSignal = doneSignal; } public void run() { try { startSignal.await(); doWork(); doneSignal.countDown(); } catch (InterruptedException ex) {} // return; } void doWork() { ... } }
另一种典型用法是,将一个问题分成 N 个部分,用执行每个部分并让锁存器倒计数的 Runnable 来描述每个部分,然后将所有 Runnable 加入到 Executor 队列。当所有的子部分完成后,协调线程就能够通过 await。(当线程必须用这种方法反复倒计数时,可改为使用 CyclicBarrier
。)(详见代码二)
class Driver2 { // ... void main() throws InterruptedException { CountDownLatch doneSignal = new CountDownLatch(N); Executor e = ... for (int i = 0; i < N; ++i) // create and start threads e.execute(new WorkerRunnable(doneSignal, i)); doneSignal.await(); // wait for all to finish } } class WorkerRunnable implements Runnable { private final CountDownLatch doneSignal; private final int i; WorkerRunnable(CountDownLatch doneSignal, int i) { this.doneSignal = doneSignal; this.i = i; } public void run() { try { doWork(i); doneSignal.countDown(); } catch (InterruptedException ex) {} // return; } void doWork() { ... } }
内存一致性效果:线程中调用 countDown()
之前的操作 happen-before 紧跟在从另一个线程中对应 await()
成功返回的操作。
代码一:
import java.util.concurrent.CountDownLatch;
class Driver { // ...
static int N = 5;
public static void main(String[] args) throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
//doneSignal为0时,才执行,所以每次最后一个执行
new Thread(new Worker(doneSignal, doneSignal),"Done Thread ").start();
for (int i = 0; i < N; ++i) // create and start threads
new Thread(new Worker(startSignal, doneSignal)).start();
doSomethingElse(); // don't let run yet
startSignal.countDown(); // let all threads proceed
Thread.sleep(3000);//加个等待,可以清晰看到结果,否则会由于通知线程的时间差导致结果不直观
doSomethingElse();
doneSignal.await(); // wait for all to finish
}
private static void doSomethingElse() {
System.err.println("do something else");
}
}
class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
public void run() {
try {
startSignal.await();
doWork();
doneSignal.countDown();
} catch (InterruptedException ex) {
} // return;
}
void doWork() {
System.err.println(Thread.currentThread().getName()+"is working");
}
}
代码二:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
class Driver2 { // ...
static int N = 5;
public static void main(String[] args) throws InterruptedException {
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = Executors.newScheduledThreadPool(1);
// doneSignal为0时,才执行,所以每次最后一个执行
new Thread(new Runnable() {
@Override
public void run() {
try {
doneSignal.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName()+"is working");
}
}, "Done Thread ").start();
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
doneSignal.await(); // wait for all to finish
}
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
doWork(i);
doneSignal.countDown();
}
void doWork(int i2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.err.println(i2 + " " + Thread.currentThread().getName() + " is working");
}
}
拓展阅读:
Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore
http://www.importnew.com/21889.html
打赏
如果觉得我的文章对你有帮助,有钱就捧个钱场,没钱就捧个人场,欢迎点赞或转发 ,并请注明原出处,谢谢....