瞭解join()和CountDownLatch的差異

一、join()

join()是線程對象的成員方法,功能是等待調用join()的線程對象執行完畢,才執行其它的邏輯。
假設我們有三個線程, 主線程,子線程B和C。在主線程中若調用了子線程的join方法,則必須等待子線程執行完畢,纔會繼續執行主線程的其它邏輯。如下是案例:

public class JoinTest {
    public static void main(String [] args) throws InterruptedException {
        ThreadJoinTest t1 = new ThreadJoinTest("子線程B");
        ThreadJoinTest t2 = new ThreadJoinTest("子線程C");
        t1.start();
        t2.start();
        t1.join(); 
        t2.join();
        System.out.println("主線程運行完畢!");
    }
}
class ThreadJoinTest extends Thread{
    public ThreadJoinTest(String name){
        super(name);
    }
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(this.getName() + ":" + i);
        }
    }
}

運行結果如下:

顯然main方法所屬的主線程必須等待t1,t2執行完畢,纔會繼續執行最後的輸出語句。那麼爲什麼t1,t2是交替執行呢?這是因爲調用t1.join()和t2.join()的線程是主線程,所以調用t1.join()時,t2不需要等待,同理調用t2.join()時,t1不需要等待。

再看main方法的另一個版本:

    public static void main(String [] args) throws InterruptedException {
        ThreadJoinTest t1 = new ThreadJoinTest("子線程B");
        ThreadJoinTest t2 = new ThreadJoinTest("子線程C");
        t1.start();
        t1.join(); 
        t2.start();
        t2.join();
        System.out.println("主線程運行完畢!");
    }

控制檯輸出如下,可以看出程序完全是先執行了t1,然後才執行了t2,沒有任何交替。

簡單分析,是因爲t1.start()執行後又執行了t1.join(),此時主線程等待,t2線程還沒有啓動。一直等到t1完成之後,才執行了t2.start()來啓動t2,然後執行t2.join()來讓主線程等待,最後t2執行完畢,主線程完成最後輸出。

二、CountDownLatch

CountDownLatch是一種靈活的閉鎖實現,它維護了一個計數器,通過計數器控制一個或多個線程等待另一組事件發生,具體控制方法是:CountDownLatch調用countDown方法來遞減計數器,表示有一個事件發生了,調用await在計數器爲0之前保持阻塞。

如下是一個簡單的案例:演示了兩個工人,老王和老張,分別做洗碗和洗衣服的工作,每個工人洗碗後通知廚師長自己已經洗碗ok,當所有的碗都洗完了,廚師長開始做菜。我們用CountDownLatch來演示廚師長的等待過程。

public class TestCount {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        Thread worker1 = new Thread(new Worker(latch, (long) (Math.random()*2000+3000)), "老李");
        Thread worker2 = new Thread(new Worker(latch,(long) (Math.random()*2000+3000)), "老張");
        Thread worker3 = new Thread(new HeadChef(latch,(long) (Math.random()*2000+3000)), "廚師長老王");
        worker1.start();
        worker2.start();
        latch.await();
        worker3.start();
    }

    static class Worker implements Runnable {

        private CountDownLatch latch;
        private Long workingTime;


        public Worker(CountDownLatch latch, Long workingTime) {
            this.latch = latch;
            this.workingTime = workingTime;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+":開始洗盤子了");
            try {
                Thread.sleep(workingTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":報告廚師長,盤子已洗完");
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+":開始洗衣服了");
            try {
                Thread.sleep(workingTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":衣服已洗完");
        }
    }

    static class HeadChef extends Worker { //廚師長,也是個打工的,不過更高級點,幹活少
        public HeadChef(CountDownLatch latch, Long workingTime) {
            super(latch, workingTime);
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+":他們把盤子洗完了,那我可以開始做菜了");
        }
    }
}

下面看執行過程:

可以看出廚師長在所有盤子都洗乾淨之後開始做菜了。也就是說,await成功地控制了worker3的執行時機,在worker1和worker2執行洗碗工作完成之後,纔開始執行炒菜工作。

要注意:案例中洗碗和洗衣服分別是不同的兩個事件,而worker1和worker2線程要執行的內容就是執行這兩個事件。worker3線程執行的內容是等待worker1和worker2完成洗碗事件之後,纔開始做菜這個事件。

三、join()和CountDownLatch的區別和聯繫

join()的阻塞原理是不停檢查join()所屬的線程對象是否存活(也就是線程完全執行完畢),如果存活則讓調用join()的線程保持阻塞。

CountDownLatch的阻塞原理是僅僅關注計數器是否爲0,若爲0才保持阻塞,它並不關注持有計數器的其它線程是否完全執行完畢。

顯然join()能控制阻塞的範圍比CountDownLatch小,且沒有CountDownLatch那麼靈活。

我們將第二個案例main改造成使用join()就能看出區別:

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        Thread worker1 = new Thread(new Worker(latch, (long) (Math.random()*2000+3000)), "老李");
        Thread worker2 = new Thread(new Worker(latch,(long) (Math.random()*2000+3000)), "老張");
        Thread worker3 = new Thread(new HeadChef(latch,(long) (Math.random()*2000+3000)), "廚師長老王");
        worker1.start();
        worker2.start();
//        latch.await();
        worker1.join();
        worker2.join();
        worker3.start();
    }

通過join()控制了老張和老李所有工作做完了,老王纔開始工作。顯然這種場景下,老王浪費了很多不必要的時間,作爲一個廚師長,他不需要關注衣服有沒有洗完,而只需要關注盤子洗完了沒有,所以此處使用CountDownLatch纔是合理的。

 

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