关于java:面试官说说CountDownLatchCyclicBarrierSemaphore的原理

31次阅读

共计 6068 个字符,预计需要花费 16 分钟才能阅读完成。

CountDownLatch

CountDownLatch 实用于在多线程的场景须要期待所有子线程全副执行结束之后再做操作的场景。

举个例子,早上部门散会,有人在上厕所,这时候须要期待所有人从厕所回来之后能力开始会议。

public class CountDownLatchTest {
    private static int num = 3;
    private static CountDownLatch countDownLatch = new CountDownLatch(num);
    private static ExecutorService executorService = Executors.newFixedThreadPool(num);
    public static void main(String[] args) throws Exception{executorService.submit(() -> {System.out.println("A 在上厕所");
            try {Thread.sleep(4000);
            } catch (InterruptedException e) {e.printStackTrace();
            }finally {countDownLatch.countDown();
                System.out.println("A 上完了");
            }
        });
        executorService.submit(()->{System.out.println("B 在上厕所");
            try {Thread.sleep(2000);
            } catch (InterruptedException e) {e.printStackTrace();
            }finally {countDownLatch.countDown();
                System.out.println("B 上完了");
            }
        });
        executorService.submit(()->{System.out.println("C 在上厕所");
            try {Thread.sleep(3000);
            } catch (InterruptedException e) {e.printStackTrace();
            }finally {countDownLatch.countDown();
                System.out.println("C 上完了");
            }
        });

        System.out.println("期待所有人从厕所回来散会...");
        countDownLatch.await();
        System.out.println("所有人都好了,开始散会...");
        executorService.shutdown();}
}

代码执行后果:

 A 在上厕所
B 在上厕所
期待所有人从厕所回来散会...
C 在上厕所
B 上完了
C 上完了
A 上完了
所有人都好了,开始散会...

初始化一个 CountDownLatch 实例传参 3,因为咱们有 3 个子线程,每次子线程执行结束之后调用 countDown()办法给计数器 -1,主线程调用 await()办法后会被阻塞,直到最初计数器变为 0,await()办法返回,执行结束。他和 join()办法的区别就是 join 会阻塞子线程直到运行完结,而 CountDownLatch 能够在任何时候让 await()返回,而且用 ExecutorService 没法用 join 了,相比起来,CountDownLatch 更灵便。

CountDownLatch 基于 AQS 实现,volatile 变量 state 维持倒数状态,多线程共享变量可见。

  1. CountDownLatch 通过构造函数初始化传入参数理论为 AQS 的 state 变量赋值,维持计数器倒数状态
  2. 当主线程调用 await()办法时,以后线程会被阻塞,当 state 不为 0 时进入 AQS 阻塞队列期待。
  3. 其余线程调用 countDown()时,state 值原子性递加,当 state 值为 0 的时候,唤醒所有调用 await()办法阻塞的线程

CyclicBarrier

CyclicBarrier 叫做回环屏障,它的作用是 让一组线程全副达到一个状态之后再全副同时执行,而且他有一个特点就是所有线程执行结束之后是能够重用的。

public class CyclicBarrierTest {
    private static int num = 3;
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(num, () -> {System.out.println("所有人都好了,开始散会...");
        System.out.println("-------------------");
    });
    private static ExecutorService executorService = Executors.newFixedThreadPool(num);
    public static void main(String[] args) throws Exception{executorService.submit(() -> {System.out.println("A 在上厕所");
            try {Thread.sleep(4000);
                System.out.println("A 上完了");
                cyclicBarrier.await();
                System.out.println("会议完结,A 退出");
            } catch (Exception e) {e.printStackTrace();
            }finally {}});
        executorService.submit(()->{System.out.println("B 在上厕所");
            try {Thread.sleep(2000);
                System.out.println("B 上完了");
                cyclicBarrier.await();
                System.out.println("会议完结,B 退出");
            } catch (Exception e) {e.printStackTrace();
            }finally {}});
        executorService.submit(()->{System.out.println("C 在上厕所");
            try {Thread.sleep(3000);
                System.out.println("C 上完了");
                cyclicBarrier.await();
                System.out.println("会议完结,C 退出");
            } catch (Exception e) {e.printStackTrace();
            }finally {}});

        executorService.shutdown();}
}

输入后果为:

 A 在上厕所
B 在上厕所
C 在上厕所
B 上完了
C 上完了
A 上完了
所有人都好了,开始散会...
-------------------
会议完结,A 退出
会议完结,B 退出
会议完结,C 退出

从后果来看和 CountDownLatch 十分类似,初始化传入 3 个线程和一个工作,线程调用 await()之后进入阻塞,计数器 -1,当计数器为 0 时,就去执行 CyclicBarrier 中构造函数的工作,当工作执行结束后,唤醒所有阻塞中的线程。这验证了 CyclicBarrier让一组线程全副达到一个状态之后再全副同时执行 的成果。

再举个例子来验证 CyclicBarrier 可重用的成果。

public class CyclicBarrierTest2 {
    private static int num = 3;
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(num, () -> {System.out.println("-------------------");
    });
    private static ExecutorService executorService = Executors.newFixedThreadPool(num);

    public static void main(String[] args) throws Exception {executorService.submit(() -> {System.out.println("A 在上厕所");
            try {Thread.sleep(4000);
                System.out.println("A 上完了");
                cyclicBarrier.await();
                System.out.println("会议完结,A 退出,开始撸代码");
                cyclicBarrier.await();
                System.out.println("C 工作完结,上班回家");
                cyclicBarrier.await();} catch (Exception e) {e.printStackTrace();
            } finally {}});
        executorService.submit(() -> {System.out.println("B 在上厕所");
            try {Thread.sleep(2000);
                System.out.println("B 上完了");
                cyclicBarrier.await();
                System.out.println("会议完结,B 退出,开始摸鱼");
                cyclicBarrier.await();
                System.out.println("B 摸鱼完结,上班回家");
                cyclicBarrier.await();} catch (Exception e) {e.printStackTrace();
            } finally {}});
        executorService.submit(() -> {System.out.println("C 在上厕所");
            try {Thread.sleep(3000);
                System.out.println("C 上完了");
                cyclicBarrier.await();
                System.out.println("会议完结,C 退出,开始摸鱼");
                cyclicBarrier.await();
                System.out.println("C 摸鱼完结,上班回家");
                cyclicBarrier.await();} catch (Exception e) {e.printStackTrace();
            } finally {}});

        executorService.shutdown();}
}

输入后果:

 A 在上厕所
B 在上厕所
C 在上厕所
B 上完了
C 上完了
A 上完了
-------------------
会议完结,A 退出,开始撸代码
会议完结,B 退出,开始摸鱼
会议完结,C 退出,开始摸鱼
-------------------
C 摸鱼完结,上班回家
C 工作完结,上班回家
B 摸鱼完结,上班回家
-------------------

从后果来看,每个子线程调用 await()计数器减为 0 之后才开始持续一起往下执行,会议完结之后一起进入摸鱼状态,最初一天完结一起上班,这就是 可重用

CyclicBarrier 还是基于 AQS 实现的,外部保护 parties 记录总线程数,count 用于计数,最开始 count=parties,调用 await()之后 count 原子递加,当 count 为 0 之后,再次将 parties 赋值给 count,这就是复用的原理。

  1. 当子线程调用 await()办法时,获取独占锁,同时对 count 递加,进入阻塞队列,而后开释锁
  2. 当第一个线程被阻塞同时开释锁之后,其余子线程竞争获取锁,操作同 1
  3. 直到最初 count 为 0,执行 CyclicBarrier 构造函数中的工作,执行结束之后子线程持续向下执行

Semaphore

Semaphore 叫做信号量,和后面两个不同的是,他的计数器是递增的。

public class SemaphoreTest {
    private static int num = 3;
    private static int initNum = 0;
    private static Semaphore semaphore = new Semaphore(initNum);
    private static ExecutorService executorService = Executors.newFixedThreadPool(num);
    public static void main(String[] args) throws Exception{executorService.submit(() -> {System.out.println("A 在上厕所");
            try {Thread.sleep(4000);
                semaphore.release();
                System.out.println("A 上完了");
            } catch (Exception e) {e.printStackTrace();
            }finally {}});
        executorService.submit(()->{System.out.println("B 在上厕所");
            try {Thread.sleep(2000);
                semaphore.release();
                System.out.println("B 上完了");
            } catch (Exception e) {e.printStackTrace();
            }finally {}});
        executorService.submit(()->{System.out.println("C 在上厕所");
            try {Thread.sleep(3000);
                semaphore.release();
                System.out.println("C 上完了");
            } catch (Exception e) {e.printStackTrace();
            }finally {}});

        System.out.println("期待所有人从厕所回来散会...");
        semaphore.acquire(num);
        System.out.println("所有人都好了,开始散会...");

        executorService.shutdown();}
}

输入后果为:

 A 在上厕所
B 在上厕所
期待所有人从厕所回来散会...
C 在上厕所
B 上完了
C 上完了
A 上完了
所有人都好了,开始散会...

略微和前两个有点区别,构造函数传入的初始值为 0,当子线程调用 release()办法时,计数器递增,主线程 acquire()传参为 3 则阐明主线程始终阻塞,直到计数器为 3 才会返回。

Semaphore 还还还是基于 AQS 实现的,同时获取信号量有偏心和非偏心两种策略

  1. 主线程调用 acquire()办法时,用以后信号量值 - 须要获取的值,如果小于 0,则进入同步阻塞队列,大于 0 则通过 CAS 设置以后信号量为残余值,同时返回残余值
  2. 子线程调用 release()给以后信号量值计数器 +1(减少的值数量由传参决定),同时不停的尝试因为调用 acquire()进入阻塞的线程

总结

CountDownLatch 通过计数器提供了比 join 更灵便的多线程管制形式,CyclicBarrier 也能够达到 CountDownLatch 的成果,而且有可复用的特点,Semaphore 则是采纳信号量递增的形式,开始的时候并不需要关注须要同步的线程个数,并且提供获取信号的偏心和非偏心策略。

正文完
 0