Java併發程式設計(二)如何保證執行緒同時/交替執行

六層樓發表於2021-05-17

第一篇文章中,我用如何保證執行緒順序執行的例子作為Java併發系列的開胃菜。本篇我們依然不會有原始碼分析,而是用另外兩個多執行緒的例子來引出Java.util.concurrent中的幾個併發工具的用法。

系列文章

Java併發程式設計(一)如何保證執行緒順序執行 - 簡書 (jianshu.com)

一、如何保證多個執行緒同時執行

保證多個執行緒同時執行,指的是多個執行緒在同一時間開始執行內部run()方法。

經過第一篇的學習,你應該能理解到,讓執行緒能按我們的意志來執行其實是需要用一些手段(訊號量、併發工具、執行緒池等)來實現的。常用的併發工具一般有CountDownLatch、CyclicBarrier、Semaphore,這些工具在多執行緒程式設計中必不可少。我們先看看如何用併發工具保證執行緒同時執行吧。

1. 使用CountDownLatch實現

關於CountDownLatch,count down的字面意思是倒數,latch是上鎖的意思。所以CountDownLatch的意思就是倒數關門。我們看看JDK8 API中是如何解釋的:

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

大概意思是,CountDownLatch是一種同步輔助工具,允許一個或多個執行緒等待一組在其他執行緒中執行的操作完成之後再執行。

public class SimultaneouslyExample {
    static CountDownLatch countDownLatch=new CountDownLatch(3);

    public static void foo(String name) {
        System.out.println("執行緒名:"+name+",開始時間:"+System.nanoTime());
        try {
            countDownLatch.await();
            //2.每次減一
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException{
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(300);
        countDownLatch.countDown();

    }
}

輸出結果:
執行緒名:A,開始時間:449768159780400
執行緒名:C,開始時間:449768159785200
執行緒名:B,開始時間:449768159795300

看到輸出結果,你可能會懷疑。明明A執行緒慢了4800納秒啊,這不是同步的。其實大可不必覺得奇怪,納秒級的時間即使是JVM也沒辦法那麼精準的把控,不過根據我的測試。這裡的同步實現邏輯能保證毫秒級的精確性。

2. 使用CyclicBarrier實現

另一種實現方式CyclicBarrier,根據字面意思我可以看到這個是一個可迴圈屏障。CyclicBarrier可以讓一個或多個執行緒到達一個屏障點之後再開始執行。

話不多說,我們直接看看程式碼中如何寫:

public class CyclicBarrierExample{
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(3);

    public static void foo(String name) {
        System.out.println("執行緒名:"+name+",開始時間:"+System.currentTimeMillis());
        try {
            cyclicBarrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException{
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(300);

    }
}

輸出結果:

執行緒名:A,開始時間:1621232496385
執行緒名:B,開始時間:1621232496385
執行緒名:C,開始時間:1621232496385

二、如何保證多個執行緒交替執行

保證多個執行緒交替執行,指的是多個執行緒可以按照一定的次序開始執行內部run()方法。這裡我們需要使用Semaphore併發工具來實現。如何你的大學課程學習過作業系統的話,那麼你一定對訊號量機制很熟悉
Semaphore(訊號量):是一種計數器,用來保護一個或者多個共享資源的訪問。如果執行緒要訪問一個資源就必須先獲得訊號量。如果訊號量內部計數器大於0,訊號量減1,然後允許共享這個資源;否則,如果訊號量的計數器等於0,訊號量將會把執行緒置入休眠直至計數器大於0.當訊號量使用完時,必須釋放。
Semaphore的初始化需要傳入一個整型引數,此引數標識該訊號量可以佔用的資源個數。例如我們有兩個訊號量A,B。A訊號量可以允許兩個執行緒佔用,B訊號量允許一個執行緒佔用,那麼初始化的時候Semaphore A = new Semaphore(2);

public class AlternateExample {
    private static Semaphore s1 = new Semaphore(1);
    private static Semaphore s2 = new Semaphore(1);
    private static Semaphore s3 = new Semaphore(1);
    static Semaphore[] signals = {s1, s2, s3};

    public static void foo(int name) {
        while (true) {
            try {
                signals[name - 1].acquire();

                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("執行緒名:" + name);
            signals[(name) % 3].release();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> foo(1));
        Thread thread2 = new Thread(() -> foo(2));
        Thread thread3 = new Thread(() -> foo(3));
        //先佔用1和2,此處我們要保證的順序是3、1、2
        s1.acquire();
        s2.acquire();
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(300);
    }
}

三、總結

本篇我們用兩個問題引出了3個併發工具CountDownLatchCyclicBarrierSemaphore的實際應用的例子。下一篇我們講從原始碼角度詳細分析下這三個工具的實現細節。

參考文章

【完整程式碼】使用Semaphore實現執行緒的交替執行列印 A1B2C3D4E5_學亮程式設計手記-CSDN部落格
CountDownLatch詳解 - 簡書 (jianshu.com)
Java中多個執行緒交替迴圈執行 - 坐看雲起時_雨宣 - 部落格園 (cnblogs.com)
JAVA Semaphore詳解 - 簡單愛_wxg - 部落格園 (cnblogs.com)

相關文章