1. 程式人生 > >countDownLatch和join的區別(轉載)

countDownLatch和join的區別(轉載)

首先,我們來看一個應用場景1:

假設一條流水線上有三個工作者:worker0,worker1,worker2。有一個任務的完成需要他們三者協作完成,worker2可以開始這個任務的前提是worker0和worker1完成了他們的工作,而worker0和worker1是可以並行他們各自的工作的。

如果我們要編碼模擬上面的場景的話,我們大概很容易就會想到可以用join來做。當在當前執行緒中呼叫某個執行緒 thread 的 join() 方法時,當前執行緒就會阻塞,直到thread 執行完成,當前執行緒才可以繼續往下執行。補充下:join的工作原理是,不停檢查thread是否存活,如果存活則讓當前執行緒永遠wait,直到thread執行緒終止,執行緒的this.notifyAll 就會被呼叫

我們首先用join來模擬這個場景:

Worker類如下: 


package com.concurrent.test3;  
  
/** 
 * 工作者類 
 * @author ThinkPad 
 * 
 */  
public class Worker extends Thread {  
  
    //工作者名  
    private String name;  
    //工作時間  
    private long time;  
      
    public Worker(String name, long time) {  
        this.name = name;  
        this.time = time;  
    }  
      
    @Override  
    public void run() {  
        // TODO 自動生成的方法存根  
        try {  
            System.out.println(name+"開始工作");  
            Thread.sleep(time);  
            System.out.println(name+"工作完成,耗費時間="+time);  
        } catch (InterruptedException e) {  
            // TODO 自動生成的 catch 塊  
            e.printStackTrace();  
        }     
    }  
}  

Test類如下:



package com.concurrent.test3;  
  
  
public class Test {  
  
    public static void main(String[] args) throws InterruptedException {  
        // TODO 自動生成的方法存根  
  
        Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000));  
        Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000));  
        Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000));  
          
        worker0.start();  
        worker1.start();  
          
        worker0.join();  
        worker1.join();  
        System.out.println("準備工作就緒");  
          
        worker2.start();          
    }  
}  

執行test,觀察控制檯輸出的順序,我們發現這樣可以滿足需求,worker2確實是等worker0和worker1完成之後才開始工作的:

worker1開始工作 worker0開始工作 worker1工作完成,耗費時間=3947 worker0工作完成,耗費時間=4738 準備工作就緒 worker2開始工作 worker2工作完成,耗費時間=4513

除了用join外,用CountDownLatch 也可以完成這個需求。需要對worker做一點修改,我把它放在另一個包下:

Worker:  



package com.concurrent.test4;  
  
import java.util.concurrent.CountDownLatch;  
  
/** 
 * 工作者類 
 * @author ThinkPad 
 * 
 */  
public class Worker extends Thread {  
  
    //工作者名  
    private String name;  
    //工作時間  
    private long time;  
      
    private CountDownLatch countDownLatch;  
      
    public Worker(String name, long time, CountDownLatch countDownLatch) {  
        this.name = name;  
        this.time = time;  
        this.countDownLatch = countDownLatch;  
    }  
      
    @Override  
    public void run() {  
        // TODO 自動生成的方法存根  
        try {  
            System.out.println(name+"開始工作");  
            Thread.sleep(time);  
            System.out.println(name+"工作完成,耗費時間="+time);  
            countDownLatch.countDown();  
            System.out.println("countDownLatch.getCount()="+countDownLatch.getCount());  
        } catch (InterruptedException e) {  
            // TODO 自動生成的 catch 塊  
            e.printStackTrace();  
        }     
    }  
}  

Test:



package com.concurrent.test4;  
  
import java.util.concurrent.CountDownLatch;  
  
  
public class Test {  
  
    public static void main(String[] args) throws InterruptedException {  
        // TODO 自動生成的方法存根  
  
        CountDownLatch countDownLatch = new CountDownLatch(2);  
        Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);  
        Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);  
        Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);  
          
        worker0.start();  
        worker1.start();  
          
        countDownLatch.await();  
        System.out.println("準備工作就緒");  
        worker2.start();          
    }  
}  

我們建立了一個計數器為2的 CountDownLatch ,讓Worker持有這個CountDownLatch 例項,當完成自己的工作後,呼叫countDownLatch.countDown() 方法將計數器減1。countDownLatch.await() 方法會一直阻塞直到計數器為0,主執行緒才會繼續往下執行。觀察執行結果,發現這樣也是可以的:

worker1開始工作 worker0開始工作 worker0工作完成,耗費時間=3174 countDownLatch.getCount()=1 worker1工作完成,耗費時間=3870 countDownLatch.getCount()=0 準備工作就緒 worker2開始工作 worker2工作完成,耗費時間=3992 countDownLatch.getCount()=0

那麼既然如此,CountDownLatch與join的區別在哪裡呢?事實上在這裡我們只要考慮另一種場景,就可以很清楚地看到它們的不同了。

應用場景2:

假設worker的工作可以分為兩個階段,work2 只需要等待work0和work1完成他們各自工作的第一個階段之後就可以開始自己的工作了,而不是場景1中的必須等待work0和work1把他們的工作全部完成之後才能開始。

試想下,在這種情況下,join是沒辦法實現這個場景的,而CountDownLatch卻可以,因為它持有一個計數器,只要計數器為0,那麼主執行緒就可以結束阻塞往下執行。我們可以在worker0和worker1完成第一階段工作之後就把計數器減1即可,這樣worker0和worker1在完成第一階段工作之後,worker2就可以開始工作了。

worker:



package com.concurrent.test5;  
  
import java.util.concurrent.CountDownLatch;  
  
/** 
 * 工作者類 
 * @author ThinkPad 
 * 
 */  
public class Worker extends Thread {  
  
    //工作者名  
    private String name;  
    //第一階段工作時間  
    private long time;  
      
    private CountDownLatch countDownLatch;  
      
    public Worker(String name, long time, CountDownLatch countDownLatch) {  
        this.name = name;  
        this.time = time;  
        this.countDownLatch = countDownLatch;  
    }  
      
    @Override  
    public void run() {  
        // TODO 自動生成的方法存根  
        try {  
            System.out.println(name+"開始工作");  
            Thread.sleep(time);  
            System.out.println(name+"第一階段工作完成");  
              
            countDownLatch.countDown();  
              
            Thread.sleep(2000); //這裡就姑且假設第二階段工作都是要2秒完成  
            System.out.println(name+"第二階段工作完成");  
            System.out.println(name+"工作完成,耗費時間="+(time+2000));  
              
        } catch (InterruptedException e) {  
            // TODO 自動生成的 catch 塊  
            e.printStackTrace();  
        }     
    }  
}  

Test:



package com.concurrent.test5;  
  
import java.util.concurrent.CountDownLatch;  
  
  
public class Test {  
  
    public static void main(String[] args) throws InterruptedException {  
        // TODO 自動生成的方法存根  
  
        CountDownLatch countDownLatch = new CountDownLatch(2);  
        Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);  
        Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);  
        Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);  
          
        worker0.start();  
        worker1.start();      
        countDownLatch.await();  
          
        System.out.println("準備工作就緒");  
        worker2.start();  
          
    }  
  
}  

觀察控制檯列印順序,可以發現這種方法是可以模擬場景2的:

worker0開始工作 worker1開始工作 worker1第一階段工作完成 worker0第一階段工作完成 準備工作就緒 worker2開始工作 worker1第二階段工作完成 worker1工作完成,耗費時間=5521 worker0第二階段工作完成 worker0工作完成,耗費時間=6147 worker2第一階段工作完成 worker2第二階段工作完成 worker2工作完成,耗費時間=5384

最後,總結下CountDownLatch與join的區別:呼叫thread.join() 方法必須等thread 執行完畢,當前執行緒才能繼續往下執行,而CountDownLatch通過計數器提供了更靈活的控制,只要檢測到計數器為0當前執行緒就可以往下執行而不用管相應的thread是否執行完畢。