JAVA多線程-CountDownLatch計數器

一、概述

CountDownLatch是一個同步工具類,它允許一個或多個線程等待其他線程執行完操作之后再繼續執行。通常用于控制多個線程的執行順序。

二、基本原理

我們可以把CountDownLatch看成是一個計數器,其內部維護著一個count計數,計數器的初始化值為需要控制的線程的數量,比如需要控制幾個線程順序執行我們就初始化傳入幾,之后每當其中一個線程完成了自己的任務后,就調用countDown()來使計數器減1;而在調用者線程中需要調用await()方法使得當前調用者線程一直處于阻塞狀態,直至當計數器到達0時,就表明其他所有的線程都已經完成了任務,然后處于阻塞狀態的調用者線程才可以繼續往下執行。

三、應用場景

假如有這樣一個需求,我們當前有一個任務,然后我們把這個任務進行分解成多個步驟完成,這個時候我們可以考慮使用多線程,每個線程完成一個步驟,等到所有的步驟都完成之后,程序提示任務完成。

對于這個需求,通常我們要實現主線程等待所有線程完成任務之后才可繼續操作,最為簡單的做法是直接使用join()方法,代碼如下:

package com.feizi.java.concurrency.tool;

import java.util.concurrent.TimeUnit;

/**
 * Created by feizi on 2018/5/30.
 */
public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("step one has finished...");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("step two has finished...");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("all thread has finished...");
    }
}

控制臺輸出結果:

step one has finished...
step two has finished...
all thread has finished...

Process finished with exit code 0

join()方法主要用于讓當前執行線程等到join線程執行結束才可繼續執行。其實現原理就是不停地去檢查join線程是否處于存活狀態while (isAlive()),如果join線程存活則讓當前線程永遠wait,我們可以看下源碼,wait(0)表示永遠等待下去。
join()的源碼:

public final void join() throws InterruptedException {
    join(0);
}

繼續跟進去:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

我們可以看到,重點是這一句,如果存活,就調用wait(0),使得永遠等待。

while (isAlive()) {
    wait(0);
}

直到join線程中止后,線程的this.notifyAll()方法才會被調用,調用notifyAll是在JVM里實現的,所以JDK里看不到。

而在JDK1.5之后的并發包中提供的CountDownLatch也可以實現join的這個功能,并且比join的功能更多。

四、CountDownLatch使用

4.1、例子1-CountDownLatchTest.java類:

package com.feizi.java.concurrency.tool;

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

/**
 * Created by feizi on 2018/5/30.
 */
public class CountDownLatchTest {
    private static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //步驟一完成
                System.out.println("step one has finished...");
                try {
                    //模擬步驟一耗時操作
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //步驟一完成,調用countDown()方法,計數器就減1
                latch.countDown();

                //步驟二完成
                System.out.println("step two has finished...");
                try {
                    //模擬步驟二耗時操作
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //步驟二完成,調用countDown()方法,計數器就減1
                latch.countDown();
            }
        }).start();

        //步驟一和步驟二完成之前會阻塞住
        latch.await();

        //直到所有的步驟都完成,主線程才繼續執行
        System.out.println("all steps have finished...");
    }
}

控制臺輸出結果:

step one has finished...
step two has finished...
all steps have finished...

Process finished with exit code 0

4.2、例子2-CountDownLatchTest2.java類:

package com.feizi.java.concurrency.tool;

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

/**
 * Created by feizi on 2018/5/30.
 */
public class CountDownLatchTest2 {
    private static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("step one has finished...");
                    TimeUnit.SECONDS.sleep(1);

                    //計數器減一
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("step two has finished...");

                    //計數器減一
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();
        //計數器清零之前,阻塞住當前線程
        latch.await();

        System.out.println("all steps have finished...");
    }
}

控制臺輸出結果:

step one has finished...
step two has finished...
all steps have finished...

Process finished with exit code 0

4.3、例子3-CountDownLatchTest3.java類

package com.feizi.java.concurrency.tool;

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

/**
 * Created by feizi on 2018/5/30.
 */
public class CountDownLatchTest3 {

    public static void main(String[] args) throws InterruptedException {
        /*線程計數器*/
        CountDownLatch latch = new CountDownLatch(2);
        Thread t1 = new Thread(new StepOneThread(latch));
        Thread t2 = new Thread(new StepTwoThread(latch));

        t1.start();
        t2.start();

        //調用await()阻塞當前線程,直至計數器清零才可繼續執行
        latch.await();
        TimeUnit.SECONDS.sleep(2);
        System.out.println("all steps has finished...");
    }
}

/**
 * 步驟一線程
 */
class StepOneThread implements Runnable{

    private CountDownLatch latch;

    public StepOneThread(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        try {
            System.out.println("step one has finished...");
            //模擬步驟一耗時操作
            TimeUnit.SECONDS.sleep(2);
            //步驟一完成計數器減一
            latch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 步驟二線程
 */
class StepTwoThread implements Runnable{

    private CountDownLatch latch;

    public StepTwoThread(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        try {
            //模擬步驟二耗時操作
            TimeUnit.SECONDS.sleep(2);
            System.out.println("step two has finished...");
            //步驟二完成計數器減一
            latch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

控制臺輸出結果:

step one has finished...
step two has finished...
all steps has finished...

Process finished with exit code 0

通過上述簡單的例子,我們可以看到CountDownLatch的構造函數接收一個int類型的參數作為計數器count,如果你想等待n個任務完成后再執行,那么這里就直接傳入n即可。

當我們調用一次CountDownLatch的countDown()方法時,計數器count便會減1,而CountDownLatch的await()方法則會一直阻塞住當前線程,直至計數器count變為0。

最佳實踐:上面所說的n個任務,可以是n個線程(上述例子2和例子3),也可以是1個線程里的n個執行步驟(上述例子1),需要注意的是在運用于多個線程時,我們只需要把這個CountDownLatch的引用傳遞到線程里即可。

五、其他方法

最后需要注意的是,如果有某個任務特別耗時,而我們又不可能讓調用者線程(比如主線程)一直等待下去,那么就可以指定等待的時間,比如這個方法await(long time, TimeUnit unit):此方法在等待指定的時間之后不會再阻塞在當前線程,另外join也有類似的方法。

六、注意

當我們初始化一個CountDownLatch時將其計數器初始化為0,則在調用await()方法時不會阻塞當前線程。比如:

CountDownLatch latch = new CountDownLatch(0);

原文參考

  1. http://www.lxweimin.com/p/4b6fbdf5a08f
  2. http://ifeve.com/talk-concurrency-countdownlatch/
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 線程池ThreadPoolExecutor corepoolsize:核心池的大小,默認情況下,在創建了線程池之后...
    irckwk1閱讀 756評論 0 0
  • 一、多線程 說明下線程的狀態 java中的線程一共有 5 種狀態。 NEW:這種情況指的是,通過 New 關鍵字創...
    Java旅行者閱讀 4,729評論 0 44
  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內存運行時,即變成一個進程.進程是處于運行過程中...
    小徐andorid閱讀 2,832評論 3 53
  • 正如大多數人一樣從小到大一直都是別人家的乖乖女 ,成績不差不好,樣貌不好不差,從小在外婆家長大,直到農村沒有好一點...
    難又難ML閱讀 178評論 1 2
  • 2017-7-28(九十八) 感恩 —— 做施與受的冥想2/21,祈愿愛人工作順利,并回向給所有為工作煩惱的有緣眾...
    慢慢花開閱讀 304評論 0 0