在jdk中,为并发编程提供了CyclicBarrier(栅栏),CountDownLatch(闭锁),Semaphore(信号量),Exchanger(数据交换)等工具类,我们在前面的学习中已经学习并分析了CountDownLatch工具类的使用方式和源码实现,接下来我们继续学习CyclicBarrier,Semaphore,Exchanger的源码实现。

1. CyclicBarrier(栅栏)

  1.1 CyclicBarrier的使用方式

   在实际的并发中,我们可能需要等待所有线程到达一个数量点之后,才会去继续执行线程,这个点就是栅栏,比如开会的场景,我们应该要做的是应该等到所有与会人员都到齐之后,我们才开始开会,使用CyclicBarrier就可以很容易的实现这么一个场景,示例源码:

package com.company.thread.t4;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { public void meeting(CyclicBarrier cyclicBarrier) {
Random random = new Random(); try {
Thread.sleep(random.nextInt(4000));
} catch (InterruptedException e) {
e.printStackTrace();
}
// if (Thread.currentThread().getName().equals("Thread-1")) {
// throw new RuntimeException("异常了");
// }
System.out.println(Thread.currentThread().getName() + "进入会议室");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "开始汇报....");
} public static void main(String[] args) { final CyclicBarrierDemo cyclicBarrierDemo = new CyclicBarrierDemo();
final CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始开会...");
}
}); for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
cyclicBarrierDemo.meeting(cyclicBarrier);
}
}).start();
}
}
}

  meeting()方法传入CyclicBarrier对象,调用await()方法等待,直到达到初始化CyclicBarrier对象是传入的值时才会唤醒所有线程并发执行剩下的任务。

  main方法中构建等待的线程数为10个,并传入一个任务,该任务由最后进入的线程来执行,将cyclicBarrier传入到meetting中,此时执行结果为:

Thread-9进入会议室
Thread-6进入会议室
Thread-5进入会议室
Thread-4进入会议室
Thread-3进入会议室
Thread-1进入会议室
Thread-8进入会议室
Thread-7进入会议室
Thread-2进入会议室
Thread-0进入会议室
Thread-0开始开会...
Thread-0开始汇报....
Thread-9开始汇报....
Thread-6开始汇报....
Thread-4开始汇报....
Thread-5开始汇报....
Thread-3开始汇报....
Thread-1开始汇报....
Thread-7开始汇报....
Thread-2开始汇报....
Thread-8开始汇报....

   执行结果显示,只有当是个线程都进入等待时,才会执行我们在构造CyclicBarrier对象时传入的任务,此任务就相当于是一个栅栏点,达到线程等待数量后,唤醒所有线程去继续执行,CyclicBarrier的使用方式就如上代码所以,需要注意的是,如果执行任务期间有一个线程出现异常,导致无法达到栅栏的点,那么所有线程都加会一直阻塞。

  1.2 CyclicBarrier源码实现

  首先看CyclicBarrier的构造方法:

 public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}

  构造函数就是执行了一个赋值的操作,将我们需要等待的线程个数进行初始化,然后看核心的方法await(),

  await():

 public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen;
}
}

  直接放回了dowait()方法,继续进入dowait()方法,

 dowait():

private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation; if (g.broken)
throw new BrokenBarrierException(); if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
} int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
} // loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
} if (g.broken)
throw new BrokenBarrierException(); if (g != generation)
return index; if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}

  它是CyclicBarrier的核心方法,我们来一步步分析:

  第一步:首先获取可重入锁,判断线程是否被中断,如果被中断,调用beakBarrier()方法,generation.broken = true,还原count为初始值,并唤醒所有等待的线程。并抛出中断异常

  beakBarrier():

 private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}

  否则将count减减,count减到最后一个线程进入时。使用ranAction判断是否执行,判断初始化时是否传入任务,传入则执行该任务的run方法,这里就是我们前面说得栅栏任务由最后一个进入的线程执行的实现,将ranAction置为true,nextGeneration()调用唤醒所有线程,并将所有参数重置为初始化状态。表示这一组任务已经等待完成并开始执行,

  nextGeneration()方法:

 private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}

  当index不等于0,表示不是最后一个线程进入时,自旋使线程等待,当g.broken = true时,表示线程出现过错误,执行breakBarrier();方法,否则就中断线程,接着就是校验线程是否正确执行和是否超时等的一些校验,dowait()方法源码说完了。

  主要的就是初始化一个状态,每次调用自减,跟直到状态为0时,唤醒所有的线程,这点于CountDownLatch()类似,但是CyclicBarrier是可重用的,当到达栅栏的所有线程被唤醒之后,它会将CyclicBarrier实例还原成初始状态,下一次可以继续使用。

2. Semaphore(信号量)

  信号量指当前空闲有三个资源,但是有一个栏杆一样的东西在拦截线程,当前面三个线程进来获取资源时,因为有资源所以允许三个线程进入,当资源被前面三个线程占用时,其他的线程就只能等待着正在使用资源的线程释放资源,栏杆才会随机的放一个线程进入,这样就达到了控制并发线程数量的一个目的;

  2.1 使用方式

  直接看代码:

package com.company.thread.t4;

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {

    public void method (Semaphore semaphore) {

        try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "is running ");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
semaphore.release();
} public static void main(String[] args) {
final SemaphoreDemo semaphoreDemo = new SemaphoreDemo();
final Semaphore semaphore = new Semaphore(10);
for (int i = 0; i < 20; i++){
new Thread(new Runnable() {
@Override
public void run() {
semaphoreDemo.method(semaphore);
}
}).start();
}
}
}

  method()方法中调用 semaphore.acquire();获取资源,当能获取到资源后继续执行,执行完成后调用 semaphore.release()方法释放资源,main方法中可以创建很多个线程,但是我们通过观察输出可以看到,每次都只能有是个线程在执行,这就是在初始化的时候控制了线程并发执行的数量只能是是个,所有多余的将会等待前面执行的线程释放资源,然后拿到资源,使用都是非常简单的,看示例源码来理解完全没有问题,接下来我们看看信号量的实现原理

 2.2 源码实现

  首先还是看构造方法:

public Semaphore(int permits) {
sync = new NonfairSync(permits);
}

  可以看到它是实例化非公平内部同步器,并将信号量个数初始化,这里就不再深入,主要的方法是acquire()/release()方法,

  acquire()方法: 

 public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

  这里调用的AQS的共享模式的acquireSharedInterruptibly(1)对线程获取资源,由于AQS的模式主要是看自己实现的tryAcquireShared()方法,所以我们主要看tryAcquireShared()源码:

 protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}

  返回nonfairTryAcquireShared()的执行结果,

  nonfairTryAcquireShared()方法是操作AQS状态。

final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}

  返回资源数量,我们知道在AQS共享模式下,如果返回大于0时,它就可以去等待队列中唤醒线程,这里是自旋直到返回状态减acquires后的能拿到资源的状态,即是自旋一直等待其他线程释放资源,此时当前线程就可以拿到资源执行。

  release()方法:

 public void release() {
sync.releaseShared(1);
}

  也是使用AQS的releaseShared()方法,所以我们这里依然只看自己实现的tryReleaseShared()方法:

 protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}

  它的实现就是,当释放资源时,不断修改AQS的stats增加releases资源,这样当释放成功后,等待中的线程就有机会获取线程去执行了。

  理解了AQS的情况下,CyclicBarrier和信号量的实现其实也就不难理解了,都是继续AQS提供的模式重写tryXX方法来实现自己想要的功能。

3. Exchanger(数据交换)

  Exchanger是可以线程之间通信的,它可以使数据在两个线程直接交换,也就是A线程可以获取到B线程中的某些数据,B线程也可能可以获取到B线程的数据。

  3.1 使用方式

  直接上代码:

package com.company.thread.t5;

import java.util.concurrent.Exchanger;

public class Demo {

    public void a (Exchanger<String> exchanger) {
System.out.println("a 方法执行"); try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String res = "12345"; try {
String value = exchanger.exchange(res);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void b(Exchanger<String> exchanger) {
System.out.println("b 方法开始执行"); try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String res = "123456"; try {
String value = exchanger.exchange(res);
System.out.println("开始进行比对");
System.out.println("比对结果为:" + value.equals(res));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

  Exchanger是一个泛型类,他管理的是传入的类型的变量,示例中使用一个Exchanger实例,在a方法中调用exchange()将需要交换的数据出入,b也一样,创建两个线程,分别调用a,b方法,传入同一个Exchanger实例,这样当两个线程执行到一个零界点时,就会将两个线程中的传入的数据交换的两外的线程中,这样就可以在另外的方法或线程中拿到其他线程的某些我们需要的数据。jdk文档中介绍,它最常用的就是缓冲这一块,有兴趣可以去了解一下。Exchanger的使用也是非常简单的。

  3.2 Exchanger 源码

    exchange()核心方法:

 public V exchange(V x) throws InterruptedException {
if (!Thread.interrupted()) {
Object v = doExchange((x == null) ? NULL_ITEM : x, false, 0);
if (v == NULL_ITEM)
return null;
if (v != CANCEL)
return (V)v;
Thread.interrupted(); // Clear interrupt status on IE throw
}
throw new InterruptedException();
}

  简单明了的看到,它调用了doExchange()方法去执行了具体的改变操作,所以进入doExchange():

 
private Object doExchange(Object item, boolean timed, long nanos) {
Node me = new Node(item); // Create in case occupying
int index = hashIndex(); // Index of current slot
int fails = 0; // Number of CAS failures for (;;) {
Object y; // Contents of current slot
Slot slot = arena[index];
if (slot == null) // Lazily initialize slots
createSlot(index); // Continue loop to reread
else if ((y = slot.get()) != null && // Try to fulfill
slot.compareAndSet(y, null)) {
Node you = (Node)y; // Transfer item
if (you.compareAndSet(null, item)) {
LockSupport.unpark(you.waiter);
return you.item;
} // Else cancelled; continue
}
else if (y == null && // Try to occupy
slot.compareAndSet(null, me)) {
if (index == 0) // Blocking wait for slot 0
return timed ?
awaitNanos(me, slot, nanos) :
await(me, slot);
Object v = spinWait(me, slot); // Spin wait for non-0
if (v != CANCEL)
return v;
me = new Node(item); // Throw away cancelled node
int m = max.get();
if (m > (index >>>= 1)) // Decrease index
max.compareAndSet(m, m - 1); // Maybe shrink table
}
else if (++fails > 1) { // Allow 2 fails on 1st slot
int m = max.get();
if (fails > 3 && m < FULL && max.compareAndSet(m, m + 1))
index = m + 1; // Grow on 3rd failed slot
else if (--index < 0)
index = m; // Circularly traverse
}
}
}

  它的核心算法是for(;;)中的内容,当slot为空时,创建一个Slot,不为空,则获取slot的value不为空,则表示已经有线程进来了,使用cas将当前线程的value与前一个进入的value进行交换,成功后唤醒前面进入之后等待的线程。当前y为空,则表示当前线程是第一个进来的线程,设置创建一个节点,将当前需要保存的value设置到slot中,以node的形式保存,当被唤醒时,则执行await()方法返回修改后的值,因为在上面代码中第二个进来的线程已经交换了两个需要交换的数据,所以当等待线程被叫醒后返回的被后一个线程交换后的数据。

  Java的并发工具类,到这里就算是学习了,理解不是很深刻,望各位各位多多指点,共同进步,谢谢!

原文 并发编程学习笔记(10)----并发工具类CyclicBarrier、Semaphore和Exchanger类的使用和原理

并发编程学习笔记(10)----并发工具类CyclicBarrier、Semaphore和Exchanger类的使用和原理的更多相关文章

  1. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...

  2. JUC并发编程学习笔记

    JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...

  3. Java并发编程学习笔记

    Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...

  4. 并发编程学习笔记(14)----ThreadPoolExecutor(线程池)的使用及原理

    1. 概述 1.1 什么是线程池 与jdbc连接池类似,在创建线程池或销毁线程时,会消耗大量的系统资源,因此在java中提出了线程池的概念,预先创建好固定数量的线程,当有任务需要线程去执行时,不用再去 ...

  5. 并发编程学习笔记(15)----Executor框架的使用

    Executor执行已提交的 Runnable 任务的对象.此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节.调度等)分离开来的方法.通常使用 Executor 而不是显式地创建 ...

  6. 并发编程学习笔记(13)----ConcurrentLinkedQueue(非阻塞队列)和BlockingQueue(阻塞队列)原理

    · 在并发编程中,我们有时候会需要使用到线程安全的队列,而在Java中如果我们需要实现队列可以有两种方式,一种是阻塞式队列.另一种是非阻塞式的队列,阻塞式队列采用锁来实现,而非阻塞式队列则是采用cas ...

  7. 并发编程学习笔记(6)----公平锁和ReentrantReadWriteLock使用及原理

    (一)公平锁 1.什么是公平锁? 公平锁指的是在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁. 2.在java中的实现? 在java的并发包中 ...

  8. 并发编程学习笔记(5)----AbstractQueuedSynchronizer(AQS)原理及使用

    (一)什么是AQS? 阅读java文档可以知道,AbstractQueuedSynchronizer是实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量.事件,等等)提供一个框架, ...

  9. 并发编程学习笔记(4)----jdk5中提供的原子类及Lock使用及原理

    (1)jdk中原子类的使用: jdk5中提供了很多原子类,它会使变量的操作变成原子性的. 原子性:原子性指的是一个操作是不可中断的,即使是在多个线程一起操作的情况下,一个操作一旦开始,就不会被其他线程 ...

随机推荐

  1. Windows 驱动开发 - 7

    在<Windows 驱动开发 - 5>我们所说的读写操作在本篇实现. 在WDF中实现此功能主要为:EvtIoRead和EvtIoWrite. 首先,在EvtDeviceAdd设置以上两个回 ...

  2. iOS学习笔记(4) — UITableView的 重用机制

    iOS学习笔记(4) — UITableView的 重用机制 UITableView中的cell是动态的,在使用过程中,系统会根据屏幕的高度(480)和每个cell的高度计算屏幕中需要显示的cell的 ...

  3. 2016/2/19 css样式表 Cascading Style Sheet 叠层样式表 美化HTML网页

    一.样式表 (一)样式表的分类 1.内联样式表 和HTML联合显示,控制精确,但是可重用性差,冗余较多. 例:<p style="font-size:14px;">内联 ...

  4. ios28--UIScrollView

    // // ViewController.m // UIScrollVIew #import "ViewController.h" @interface ViewControlle ...

  5. 修改Android系统的触摸提示音【学习笔记】

    平台信息:内核:Linux version 3.10.0系统:android/android6.0平台:rk3288 作者:庄泽彬(欢迎转载,请注明作者) 邮箱:2760715357@qq.com 本 ...

  6. oracle存储过程和游标的使用

    oracle存储过程和游标的使用 (2011-04-19 14:52:47) 转载▼ 游标: 用来查询数据库,获取记录集合(结果集)的指针,我们所说的游标通常是指显式游标,因此从现在起没有特别指明的情 ...

  7. android短信拦截

    广播分2种,无序广播和有序广播.可以理解为散列和队列广播. 首先无序广播,不能中断,分发机制有点类似散列发送.这种广播的的发送为:context.sendBroadcast这种广播是不能中断的,请看A ...

  8. luogu3941入阵曲

    https://www.zybuluo.com/ysner/note/1301562 题面 统计在给出的\(n*m\)矩阵中,有多少个不同的子矩形中的数字之和是\(k\)的倍数? 解析 切不掉这道题是 ...

  9. E20180127-hm

    retain  vt. 保持; 留在心中,记住; 雇用; 付定金保留;

  10. E20170606-gg

    complete adj. 完整的; 完成的; (用以强调) 完全的; 达到结尾的;  vt. 完成,使完满; 完成或结束; 填写(表格); process   n. 过程; 工序; 做事方法; 工艺 ...