【Java并发编程实战】 5.5.1章节 闭锁 CountDownLatch 实现

1. 什么是闭锁

闭锁(latch)是一种 Synchronizer,他可以延迟线程的进度直到线程到达终止状态。

一个闭锁工作起来就像一道大门:直到闭锁达到终点状态之前,门一直是关闭的,没有线程通过,在终点状态到来的时候,门开了,允许所有线程都通过。一旦闭锁到达了终点状态,他就不能够在改变状态了,所以它会永远保持敞开的状态。

2. 闭锁的应用场景

  • 确保一个计算不会执行,直到它需要的资源被初始化。

  • 确保一个服务不会开始,直到它依赖的服务都已经开始。

  • 等待直到活动的所有部分都为继续处理做好准备。比如王者荣耀需要等待所有玩家准备才能开始

    Jmeter模拟高并发也是这个场景,如果我想测试10个线程同时工作对cpu的影响,那么如果线程执行的快慢程度不一样,可能第10个线程刚创建,第一个线程就执行结束了,此时就只有9个线程在同时执行,和我预期不一致,此时,就可以用CountDownLatch 控制,在下文5.2复杂例子,就有这样的例子!

3. 闭锁的实现

CountDownLatch是一个同步辅助类,存在于java.util.concurrent包下,灵活的实现了闭锁,它允许一个或多个线程等待一个事件集的发生。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数的值就会减1。当计数器值到达0时,它所表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

4. CountDownLatch原理

CountDownLatch构造函数:

CountDownLatch(int count);

构造器中计数值(count)就是闭锁需要等待的线程数量,这个值只能被设置一次。

CountDownLatch类的方法:

  • void await():使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。

  • boolean await(long timeout, TimeUnit unit):使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。

    其实就是 void await(),额外多了一个超时时间,避免某个因素导致锁长久未归零,程序可以提前做出处理。

  • void countDown():递减锁存器的计数,如果计数到达零,则释放所有等待的线程。

  • long getCount():返回当前计数。

  • String toString():返回标识此锁存器及其状态的字符串。

常规用法是这样:线程A调用锁B的await()方法后,线程A会等待锁B归零,期间线程A会一直阻塞,如果锁B一直未归零,线程A也会一直阻塞下去;如果用await(timeout)的话,超时后,会自行唤醒,继续完成后续流程。

同样的,如果其他的N个线程也调用锁B的await()方法后象,这些线程会和线程A一样,都会阻塞,直至锁B归零。

5. 使用案例

5.1 入门案例

我们前面介绍了CountDownLatch 的使用场景,那就是等待资源就绪。

假设你是一个厨师,你有2个助手,你要做一道荤菜,那么需要肉和调味品,你可以吩咐2个助手分别去买肉和买盐,等到2个助手都回来后,你才能点火做饭,在此期间,你就一直在等待。如果某个助手先到了,你也不能做任何事情,继续等待另一个的到来。

买肉任务:

package com.test;

import java.util.concurrent.CountDownLatch;

/**
 * 买肉任务
 */
public class BuyMeatTask implements Runnable {
    private CountDownLatch countDownLatch;

    public BuyMeatTask(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    public void run() {
        try {
            System.out.println("出门去买肉了");
            Thread.sleep(2000);
            System.out.println("肉买来了!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (countDownLatch != null)
                countDownLatch.countDown();
        }
    }

}

买盐的任务:

package com.test;

import java.util.concurrent.CountDownLatch;

/**
 * 买盐的任务
 */
public class BuySaltTask implements Runnable {

    private CountDownLatch countDownLatch;

    public BuySaltTask(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    public void run() {
        try {
            System.out.println("出门买盐了");
            Thread.sleep(5000);
            System.out.println("盐买回来了!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (countDownLatch != null)
                countDownLatch.countDown();
        }
    }
}

执行类:

package com.test;

import java.util.concurrent.CountDownLatch;

/**
 * 我是厨师,等待配菜和调味品
 */
public class CountDownLaunchRunner {

    static int sub = 0;
    static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(new BuyMeatTask(countDownLatch)).start();
        new Thread(new BuySaltTask(countDownLatch)).start();

        // 等待线程池中的2个任务执行完毕,否则一直等待
        countDownLatch.await();
        System.out.println("准备点火炒菜");
    }

}

执行结果:

出门去买肉了
出门买盐了
肉买来了!
盐买回来了!
准备点火炒菜

当子线程调用countDownLatch.countDown()满足为0时,会发通知消息,唤醒阻塞的主线程:
在这里插入图片描述

5.2 复杂案例

我们使用书上原文的例子,我们想统计所有子线程的运行时间,由于子线程运行时间有长有短,比如子线程A花费10s,子线程B花费2S,那么结果应该是最长的那个,即10s,因此,我们希望在最后一个线程(耗时最长的那个)执行完毕后,由主线程统计时间。

package net.jcip.examples;

import java.util.Date;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class TestHarness {
    public long timeTasks(int nThreads, final Runnable task) throws InterruptedException {
        // 开始锁,当其值变为0时,表示等待的线程可以运行
        final CountDownLatch startGate = new CountDownLatch(1);
        // 结束锁,当其值变为0时,表示最后一个线程也执行完了,也就是说所有的线程都执行完了
        final CountDownLatch endGate = new CountDownLatch(nThreads);

        for (int i = 0; i < nThreads; i++) {
            // 模拟不同线程的间隔一定的时间才创建好
            TimeUnit.SECONDS.sleep(1);

            Thread t = new Thread() {
                public void run() {
                    try {
                        System.out.println(new Date() + " " + Thread.currentThread().getName()
                                + " started");
                        startGate.await(); // <注1>每个子线程立即进入等待,直至开始锁释放,才一起运行
                        try {
                            task.run();
                        } finally {
                            endGate.countDown(); // <注2>每个子线程运行结束,通知结束锁计数器减一
                        }
                    } catch (InterruptedException ignored) {
                    }
                }
            };
            t.start(); // 启动子线程
        }

        long start = System.nanoTime(); // 开始的时间

        // 为了让结果更直观,主线程增加休眠,避免间隔太短,瞬时打印日志IO顺序不符合预期
        // 确保所有的线程都达到阻塞点,再去唤醒它们
        Thread.sleep(1000);

        startGate.countDown(); // 开始锁减一,计数器归零,通知所有的子线程(卡在<注1>)处的)可以执行了
        endGate.await(); // 主线程进入等待,直至最后一个线程运行结束(<注2>处发出通知),才会被唤醒,继续执行
        long end = System.nanoTime();
        return end - start; // 统计时间
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable task = new Runnable() {

            @Override
            public void run() {
                // 模拟线程耗时不同
                int counter = new Random().nextInt(100);
                System.out.println(new Date() + " " + Thread.currentThread().getName()
                        + " will run " + counter + " times");
                for (int i = 0; i < counter; i++) {
                    try {
                        Thread.sleep(100); // 模拟耗时处理
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(new Date() + " " + Thread.currentThread().getName()
                        + " finished");
            }
        };

        TestHarness test = new TestHarness();
        long time = test.timeTasks(3, task);
        System.out.println("total time is : " + time);
    }
}

执行结果,每次内容随机,但存在特定规律:

Fri Feb 12 17:03:05 CST 2021 Thread-0 started
Fri Feb 12 17:03:06 CST 2021 Thread-1 started
Fri Feb 12 17:03:07 CST 2021 Thread-2 started
Fri Feb 12 17:03:08 CST 2021 Thread-2 will run 88 times
Fri Feb 12 17:03:08 CST 2021 Thread-0 will run 71 times
Fri Feb 12 17:03:08 CST 2021 Thread-1 will run 39 times
Fri Feb 12 17:03:12 CST 2021 Thread-1 finished
Fri Feb 12 17:03:15 CST 2021 Thread-0 finished
Fri Feb 12 17:03:17 CST 2021 Thread-2 finished
total time is : 9853746100

在这里插入图片描述

分析:通过结果的注释,基本上就能看到规则了,主线程确实是在所有子线程结束后,才统计的运行时间。

我们之前列举了CountDownLatch 的3个应用场景,这个例子里面有2个锁,覆盖了其中的2种:

  • 等待直到活动的所有部分都为继续处理做好准备
    startGate锁保证所有的子线程都准备就绪后,才开始;
  • 确保一个计算不会执行,直到它需要的资源被初始化
    endGate锁保证主线程依赖的所有子线程都完成后,才开始 ;对于主线程来说,子线程完成就是它依赖资源。

参考:【Java并发编程三】闭锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值