2-2-3-4-3、AQS之Semaphore&CountDownLatch&CyclicBarrier详解


Semaphore

Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的
Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量

在这里插入图片描述

PV操作是操作系统一种实现进程互斥与同步的有效方法。PV操作与信号量(S)的处理相关,P表示通过的意思,V表示释放的意思。用PV操作来管理共享资源时,首先要确保PV操作自身执行的正确性。
P操作的主要动作是:
①S减1
②若S减1后仍大于或等于0,则进程继续执行
③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度
V操作的主要动作是:
①S加1
②若相加后结果大于0,则进程继续执行
③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度

使用

构造器

在这里插入图片描述

  • permits 表示许可证的数量(资源数)
  • fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

方法

  • acquire() 表示阻塞并获取许可
  • tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞
  • release() 表示释放许可
  • int availablePermits():返回此信号量中当前可用的许可证数
  • int getQueueLength():返回正在等待获取许可证的线程数
  • boolean hasQueuedThreads():是否有线程正在等待获取许可证
  • void reducePermit(int reduction):减少 reduction 个许可证
  • Collection getQueuedThreads():返回所有等待获取许可证的线程集合

应用场景

限流

用于做流量控制,特别是公用资源有限的应用场景

@Slf4j
public class SemaphoreTest2 {
    /**
     * 实现一个同时只能处理5个请求的限流器
     */
    private static final Semaphore semaphore = new Semaphore(5);
    /**
     * 定义一个线程池
     */
    private static final ThreadPoolExecutor executor = new ThreadPoolExecutor
            (10, 50, 60,
                    TimeUnit.SECONDS, new LinkedBlockingDeque<>(200));
    /**
     * 模拟执行方法
     */
    public static void exec() {
        try {
            //占用1个资源
            semaphore.acquire(1);
            //TODO  模拟业务执行
            log.info("执行exec方法");
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放一个资源
            semaphore.release(1);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            Thread.sleep(100);
            // 模拟请求以10个/s的速度
            executor.execute(SemaphoreTest2::exec);
        }
    }
}

运行结果
同一时间,只会有五个线程在执行

12:22:38.087 [pool-1-thread-1] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:38.197 [pool-1-thread-2] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:38.301 [pool-1-thread-3] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:38.411 [pool-1-thread-4] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:38.523 [pool-1-thread-5] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:40.092 [pool-1-thread-6] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:40.200 [pool-1-thread-7] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:40.307 [pool-1-thread-8] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:40.415 [pool-1-thread-4] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:40.538 [pool-1-thread-9] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:42.098 [pool-1-thread-10] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:42.206 [pool-1-thread-1] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:42.314 [pool-1-thread-2] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:42.418 [pool-1-thread-4] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法
12:22:42.540 [pool-1-thread-3] INFO com.example.demo.seven_two_six.concurrent.lock.SemaphoreTest2 - 执行exec方法

Semaphore源码分析

关注点

  • Semaphore的加锁解锁(共享锁)逻辑实现
  • 线程竞争锁失败入队阻塞逻辑和获取锁的线程释放锁唤醒阻塞线程竞争锁的逻辑实现

CountDownLatch

CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集
CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置。如果你需要一个重置count的版本,那么请考虑使用CyclicBarrier

在这里插入图片描述

使用

构造器

在这里插入图片描述

常用方法

  • await():调用 await() 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
  • await(long timeout, TimeUnit unit):和 await() 类似,若等待 timeout 时长后,count 值还是没有变为 0,不再等待,继续执行
  • countDown():会将 count 减 1,直至为 0

应用场景

CountDownLatch一般用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成
两种使用场景:

  • 让多个线程等待:运动员准备就位听到发令枪后进行赛跑
  • 让单个线程等待:多线程统计不同维度的数据,统计完成,单线程汇总

多个线程等待示例

@Slf4j
public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                   log.info("准备完成");
                    countDownLatch.await();
                   log.info("开始赛跑");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        Thread.sleep(2000);// 裁判准备发令
        countDownLatch.countDown();// 发令枪:执行发令
    }
}

运行结果

12:35:01.108 [Thread-4] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 准备完成
12:35:01.107 [Thread-1] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 准备完成
12:35:01.107 [Thread-0] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 准备完成
12:35:01.108 [Thread-2] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 准备完成
12:35:01.108 [Thread-3] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 准备完成
12:35:03.112 [Thread-1] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 开始赛跑
12:35:03.112 [Thread-3] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 开始赛跑
12:35:03.112 [Thread-0] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 开始赛跑
12:35:03.112 [Thread-2] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 开始赛跑
12:35:03.112 [Thread-4] INFO com.example.demo.seven_two_six.concurrent.lock.CountDownLatchTest - 开始赛跑

单个线程等待示例

public class CountDownLatchTest2 {
    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            final int index = i;
            new Thread(() -> {
                try {
                    Thread.sleep(1000 +
                            ThreadLocalRandom.current().nextInt(1000));
                    System.out.println(Thread.currentThread().getName()
                            + " finish task" + index);

                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        // 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
        countDownLatch.await();
        System.out.println("主线程:在所有任务运行完成后,进行结果汇总");
    }
}

运行结果

Thread-1 finish task1
Thread-4 finish task4
Thread-0 finish task0
Thread-3 finish task3
Thread-2 finish task2
主线程:在所有任务运行完成后,进行结果汇总

实现原理

底层基于 AbstractQueuedSynchronizer 实现,CountDownLatch 构造函数中指定的count直接赋给AQS的state;每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;这一步是由最后一个执行countdown方法的线程执行的
而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程

对比

CountDownLatch与Thread.join的区别

  • CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API
  • CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作
  • 而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活

CountDownLatch与CyclicBarrier的区别

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

  1. CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
  2. CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、isBroken(用来知道阻塞的线程是否被中断)等方法
  3. CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程
  4. CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执行。CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行
  5. CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果
  6. CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现

CyclicBarrier

字面意思回环栅栏(循环屏障),通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用

在这里插入图片描述

使用

构造方法

  • public CyclicBarrier(int parties)

parties表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞

  • public CyclicBarrier(int parties, Runnable barrierAction)

用于在线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景(该线程的执行时机是在到达屏障之后再执行)

重要方法

指定数量的线程全部调用await()方法时,这些线程不再阻塞
通过reset()方法可以进行重置计数器

  • await()
  • await(long timeout, TimeUnit unit)
  • reset()

应用场景

CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的场景

示例

public class CyclicBarrierTest2 {
    //保存每个学生的平均成绩
    private final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

    private final ExecutorService threadPool = Executors.newFixedThreadPool(3);

    private final CyclicBarrier cb = new CyclicBarrier(3, () -> {
        int result = 0;
        Set<String> set = map.keySet();
        for (String s : set) {
            result += map.get(s);
        }
        System.out.println("三人平均成绩为:" + (result / 3) + "分");
    });
    public void count() {
        for (int i = 0; i < 3; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    //获取学生平均成绩
                    int score = (int) (Math.random() * 40 + 60);
                    map.put(Thread.currentThread().getName(), score);
                    System.out.println(Thread.currentThread().getName() + "同学的平均成绩为:" + score);
                    try {
                        //执行完运行await(),等待所有学生平均成绩都计算完毕
                        cb.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    public static void main(String[] args) {
        CyclicBarrierTest2 cb = new CyclicBarrierTest2();
        cb.count();
    }
}

运行结果:

pool-1-thread-1同学的平均成绩为:80
pool-1-thread-3同学的平均成绩为:78
pool-1-thread-2同学的平均成绩为:82
三人平均成绩为:80分

利用CyclicBarrier的计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景

示例

@Slf4j
public class CyclicBarrierTest3 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger counter = new AtomicInteger();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1000, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100), (r) -> new Thread(r, counter.addAndGet(1) + " 号 "), new ThreadPoolExecutor.AbortPolicy());
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("裁判:比赛开始~~"));
        for (int i = 0; i < 10; i++) {
            threadPoolExecutor.submit(new Runner(cyclicBarrier));
        }
        TimeUnit.SECONDS.sleep(2);
        threadPoolExecutor.shutdown();
    }
    static class Runner extends Thread {
        private final CyclicBarrier cyclicBarrier;
        public Runner(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            try {
                int sleepMills = ThreadLocalRandom.current().nextInt(1000);
                Thread.sleep(sleepMills);
                System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms" + cyclicBarrier.getNumberWaiting());
                cyclicBarrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果

1 号 选手已就位, 准备共用时: 57ms0
3 号 选手已就位, 准备共用时: 273ms1
4 号 选手已就位, 准备共用时: 290ms2
2 号 选手已就位, 准备共用时: 830ms3
5 号 选手已就位, 准备共用时: 991ms4
裁判:比赛开始~~
3 号 选手已就位, 准备共用时: 267ms0
2 号 选手已就位, 准备共用时: 605ms1
4 号 选手已就位, 准备共用时: 706ms2
1 号 选手已就位, 准备共用时: 788ms3
5 号 选手已就位, 准备共用时: 906ms4
裁判:比赛开始~~

源码分析

CyclicBarrier等待流程图

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值