1. 程式人生 > >騰訊面試居然跟我扯了半小時的CountDownLatch

騰訊面試居然跟我扯了半小時的CountDownLatch

一個長頭髮、穿著清爽的小姐姐,拿著一個嶄新的Mac筆記本向我走來,看著來勢洶洶,我心想著肯定是技術大佬吧!但是我也是一個才華橫溢的人,穩住我們能贏。 ![面試官](https://img-blog.csdnimg.cn/20200509090547451.jpeg#pic_center) > **面試官**:看你簡歷上有寫熟悉併發程式設計,CountDownLatch一定用過吧,跟我說說它! **我**:CountDownLatch是JDK提供的一個同步工具,它可以讓一個或多個執行緒等待,一直等到其他執行緒中執行完成一組操作。 > **面試官**:CountDownLatch有哪些常用的方法? **我**:有`countDown`方法和`await`方法,CountDownLatch在初始化時,需要指定用給定一個整數作為計數器。當呼叫`countDown`方法時,計數器會被減1;當呼叫`await`方法時,如果計數器大於0時,執行緒會被阻塞,一直到計數器被`countDown`方法減到0時,執行緒才會繼續執行。計數器是無法重置的,當計數器被減到0時,呼叫`await`方法都會直接返回。 > **面試官**:呼叫`countDown`方法時,執行緒也會阻塞嘛? **我**:不會的,呼叫`countDown`的執行緒可以繼續執行,不需要等待計數器被減到0,只是呼叫await方法的執行緒需要等待。 歡迎關注微信公眾號:萬貓學社
,每週一分享Java技術乾貨。 > **面試官**:可以舉一個使用CountDownLatch的例子嗎? **我**:比如張三、李四和王五幾個人約好去飯店一起去吃飯,這幾個人都是比較紳士,要等到所有人都到齊以後才讓服務員上菜。這種場景就可以用到CountDownLatch。 > **面試官**:可以寫一下嗎? **我**:當然可以,這是張三、李四和王五的顧客類: ```java package onemore.study; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random; import java.util.concurrent.CountDownLatch; public class Customer implements Runnable { private CountDownLatch latch; private String name; public Customer(CountDownLatch latch, String name) { this.latch = latch; this.name = name; } @Override public void run() { try { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); Random random = new Random(); System.out.println(sdf.format(new Date()) + " " + name + "出發去飯店"); Thread.sleep((long) (random.nextDouble() * 3000) + 1000); System.out.println(sdf.format(new Date()) + " " + name + "到了飯店"); latch.countDown(); } catch (Exception e) { e.printStackTrace(); } } } ``` 這是服務員類: ```java package onemore.study; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.CountDownLatch; public class Waitress implements Runnable { private CountDownLatch latch; private String name; public Waitress(CountDownLatch latch, String name) { this.latch = latch; this.name = name; } @Override public void run() { try { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); System.out.println(sdf.format(new Date()) + " " + name + "等待顧客"); latch.await(); System.out.println(sdf.format(new Date()) + " " + name + "開始上菜"); } catch (Exception e) { e.printStackTrace(); } } } ``` 然後,再寫一個測試類,用於模擬上面所說的場景: ```java package onemore.study; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; public class CountDownLatchTester { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); List threads = new ArrayList<>(3); threads.add(new Thread(new Customer(latch, "張三"))); threads.add(new Thread(new Customer(latch, "李四"))); threads.add(new Thread(new Customer(latch, "王五"))); for (Thread thread : threads) { thread.start(); } Thread.sleep(100); new Thread(new Waitress(latch, "♥小芳♥")).start(); } } ``` 執行以後的結果應該是這樣的: ``` 15:25:53.015 王五出發去飯店 15:25:53.015 李四出發去飯店 15:25:53.015 張三出發去飯店 15:25:53.062 ♥小芳♥等待顧客 15:25:54.341 張三到了飯店 15:25:54.358 李四到了飯店 15:25:56.784 王五到了飯店 15:25:56.784 ♥小芳♥開始上菜 ``` 可以看到,服務員小芳在呼叫`await`方法時一直阻塞著,一直等到三個顧客都呼叫了`countDown`方法才繼續執行。 歡迎關注微信公眾號:萬貓學社
,每週一分享Java技術乾貨。 > **面試官**:如果有一個顧客遲遲沒到,飯店都打樣了,也不能一直等啊,應該這麼辦? **我**:可以使用`await`方法的另一個過載,傳入等待的超時時間,比如服務員只等3秒鐘,可以把服務員類中的 ```java latch.await(); ``` 改成: ```java latch.await(3, TimeUnit.SECONDS); ``` 執行結果可能是這樣的: ``` 17:24:40.915 張三出發去飯店 17:24:40.915 李四出發去飯店 17:24:40.915 王五出發去飯店 17:24:40.948 ♥小芳♥等待顧客 17:24:43.376 李四到了飯店 17:24:43.544 王五到了飯店 17:24:43.951 ♥小芳♥開始上菜 17:24:44.762 張三到了飯店 ``` 可以看到,服務員小芳在呼叫`await`方法時雖然被阻塞了,但是時間超過3秒後,沒等顧客張三呼叫`countDown`方法就繼續執行開始上菜了。 歡迎關注微信公眾號:萬貓學社
,每週一分享Java技術乾貨。 > **面試官**:CountDownLatch的實現原理是什麼? **我**:CountDownLatch有一個內部類叫做Sync,它繼承了AbstractQueuedSynchronizer類,其中維護了一個整數`state`,並且保證了修改`state`的可見性和原子性。 建立CountDownLatch例項時,也會建立一個Sync的例項,同時把計數器的值傳給Sync例項,具體是這樣的: ```java public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); } ``` 在 `countDown`方法中,只調用了Sync例項的`releaseShared`方法,具體是這樣的: ```java public void countDown() { sync.releaseShared(1); } ``` 其中的`releaseShared`方法,先對計數器進行減1操作,如果減1後的計數器為0,喚醒被await方法阻塞的所有執行緒,具體是這樣的: ```java public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { //對計數器進行減一操作 doReleaseShared();//如果計數器為0,喚醒被await方法阻塞的所有執行緒 return true; } return false; } ``` 其中的`tryReleaseShared`方法,先獲取當前計數器的值,如果計數器為0時,就直接返回;如果不為0時,使用CAS方法對計數器進行減1操作,具體是這樣的: ```java protected boolean tryReleaseShared(int releases) { for (;;) {//死迴圈,如果CAS操作失敗就會不斷繼續嘗試。 int c = getState();//獲取當前計數器的值。 if (c == 0)// 計數器為0時,就直接返回。 return false; int nextc = c-1; if (compareAndSetState(c, nextc))// 使用CAS方法對計數器進行減1操作 return nextc == 0;//如果操作成功,返回計數器是否為0 } } ``` 在`await`方法中,只調用了Sync例項的`acquireSharedInterruptibly`方法,具體是這樣的: ```java public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } ``` 其中`acquireSharedInterruptibly`方法,判斷計數器是否為0,如果不為0則阻塞當前執行緒,具體是這樣的: ```java public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0)//判斷計數器是否為0 doAcquireSharedInterruptibly(arg);//如果不為0則阻塞當前執行緒 } ``` 其中`tryAcquireShared`方法,是AbstractQueuedSynchronizer中的一個模板方法,其具體實現在Sync類中,其主要是判斷計數器是否為零,如果為零則返回1,如果不為零則返回-1,具體是這樣的: ```java protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } ``` >**面試官**:嗯,很不錯,馬上給你發offer。

微信公眾號:萬貓學社

微信掃描二維碼

獲得更多Java技術乾貨