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時間不長現在還在一個初步學習的階段,以上文章中哪裏有誤的,希望大家可以指正我一下,相互學習,謝謝!
ps:最近用Hexo搭建了個個人博客,來捧捧場唄點我點我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章