CountDownLatch、CyclicBarrier、Semaphore基础使用
jdk1.7引入的好玩意
Phaser相位器(移相器),可以重复使用的循环屏障。
https://blog.csdn.net/m0_37540696/article/details/113521523
CountDownLatch
CountDownLatch是一个同步工具类,从命名可以解读到 countdown 是倒数的意思,类似于我们倒计
时的概念。
CountDownLatch提供了两个方法,一个是 countDown,一个是 await, CountDownLatch初始化的时候需要传入一个整数,在这个整数倒数到 0 之前,调用了 await 方法的程序都必须要等待,然后通过 countDown 来倒数。
使用案例一
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(3);
new Thread(()->{
System.out.println(""+Thread.currentThread().getName()+"- 执行中");
countDownLatch.countDown();
System.out.println(""+Thread.currentThread().g
etName()+"- 执行完毕");
},"t1").start();
new Thread(()->{
System.out.println(""+Thread.currentThread().g
etName()+"- 执行中");
countDownLatch.countDown();
System.out.println(""+Thread.currentThread().g
etName()+"- 执行完毕");
},"t2").start();
new Thread(()->{
System.out.println(""+Thread.currentThread().g
etName()+"- 执行中");
countDownLatch.countDown();
System.out.println(""+Thread.currentThread().g
etName()+"- 执行完毕");
},"t3").start();
countDownLatch.await();
System.out.println(" 所有线程执行完毕");
}
从代码的实现来看,有点类似 join 的功能,但是比 join 更加灵活。CountDownLatch 构造函数会接收一个 int 类型的参数作为计数器的初始值,当调用 CountDownLatch 的countDown 方法时,这个计数器就会减一。通过 await 方法去阻塞去阻塞主流程。
高并发场景案例二
static CountDownLatch countDownLatch=new CountDownLatch(1);
@Override
public void run() {
try {
countDownLatch.await();
//TODO
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println("ThreadName:"+Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<1000;i++){
new Demo().start();
}
countDownLatch.countDown();
}
凡是涉及到需要指定某个线程在执行之前,要等到前置线程执行完毕之后才执行的场景,都可以CountDownLatch。
对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown() 方法,另一个是 await() 方法。
countDown() 方法每次调用都会将 state 减 1,直到state 的值为 0;而 await 是一个阻塞方法,当 state 减
为 0 的时候,await 方法才会返回。await 可以被多个线程调用,大家在这个时候脑子里要有个图:所有调用了
await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。
Semaphore
semaphore 也就是我们常说的信号灯,semaphore 可以控制同时访问的线程个数,通过 acquire 获取一个许可,如果没有就等待,通过 release 释放一个许可。有点类似限流的作用。叫信号灯的原因也和他的用处有关,比如某商场就 5 个停车位,每个停车位只能停一辆车,如果这个时候来了 10 辆车,必须要等前面有空的车位才能进入。
使用案例一
public class Test {
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(5);
for(int i=0;i<10;i++){
new Car(i,semaphore).start();
}
}
static class Car extends Thread{
private int num;
private Semaphore semaphore;
public Car(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
public void run(){
try {
semaphore.acquire();// 获取一个许可
System.out.println(" 第"+num+" 占用一个停车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(" 第"+num+" 俩车走喽");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
使用场景
Semaphore 比较常见的就是用来做限流操作了。
Semaphore 公平策略和非公平策略
FairSync
/**
* Fair version
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
// 区别就在于是不是会先判断是否有线程在排队,然后才进行 CAS 减操作
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
NofairSync
通过对比发现公平和非公平的区别就在于是否多了一个hasQueuedPredecessors 的判断
/**
* NonFair version
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
//
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
由于后面的代码和 CountDownLatch 的是完全一样,都是基于共享锁的实现。
CyclicBarrier
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续工作。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 当前线程已经到达了屏障,然后当前线程被阻塞
使用场景
当存在需要所有的子任务都完成时,才执行主任务,这个时候就可以选择使用 CyclicBarrier。
使用案例一
DataImportThread
public class DataImportThread extends Thread{
private CyclicBarrier cyclicBarrier;
private String path;
public DataImportThread(CyclicBarrier cyclicBarrier, String path) {
this.cyclicBarrier = cyclicBarrier;
this.path = path;
}
@Override
public void run() {
System.out.println(" 开始导入:"+path+" 位置的数据");
try {
cyclicBarrier.await();//阻塞
} catch
(InterruptedException e) {
e.printStackTrace();
} catch
(BrokenBarrierException e) {
e.printStackTrace();
}
}
}
CycliBarrierDemo
public class CycliBarrierDemo extends Thread{
@Override
public void run() {
System.out.println("数据准备完毕,开始进行数据分析");
}
public static void main(String[] args) {
CyclicBarrier cycliBarrier=new CyclicBarrier(3,new CycliBarrierDemo());
new Thread(new DataImportThread(cycliBarrier,"file1")).start();
new Thread(new DataImportThread(cycliBarrier,"file2")).start();
new Thread(new DataImportThread(cycliBarrier,"file3")).start();
}
}
注意点
1)对于指定计数值 parties,若由于某种原因,没有足够的线程调用 CyclicBarrier 的 await,则所有调用 await 的线程都会被阻塞;
2)同样的 CyclicBarrier 也可以调用 await(timeout, unit)
,设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作;
3)通过 reset()
重置计数(重置规则),会使得进入 await 的线程出现BrokenBarrierException,线程重新排队;
reset()
方法,重置 barrier 到初始化状态
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
4 ) 如 果 采 用 是 CyclicBarrier(int parties, RunnablebarrierAction) 构造方法,执行 barrierAction 操作的是最后一个到达的线程。
5)CyclicBarrier 与 CountDownLatch 功能很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)的计数器可以重置,重复使用(Cyclic)。
6)内部有一个线程把规则破坏了(接收到中断信号)或者等待await(time)
时间后,会调用#breakBarrier()通知其他线程,其他线程都不按规则来了,不会等待(await())了,则其他所有线程都将抛出 BrokenBarrierException 异常,并将 barrier 置于损坏状态,继续执行。
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
7)Generation 是 CyclicBarrier 内部静态类,描述了 CyclicBarrier 的更新换代。在CyclicBarrier中,同一批线程属于同一代。当有 parties 个线程全部到达 barrier 时,generation 就会被更新换代。其中 broken 属性,标识该当前 CyclicBarrier 是否已经处于中断状态。代码如下:
private static class Generation {
boolean broken = false;
}
默认 barrier 是没有损坏的。
8)nextGeneration()
当所有线程都已经到达 barrier 处(index == 0),则会通过 nextGeneration() 方法,进行更新换代操作。在这个步骤中,做了三件事:
- 唤醒所有线程。
- 重置 count 。
- 重置 generation 。
private void nextGeneration() {
trip.signalAll();
count = parties;
generation = new Generation();
}
实现原理
CyclicBarrier 相比 CountDownLatch 来说,要简单很多,源码实现是基于 ReentrantLock 和 Condition 的组合使用