JUC-倒计时锁

一、CountDownLatch

用来进行线程同步协作,等待所有线程完成倒计时。其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一。

1.1 基本使用

@Slf4j
public class CountDownLatchSample {

    public static void doTask(ExecutorService pool, CountDownLatch latch, int nSeconds) {
        pool.submit(() -> {
            log.debug("begin...");
            sleep(nSeconds);
            // 计数减一。
            latch.countDown();
            log.debug("end...{}", latch.getCount());
        });
    }

    public static void waitEnd(ExecutorService pool, CountDownLatch latch) {
        pool.submit(() -> {
            try {
                log.debug("waiting...");
                // 等待计数归零。
                latch.await();
                log.debug("wait end");
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            }
        });
    }

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(3);
        ExecutorService pool = Executors.newFixedThreadPool(4);

        // 模拟三个线程执行任务。
        doTask(pool, latch, 1);
        doTask(pool, latch, 2);
        doTask(pool, latch, 3);

        // 模拟一个线程等待任务执行结束。
        waitEnd(pool, latch);
        pool.shutdown();
        // begin...
        // waiting...
        // begin...
        // begin...
        // end...2
        // end...1
        // end...0
        // wait end
    }
}

1.2 应用之同步等待多线程准备完毕

需求:使用 CountdownLatch 模拟十位玩家等待加载完成后,游戏开始。

@Slf4j
public class SyncWaitSample {
    
    public static void main(String[] args) throws InterruptedException {
        // 原子整数用于给线程命名。
        AtomicInteger num = new AtomicInteger(0);
        ExecutorService pool = Executors.newFixedThreadPool(
                10,
                r -> new Thread(r, "t_" + num.getAndIncrement())
        );

        CountDownLatch latch = new CountDownLatch(10);
        String[] all = new String[10];
        SecureRandom random = new SecureRandom();
        for (int i = 0; i < 10; i++) {
            int x = i;
            pool.submit(() -> {
                // 从 0% 加载 到 100%。
                for (int j = 0; j <= 100; j++) {
                    // 模拟网络加载的等待时间。
                    sleep(random.nextInt(2), TimeUnit.SECONDS);
                    all[x] = Thread.currentThread().getName() + "(" + (j + "%") + ")";
                    System.out.print("\r" + Arrays.toString(all));
                }
                latch.countDown();
            });
        }
        latch.await();
        System.out.println("\n游戏开始...");
        pool.shutdown();

        // 开始:[t_0(8%), t_1(8%), t_2(15%), t_3(27%), t_4(18%), t_5(12%), t_6(17%), t_7(12%), t_8(23%), t_9(10%)]
        // 中间:[t_0(75%), t_1(58%), t_2(69%), t_3(78%), t_4(68%), t_5(62%), t_6(68%), t_7(77%), t_8(84%), t_9(62%)]
        // 结束:[t_0(100%), t_1(100%), t_2(100%), t_3(100%), t_4(100%), t_5(100%), t_6(100%), t_7(100%), t_8(100%), t_9(100%)]
        // 游戏开始...
    }
}

1.3 应用之同步等待多个远程调用结束

需求:模拟调用不同微服务,并等待执行结束。

  • Controller
@RestController
public class TestCountDownLatchController {

    @GetMapping("/order/{id}")
    public Map<String, Object> order(@PathVariable int id) {
        Map<String, Object> map = new HashMap<>();
        map.put("id", id);
        map.put("total", "2300.00");
        sleep(2, TimeUnit.SECONDS);
        return map;
    }

    @GetMapping("/product/{id}")
    public Map<String, Object> product(@PathVariable int id) {
        Map<String, Object> map = new HashMap<>();
        if (id == 1) {
            map.put("name", "小爱音箱");
            map.put("price", 300);
        } else if (id == 2) {
            map.put("name", "小米手机");
            map.put("price", 2000);
        }
        map.put("id", id);
        sleep(1, TimeUnit.SECONDS);
        return map;
    }

    @GetMapping("/logistics/{id}")
    public Map<String, Object> logistics(@PathVariable int id) {
        Map<String, Object> map = new HashMap<>();
        map.put("id", id);
        map.put("name", "中通快递");
        sleep(3, TimeUnit.SECONDS);
        return map;
    }
}
  • Tests
@SpringBootTest
@Slf4j
class DemoApplicationTests {

    /**
     * 模拟不使用任何手段,直接进行远程调用。
     */
    @Test
    void mockRemoteCall() {
        RestTemplate restTemplate = new RestTemplate();
        log.info("begin");
        Instant start = Instant.now();
        log.info(restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1) + "");
        log.info(restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1) + "");
        log.info(restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2) + "");
        log.info(restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1) + "");
        Instant end = Instant.now();
        long cost = Duration.between(start, end).toMillis();
        log.info("end... cost=[{}]ms", cost);
        // begin
        // {total=2300.00, id=1}
        // {price=300, name=小爱音箱, id=1}
        // {price=2000, name=小米手机, id=2}
        // {name=中通快递, id=1}
        // end... cost=[7132]ms
    }

    /**
     * 使用 CountDownLatch 模拟等待远程调用结束。
     *
     * @throws InterruptedException 中断异常
     */
    @Test
    void mockRemoteCallByCountDownLatch() throws InterruptedException {
        RestTemplate restTemplate = new RestTemplate();
        log.info("begin");
        Instant start = Instant.now();
        CountDownLatch latch = new CountDownLatch(4);
        ExecutorService pool = Executors.newCachedThreadPool();
        pool.submit(() -> {
            log.info(restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1) + "");
            latch.countDown();
        });

        pool.submit(() -> {
            log.info(restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1) + "");
            latch.countDown();
        });

        pool.submit(() -> {
            log.info(restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2) + "");
            latch.countDown();
        });

        pool.submit(() -> {
            log.info(restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1) + "");
            latch.countDown();
        });

        latch.await();
        Instant end = Instant.now();
        long cost = Duration.between(start, end).toMillis();
        log.info("end... cost=[{}]ms", cost);
        pool.shutdown();
        // [           main] begin
        // [pool-1-thread-2] {price=300, name=小爱音箱, id=1}
        // [pool-1-thread-3] {price=2000, name=小米手机, id=2}
        // [pool-1-thread-1] {total=2300.00, id=1}
        // [pool-1-thread-4] {name=中通快递, id=1}
        // end... cost=[3098]ms
    }

    /**
     * 在有返回值的情况下,使用 Future 模拟等待远程调用结束。
     *
     * @throws ExecutionException   执行异常
     * @throws InterruptedException 中断异常
     */
    @Test
    @SuppressWarnings("unchecked")
    void mockRemoteCallByFuture() throws ExecutionException, InterruptedException {
        RestTemplate restTemplate = new RestTemplate();
        log.info("begin");
        Instant start = Instant.now();
        ExecutorService pool = Executors.newCachedThreadPool();
        Future<Map<String, Object>> f1 = pool.submit(() ->
                restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1));

        Future<Map<String, Object>> f2 = pool.submit(() ->
                restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1));

        Future<Map<String, Object>> f3 = pool.submit(() ->
                restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2));

        Future<Map<String, Object>> f4 = pool.submit(() ->
                restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1));

        log.info(String.valueOf(f1.get()));
        log.info(String.valueOf(f2.get()));
        log.info(String.valueOf(f3.get()));
        log.info(String.valueOf(f4.get()));

        Instant end = Instant.now();
        long cost = Duration.between(start, end).toMillis();
        log.info("end... cost=[{}]ms", cost);
        pool.shutdown();
        // begin
        // {total=2300.00, id=1}
        // {price=300, name=小爱音箱, id=1}
        // {price=2000, name=小米手机, id=2}
        // {name=中通快递, id=1}
        // end... cost=[3102]ms
    }
}

二、CyclicBarrier

[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行。

2.1 场景引出

v1需求:主线程需要等待其它线程执行任务结束,才开始执行后面的逻辑。

  • 代码示例(使用 CountDownLatch 实现)
    @Test
    public void test01() {
        ExecutorService pool = Executors.newFixedThreadPool(5);
        CountDownLatch latch = new CountDownLatch(2);

        pool.submit(() -> {
            log.debug("task1 begin...");
            sleep(1, TimeUnit.SECONDS);
            latch.countDown();
        });

        pool.submit(() -> {
            log.debug("task2 begin...");
            sleep(2, TimeUnit.SECONDS);
            latch.countDown();
        });

        try {
            latch.await();
            log.debug("task1 and task2 finish.");
             pool.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 21:04.627 [pool-1-thread-1] task1 begin...
        // 21:04.627 [pool-1-thread-2] task2 begin...
        // 21:06.634 [main] task1 and task2 finish.
    }

v2需求:现在需要反复执行3次上面的操作。

  • 代码示例(使用 CountDownLatch 实现)
    @Test
    public void test02() {
        ExecutorService pool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 3; i++) {
            CountDownLatch latch = new CountDownLatch(2);
            pool.submit(() -> {
                log.debug("task1 begin...");
                sleep(1, TimeUnit.SECONDS);
                latch.countDown();
            });

            pool.submit(() -> {
                log.debug("task2 begin...");
                sleep(2, TimeUnit.SECONDS);
                latch.countDown();
            });

            try {
                latch.await();
                log.debug("task1 and task2 finish.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        pool.shutdown();
        // 27:28.277 [pool-1-thread-1] task1 begin...
        // 27:28.277 [pool-1-thread-2] task2 begin...
        // 27:30.289 [main] task1 and task2 finish. 
        // * 3 ...
    }
  • 问题说明:我们会很自然想到 CountDownLatch 放到循环体中去创建并执行,但是这会存在一个问题,那就是倒计时锁对象在这个过程中被反复的创建了。基于这一点,我们接下来使用 CyclicBarrier 来进行优化。

  • 代码示例(使用 CyclicBarrier 实现)

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(5);
        // 设置屏障拦截的线程数量。
        CyclicBarrier barrier = new CyclicBarrier(2, () -> log.debug("task1, task2 finish..."));

        for (int i = 0; i < 3; i++) { // task1  task2  task1
            pool.submit(() -> {
                log.debug("task1 begin...");
                sleep(1, TimeUnit.SECONDS);
                try {
                    barrier.await(); // 2-1=1
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
            pool.submit(() -> {
                log.debug("task2 begin...");
                sleep(2, TimeUnit.SECONDS);
                try {
                    // `await()`:已经到达了屏障,则当前线程会被阻塞。
                    barrier.await(); // 1-1=0
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
        pool.shutdown();
        // 41:53.609 [pool-1-thread-1] task1 begin...
        // 41:53.609 [pool-1-thread-3] task2 begin...
        // 41:53.609 [pool-1-thread-1] task1 and task2 finish.
        // * 3 ...
    }

2.2 CyclicBarrier与CountDownLatch的区别

  • CountDownLatch 的计数器只能使用一次,而 CyclicBarrier 的计数器可以使用 reset() 方法重置,可以使用多次,所以 CyclicBarrier 能够处理更为复杂的场景;
  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行;
  • 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待
  • CountDownLatch计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

三、结束语


“-------怕什么真理无穷,进一寸有一寸的欢喜。”

微信公众号搜索:饺子泡牛奶

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值