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


一個長頭發、穿着清爽的小姐姐,拿着一個嶄新的Mac筆記本向我走來,看着來勢洶洶,我心想着肯定是技術大佬吧!但是我也是一個才華橫溢的人,穩住我們能贏。
面試官

面試官:看你簡歷上有寫熟悉並發編程,CountDownLatch一定用過吧,跟我說說它!

:CountDownLatch是JDK提供的一個同步工具,它可以讓一個或多個線程等待,一直等到其他線程中執行完成一組操作。

面試官:CountDownLatch有哪些常用的方法?

:有countDown方法和await方法,CountDownLatch在初始化時,需要指定用給定一個整數作為計數器。當調用countDown方法時,計數器會被減1;當調用await方法時,如果計數器大於0時,線程會被阻塞,一直到計數器被countDown方法減到0時,線程才會繼續執行。計數器是無法重置的,當計數器被減到0時,調用await方法都會直接返回。

面試官:調用countDown方法時,線程也會阻塞嘛?

:不會的,調用countDown的線程可以繼續執行,不需要等待計數器被減到0,只是調用await方法的線程需要等待。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術干貨。

面試官:可以舉一個使用CountDownLatch的例子嗎?

:比如張三、李四和王五幾個人約好去飯店一起去吃飯,這幾個人都是比較紳士,要等到所有人都到齊以后才讓服務員上菜。這種場景就可以用到CountDownLatch。

面試官:可以寫一下嗎?

:當然可以,這是張三、李四和王五的顧客類:

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();
        }
    }
}

這是服務員類:

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();
        }
    }
}

然后,再寫一個測試類,用於模擬上面所說的場景:

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<Thread> 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秒鍾,可以把服務員類中的

latch.await();

改成:

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實例,具體是這樣的:

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

countDown方法中,只調用了Sync實例的releaseShared方法,具體是這樣的:

public void countDown() {
    sync.releaseShared(1);
}

其中的releaseShared方法,先對計數器進行減1操作,如果減1后的計數器為0,喚醒被await方法阻塞的所有線程,具體是這樣的:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { //對計數器進行減一操作
        doReleaseShared();//如果計數器為0,喚醒被await方法阻塞的所有線程
        return true;
    }
    return false;
}

其中的tryReleaseShared方法,先獲取當前計數器的值,如果計數器為0時,就直接返回;如果不為0時,使用CAS方法對計數器進行減1操作,具體是這樣的:

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方法,具體是這樣的:

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

其中acquireSharedInterruptibly方法,判斷計數器是否為0,如果不為0則阻塞當前線程,具體是這樣的:

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,具體是這樣的:

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

面試官:嗯,很不錯,馬上給你發offer。

微信公眾號:萬貓學社

微信掃描二維碼

獲得更多Java技術干貨


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM