CountDownLatch和WaitGroup

引言

最近开始学习Go语言,前两天看到了Go语言中的WaitGroup,稍微看了一下用法,咋一看这和我平时熟悉的java中的CountDownLatch的用法很像啊。

CountDownLatch

咱先说说啥是CountDownLatch,它是一个同步器,是JAVA并发包下的一个常用的并发工具类,一般使用在一个线程在等待其他几个线程完成后再进行下一步操作时使用的。举个栗子:我们现在在家想吃火锅(广东话:打边炉)。我们是不是要先买肉,买蔬菜,买饮料等等。我们只有买齐了材料才可以围在桌前一起吃火锅。但是如果买这么多材料让我自己一个人去做,那岂不是要很长时间才能搞定。吃火锅肯定不是一个人吃的嘛(这样太寂寞了),所以我们可以让A去买肉,让B去买蔬菜,让C去饮料,我就在家等着他们回来,就可以开锅吃火锅了,哇哈哈哈哈(这个栗子可能举得不是很好,希望能明白我的意思)下面来看看我们的代码,也许你就懂了。

一个人去买所有材料
/**
 * 以下写是三个相似的方法,没有抽成一个方法
 * 主要还是为了模拟实际情况中
 * 我们一般在不用线程中调用不同的业务方法
 */
public static void buyMeat(String who) {
    System.out.println(who + "去买肉了!!");
    try {
        // 模拟耗时操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好肉了!!");
}

public static void buyVegetables(String who) {
    System.out.println(who + "去蔬菜了!!");
    try {
        // 模拟耗时操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好蔬菜了!!");
}

public static void buyDrink(String who) {
    System.out.println(who + "去买饮料了!!");
    try {
        // 因为饮料很重,所以花费的时间比较长
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好饮料了!!");
}

public static void main(String[] args) throws Exception {
    long start = System.currentTimeMillis();
    buyMeat("我");
    buyVegetables("我");
    buyDrink("我");
    System.out.println("耗时:" + (System.currentTimeMillis() - start) + "毫秒");

    System.out.println("材料都买好了!");
    System.out.println("开始吃火锅!");
}

输出结果:

我去买肉了!!
我买好肉了!!
我去蔬菜了!!
我买好蔬菜了!!
我去买饮料了!!
我买好饮料了!!
耗时:4002毫秒
材料都买好了!
开始吃火锅!

从上面可以发现,如果一个人去完成所有的事情是要很长时间,程序是顺序执行的,所以总耗时是执行所有方法耗时的和(所有买材料方法的耗时)。如果我们可以多个人去购买材料,这样耗时就可以大幅度减少。但是有个问题就是,我们怎么确定材料都买好了呢,这就要用到同步器,买好了材料都告诉同步器,我买好了材料。我工作中比较常用的同步器是CountDownLatch,当然还有其他的同步器比如说:Semaphore,Barrier等等

多个人去买材料(多线程操作):
/**
 * 创建一个有三个线程的线程池
 */
private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(3);

/**
 * 以下写上个相识的方法
 * 主要还是为了模拟实际情况中
 * 我们一般在不用线程中调用不同的业务方法
 */
public static void buyMeat(String who) {
    System.out.println(who + "去买肉了!!");
    try {
        // 模拟耗时操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好肉了!!");
}

public static void buyVegetables(String who) {
    System.out.println(who + "去蔬菜了!!");
    try {
        // 模拟耗时操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好蔬菜了!!");
}

public static void buyDrink(String who) {
    System.out.println(who + "去买饮料了!!");
    try {
        // 因为饮料很重,所以花费的时间比较长
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(who + "买好饮料了!!");
}

public static void main(String[] args) throws Exception {
    long start = System.currentTimeMillis();
    final CountDownLatch countDownLatch = new CountDownLatch(3);

    //安排A B C各自去买东西
    EXECUTOR_SERVICE.execute(new Runnable() {
        public void run() {
            buyMeat("A");
            countDownLatch.countDown();
        }
    });

    EXECUTOR_SERVICE.execute(new Runnable() {
        public void run() {
            buyVegetables("B");
            countDownLatch.countDown();
        }
    });

    EXECUTOR_SERVICE.execute(new Runnable() {
        public void run() {
            buyDrink("C");
            countDownLatch.countDown();
        }
    });

    //我在家等着
    countDownLatch.await();

    System.out.println("耗时:" + (System.currentTimeMillis() - start) + "毫秒");

    System.out.println("材料都买好了!");
    System.out.println("开始吃火锅!");
}

输出结果:

A去买肉了!!
B去蔬菜了!!
C去买饮料了!!
B买好蔬菜了!!
A买好肉了!!
C买好饮料了!!
耗时:2005毫秒
材料都买好了!
开始吃火锅!

可以看到这次的耗时减少差不多2000毫秒,其实整个操作下来的主要耗时在C买饮料的操作上,买饮料的操作耗时需要2000毫秒,其他买肉买蔬菜的操作都是1000毫秒。看到这里你应该能看懂CountDownLatch大概是干嘛用的,大概是怎么用的了吧。

使用CountDownLatch需要注意的点

1、使用CountDownLatch的时候,记得在每个线程调用完成业务方法之后要调用CountDownLatch的countDown()方法。
CountDownLatch内部是通过一个计数器来实现的,每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务。
计数器的初始值就是new CountDownLatch(x)时传入的x值,x应设置为你线程调用的数量,每个线程完成一个操作后应该调用countDown()方法,进行计数器的减1操作。
在实际工作当中,根据业务场景,我一般会把countDown()方法写在finally中,因为即便是业务方法调用出现异常,也能正常的countDown,这样不会使得CountDownLatch一直在await()等待。
2、为了避免CountDownLatch一直在await()等待,我在工作中一般不会直接使用await()方法,一般使用其重载的方法
await(long timeout, TimeUnit unit),第一个参数是等待的时长,第二个参数是时间单位,比如说:等待5秒 countDownLatch.await(5, TimeUnit.SECONDS);

/*
* @param timeout the maximum time to wait
* @param unit the time unit of the {@code timeout} argument
* @return {@code true} if the count reached zero and {@code false}
*         if the waiting time elapsed before the count reached zero
* @throws InterruptedException if the current thread is interrupted
*         while waiting
*/
public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

以上就是我平时CountDownLatch的一些总结,如果那些地方有错的,还希望各位大神指正指正。谢谢啦!

WaitGroup

WaitGroup是我在学习Go语言时在学到Channel的时候看到的,WaitGroup给我的第一感觉就是它和CountDownLatch十分的相识。它是等待各个goruntine协程完成操作后,再进行下一步操作。
还是用买火锅材料的例子来看一下Go的实现的代码。这次直接演示A B C都去买材料的情况

package main

import (
	"fmt"
	"sync"
	"time"
)

func buyMeat(who string) {
	fmt.Println(who, "去买肉了!!!")
	//睡眠一秒
	time.Sleep(time.Second)
	fmt.Println(who, "买好肉了!!!")
}

func buyVegetables(who string) {
	fmt.Println(who, "去蔬菜了!!!")
	//睡眠一秒
	time.Sleep(time.Second)
	fmt.Println(who, "买好蔬菜了!!!")
}

func buyDrink(who string) {
	fmt.Println(who, "去买饮料了!!!")
	//睡眠两秒
	time.Sleep(2 * time.Second)
	fmt.Println(who, "买好饮料了!!!")
}

func main() {
	start := time.Now().UnixNano() / 1e6

	wg := sync.WaitGroup{}
	wg.Add(3)

	//安排A B C去买材料
	go func() {
		buyMeat("A")
		wg.Done()
	}()

	go func() {
		buyVegetables("B")
		wg.Done()
	}()

	go func() {
		buyDrink("C")
		wg.Done()
	}()

	wg.Wait()
	end := time.Now().UnixNano() / 1e6
	fmt.Println("耗时:", end-start, "毫秒")
	fmt.Println("材料买好了!!!")
	fmt.Println("吃火锅啦!!!")
}

输出结果:

C 去买饮料了!!!
A 去买肉了!!!
B 去蔬菜了!!!
A 买好肉了!!!
B 买好蔬菜了!!!
C 买好饮料了!!!
耗时: 2001 毫秒
材料买好了!!!
吃火锅啦!!!

来来来,即便你没学习过Go,你是否也会感觉WaitGroup和CountDownLatch的用法是不是很像。我觉得是很像的,感觉JAVA程序猿上手这个WaitGroup还是比较容易的。
WaitGroup通过设定计数器,每个写个goruntine协程在退出前进行Done()操作递减1次,直到为0时解除阻塞。(CountDownLatch上面也说到也是进行计数器减1操作,有没有很像(っ•̀ω•́)っ✎⁾⁾)

WaitGroup注意的点

1、要保证每次操作Done的WaitGroup对象是同一个对象
2、WaitGroup.Add实现了院子操作,但是还是建议在goruntine外进行累加计数器,以避免Add操作未执行,Wait就已经退出了

总结

最近在学习Go语言,一边学习一边对比着JAVA,感觉整个过程也是挺有趣的,因为接触Go时间不长现在还在一个初步学习的阶段,以上文章中哪里有误的,希望大家可以指正我一下,相互学习,谢谢!

你可能感兴趣的:(JAVA,Golang)