Java concurrent包并发工具——CountDownLatch用法实践

参考:http://blog.csdn.net/qq_30739519/article/details/51350527

很多系统在启动运行之前都会有一个称为"健康检查"的阶段,比如会去检查数据库、网络、虚拟机等基础组件服务的状态是否正常,当确认这些服务都正常之后,才会继续做其他的启动工作(否则地基不牢,后面的事情做了也是白搭)。这种场景简而言之就是,后面的任务对于前面的一个或多个任务有前后依赖关系,只有当前面的任务完成了,后面的任务才能开始,而这样的场景正好可以由CountDownLatch这样一个并发辅助工具来完成。

CountDownLatch,按字面意思理解,就是:计数闩(shuan),这个就是门闩的意思,可以理解为一个开关,通过一个计数开关来控制并发任务的执行。该类位于java.util.concurrent.CountDownLatch,从JDK 1.5版本引入。

本文就以一个简单的健康检查场景为例子,来理一理CountDownLatch这样一个并发工具的用法。

场景简要描述

假设有一个系统,在系统启动之前需要先检查数据库、网络、虚拟机三个组件服务是否正常,如果都正常,才继续启动;否则启动失败。

可以用一个图示来表示:


创建一个健康检查任务基类

每个健康检查任务都是跑在子线程中的,所以要实现Runnable接口:

package com.countdownlatch;

import java.util.concurrent.CountDownLatch;

/**
 * 系统健康检查任务基类
 * 
 * @author Administrator
 *
 */
public abstract class BaseHealthCheckTask implements Runnable {

    // 同步计数闩对象
    private CountDownLatch latch;

    // 要进行健康检查的服务名称
    protected String serviceName;

    // 健康检查是否完成
    protected boolean checkOk;

    public BaseHealthCheckTask(CountDownLatch latch, String serviceName) {
        this.latch = latch;
        this.serviceName = serviceName;
        // checkOk初始化为false,等正常完成健康检查操作之后赋值true
        this.checkOk = false;
    }

    public String getServiceName() {
        return serviceName;
    }

    public boolean isCheckOk() {
        return checkOk;
    }

    @Override
    public void run() {
        try {
            // 执行具体的健康检查工作
            doCheck();
            // 检查成功,给检查成功标志对象赋值true
            this.checkOk = true;
        } catch (Exception e) {
            // 如果执行健康检查任务过程中发生异常,不再给checkOk赋值true,表示健康检查失败
            e.printStackTrace();
        }

        // 健康检查完成之后(不管成功还是失败),将同步计数闩的计数值减1
        this.latch.countDown();
    }

    /**
     * 需要由子类实现的抽象方法,用于做具体的健康检查工作
     */
    protected abstract void doCheck() throws Exception;
}

创建不同服务的健康检查任务子类

  • DBHealthCheckTask
    假设数据库健康检查任务需要耗时3秒完成:
package com.countdownlatch;

import java.util.concurrent.CountDownLatch;

/**
 * 数据库服务健康检查任务类
 * 
 * @author Administrator
 *
 */
public class DBHealthCheckTask extends BaseHealthCheckTask {

    public DBHealthCheckTask(CountDownLatch latch) {
        super(latch, "DBService");
    }

    @Override
    public void doCheck() throws InterruptedException {
        System.out
                .println("start to check: " + this.serviceName + " health...");
        // sleep 3秒,模拟执行数据库健康检查任务
        Thread.sleep(3000);
        System.out.println("finish to check: " + this.serviceName + " health!");
    }

}

  • NetworkHealthCheckTask
    假设网络健康检查任务需要耗时5秒完成:
package com.countdownlatch;

import java.util.concurrent.CountDownLatch;

/**
 * 网络服务健康检查任务类
 * 
 * @author Administrator
 *
 */
public class NetworkHealthCheckTask extends BaseHealthCheckTask {

    public NetworkHealthCheckTask(CountDownLatch latch) {
        super(latch, "NetworkService");
    }

    @Override
    public void doCheck() throws InterruptedException {
        System.out
                .println("start to check: " + this.serviceName + " health...");
        // sleep 5秒,模拟执行网络健康检查任务
        Thread.sleep(5000);
        System.out.println("finish to check: " + this.serviceName + " health!");
    }

}

  • VmHealthCheckTask
    假设虚拟机健康检查任务需要耗时4秒完成:
package com.countdownlatch;

import java.util.concurrent.CountDownLatch;

/**
 * 虚拟机服务健康检查任务类
 * 
 * @author Administrator
 *
 */
public class VmHealthCheckTask extends BaseHealthCheckTask {

    public VmHealthCheckTask(CountDownLatch latch) {
        super(latch, "VmService");
    }

    @Override
    public void doCheck() throws InterruptedException {
        System.out
                .println("start to check: " + this.serviceName + " health...");
        // sleep 4秒,模拟执行虚拟机健康检查任务
        Thread.sleep(4000);
        System.out.println("finish to check: " + this.serviceName + " health!");
    }

}

创建一个系统启动器类

用单例模式创建一个系统启动器,在启动器主线程中,先启动并等待3个健康检查任务的完成,才能接着进行其他启动工作。如果有健康检查任务的检查结果是失败的,那么终止启动主线程。

package com.countdownlatch;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 系统启动器
 * 
 * @author Administrator
 *
 */
public class SystemStarter {
    // 单例化,因为系统启动器对象全局只能有一个
    private static SystemStarter INSTANCE = new SystemStarter();

    // 同步计数闩对象
    private CountDownLatch latch;

    // 设置默认的同步计数闩所允许的线程数为3
    private static final int LATCH_COUNT = 3;

    // 默认的超时等待描述
    private static final int DEFAULT_LATCH_TIMEOUT_SECOND = 60;

    private SystemStarter() {
        // 初始化启动器的同步闩对象
        this.latch = new CountDownLatch(LATCH_COUNT);
    }

    public static SystemStarter getInstance() {
        return INSTANCE;
    }

    public void startUp() throws InterruptedException {
        // 用于函数计时
        long start = System.currentTimeMillis();

        // 执行器对象,用于执行3个健康检查线程任务
        Executor executor = Executors.newFixedThreadPool(LATCH_COUNT);
        List<BaseHealthCheckTask> tasks = new ArrayList<BaseHealthCheckTask>();
        // 使用同步计数闩对象初始化3个健康检查任务对象:数据库、网络和虚拟机健康检查
        tasks.add(new DBHealthCheckTask(this.latch));
        tasks.add(new NetworkHealthCheckTask(this.latch));
        tasks.add(new VmHealthCheckTask(this.latch));

        // 并发执行健康检查任务
        for (BaseHealthCheckTask task : tasks) {
            executor.execute(task);
        }

        // 同步计数闩阻塞主线程进行等待,直到上面的3个健康检查任务全部执行完成,或超过默认超时时间才继续往下执行主线程
        this.latch.await(DEFAULT_LATCH_TIMEOUT_SECOND, TimeUnit.SECONDS);

        System.out.println("check system health FINISH!");

        // 输出每个健康检查任务的检查结果(是否检查通过)
        for (BaseHealthCheckTask task : tasks) {
            System.out.println("health check result (is passed): "
                    + task.getServiceName() + " - " + task.isCheckOk());
            // 一旦有一个任务失败,显示系统启动失败,退出启动器
            if (!task.isCheckOk()) {
                System.out.println("start up the system FAILED! "
                        + task.getServiceName() + " is NOT OK!");
                return;
            }
        }

        System.out.println("-----");

        // 在全部健康检查完成后,才开始执行启动过程中的其他任务
        doOtherStartupWork();

        System.out.println("-----");

        long end = System.currentTimeMillis();
        System.out.println("start up the system FINISH! totally spent "
                + (end - start) + "ms.");
    }

    /**
     * 其他任务,依赖于所有健康检查完成之后,才能执行这些任务
     * 
     * @throws InterruptedException
     */
    private void doOtherStartupWork() throws InterruptedException {
        System.out.println("do some other works...");
        Thread.sleep(1500);
    }
}

在main方法中调用启动器启动系统

package com.countdownlatch;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SystemStarter startup = SystemStarter.getInstance();
        startup.startUp();
    }
}

执行结果:

start to check: DBService health...
start to check: VmService health...
start to check: NetworkService health...
finish to check: DBService health!
finish to check: VmService health!
finish to check: NetworkService health!
check system health FINISH!
health check result (is passed): DBService - true
health check result (is passed): NetworkService - true
health check result (is passed): VmService - true
-----
do some other works...
-----
start up the system FINISH! totally spent 6520ms.

可见启动器最终耗时为3个健康检查任务中最长耗时的任务的耗时(网络健康检查的5秒)+做其他启动工作的耗时(1.5秒),即6.5秒。

附:CountDownLatch类方法梳理

CountDownLatch类位于java.util.concurrent.CountDownLatch,是JDK并发工具包concurrent中的一个辅助工具类。

  • 查看CountDownLatch类的源码,可以看到有1个成员变量sync、1个带参构造方法和5个方法。

  • 该类的核心功能由一个Sync类型的成员变量sync实现,Sync是一个静态内部类,继承自AbstractQueuedSynchronizer类。

  • await()方法会让调用该方法的当前线程阻塞等待,直到latch的计数值变为0,或者有线程抛出中断异常。下面是方法的文档说明:

Causes the current thread to wait until the latch has counted down to zero, unless the thread is {@linkplain Thread#interrupt interrupted}.
  • await(long timeout, TimeUnit unit)方法是和await()方法类似的一个方法,只不过可以设置一个阻塞等待的超时时间,当超过该超时时间后,会结束阻塞等待(不管子线程有没有执行完),接着往下执行。下面是方法的文档说明:
Causes the current thread to wait until the latch has counted down to zero, unless the thread is {@linkplain Thread#interrupt interrupted}, or the specified waiting time elapses.
  • countDown()方法由子线程调用,会对latch的计数值减1,直到减到0为止。下面是方法的文档说明:
Decrements the count of the latch, releasing all waiting threads if the count reaches zero.
  • getCount()方法用于获取当前latch的计数值,下面是方法的文档说明:
Returns the current count.
This method is typically used for debugging and testing purposes.
  • toString()方法会打印出当前latch的计数值:
Returns a string identifying this latch, as well as its state.
The state, in brackets, includes the String {@code "Count ="}
followed by the current count.
  • 最后看一看该类的作者:


Doug Lea正是java.util.concurrent包的作者,是一位对Java影响力深远的人,可以看一看百度百科的介绍:Doug Lea

本文源码地址

我的GitHub

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341

推荐阅读更多精彩内容