Java多線程之CountDownLatch、CyclicBarrier、Semaphore與Exchanger

Java多線程有Runnable、Thread、Callable、線程池、synchronized、volatile、Lock等可以直接使用。也有線程的直接實現可用。

下邊主要講下CountDownLatch、CyclicBarrier、Semaphore與Exchanger

CountDownLatch

從名字可以知道,是個倒計數鎖。通過一個計數器,每個線程完成則減一,並在原地等待。直至減到0,開始後續工作。

應用場景:應用場景:A、B、C三個任務,可以併發執行,然後都執行完後纔可以執行任務D。

public class TryCountDownLatch implements Runnable {
    private int sequence;

    public TryCountDownLatch(int sequence) {
        this.sequence = sequence;
    }

    // 初始化計數器,注意這裏是 static 的。爲了共用
    static final CountDownLatch latch = new CountDownLatch(10);

    @Override
    public void run() {
        try {
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("Complete Run " + sequence);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 計數減一
            latch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            exec.submit(new TryCountDownLatch(i));
        }
        // 等待檢查
        System.out.println("All Thread Wait " + System.currentTimeMillis());
        latch.await();

        System.out.println("All Thread Completed " + System.currentTimeMillis());
        // 關閉線程池
        exec.shutdown();
    }
}

這裏主線程會阻塞在 await() 的地方,然後所有線程類執行後都調用CountDownLatch的countDown()方法,即數字減一。直到爲0,主線程開始繼續工作。幫我們解決了多線程的執行依賴關係。

CyclicBarrier

也即是我們常說的柵欄類,線程走到柵欄後阻塞等待,直到所有線程都滿足才能繼續往下執行。至於它與CountDownLatch 的區別,網上說CyclicBarrier是N個線程相互等待,而
CyclicBarrier 是一個後續線程等待N個線程。我覺得沒事區別。唯一的區別是:CountDownLatch採用計算器,只能使用一次。而CyclicBarrier 即循環柵欄,也就是說它可以循環使用。

它的用法跟CountDownLatch 差不多,首先它的構造函數需要一個等待線程數,和一個後續線程任務

public class TestCyclicBarrier {

    public static void main(String[] args) {
        // 注意這裏柵欄數和下邊的線程數
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("所有線程均完成,此處進行柵欄後的收尾工作");
            }
        });

        for (int i = 0; i < 5; i++) {
            TryCyclicBarrier thread = new TryCyclicBarrier(barrier, i);
            new Thread(thread).start();
        }
        System.out.println("主線程結束 " + System.currentTimeMillis());
    }
}

在所有要先執行的線程裏調用await() 方法

public class TryCyclicBarrier implements Runnable {

    private CyclicBarrier cyclicBarrier;

    private int sequence;

    public TryCyclicBarrier(CyclicBarrier cyclicBarrier, int sequence) {
        this.cyclicBarrier = cyclicBarrier;
        this.sequence = sequence;
    }

    @Override
    public void run() {
        try {
            System.out.println("線程 " + sequence + " 開始工作");
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("線程 " + sequence + " 到達柵欄");

            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

在有指定線程數 await() 後,等待的線程任務纔開始執行。輸出如下:

線程 2 開始工作
線程 1 開始工作
線程 4 開始工作
線程 3 開始工作
線程 0 開始工作
主線程結束 1585920830794
線程 3 到達柵欄
線程 0 到達柵欄
線程 4 到達柵欄
線程 1 到達柵欄
線程 2 到達柵欄
所有線程均完成,此處進行柵欄後的收尾工作

注意:如果超過指定線程數,等待的線程任務有可能會再次執行。

假如CyclicBarrier 指定了需要3個線程await()和一個後續線程任務D,當A、B、C三個await後,CyclicBarrier會執行D。之後A、B、C三個再次 await() 後,還會再次執行一次任務D。

 

Semaphore

即信號量類。我們知道synchronized 用來控制方法或者代碼塊互斥的,同一時間只有一個線程進入。Semaphore 是 synchronized 的加強版,作用是控制線程的併發數量。

新建一個信號量對象,並設置最多可進入的線程數。在要控制併發的代碼之前調用 acquire() ,之後調用 release() 方法。

public class TrySemaphore {
    // 同步關鍵類,構造方法傳入的數字是多少,則同一個時刻,最多允許多少個進程同時運行
    private Semaphore semaphore = new Semaphore(2);
    
    public void process(String threadName) throws Exception {
        // 在 semaphore.acquire() 和 semaphore.release()之間的代碼,同一時刻只允許指定個數線程進入,
        semaphore.acquire();
        System.out.println(System.currentTimeMillis() + " 進入互斥區 " + threadName);
        Thread.sleep(new Random().nextInt(10) * 1000);
        System.out.println(System.currentTimeMillis() + " 離開互斥區 " + threadName);
        semaphore.release();

    }
}

然後將該任務放入多線程中執行:

public class TestSemaphore extends Thread {

    private TrySemaphore work;

    private int sequence;

    public TestSemaphore(TrySemaphore work, int sequence) {
        this.work = work;
        this.sequence = sequence;
    }

    @Override
    public void run() {
        try {
            this.work.process("Thread " + sequence);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TrySemaphore trySemaphore = new TrySemaphore();
        for (int i = 0; i < 5; i++) {
            TestSemaphore thread = new TestSemaphore(trySemaphore, i);
            thread.start();
        }
    }
}

執行結果如下:

1585920227612 進入互斥區 Thread 0
1585920227612 進入互斥區 Thread 1
1585920233615 離開互斥區 Thread 1
1585920233616 進入互斥區 Thread 2
1585920235616 離開互斥區 Thread 0
1585920235616 進入互斥區 Thread 3
1585920239618 離開互斥區 Thread 2
1585920239618 進入互斥區 Thread 4
1585920242619 離開互斥區 Thread 3
1585920242622 離開互斥區 Thread 4

從上邊日誌輸出可以看出,最開始只有倆線程進入互斥區,然後有線程離開後,其他線程才能進去該代碼區。從這點來說,它跟synchronized 效果一模一樣,只是允許的線程數量大於1而已。

Exchanger

Exchanger 是一個交換服務,允許原子性的交換兩個(多個)對象,但同時只有一對纔會成功。

當一個線程到達 exchange 調用點時,如果其他線程此前已經調用了此方法,則其他線程會被調度喚醒並與之進行對象交換,然後各自返回;
如果其他線程還沒到達交換點,則當前線程會被掛起,直至其他線程到達纔會完成交換並正常返回,或者當前線程被中斷或超時返回
例如

public class TestExchange {
    static class Processer extends Thread {
        private Exchanger<String> exchanger;

        public Processer(String name, Exchanger<String> exchanger) {
            super(name);
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            for (int i = 1; i < 5; i++) {// 注意:這裏從1 開始,每個線程去去交換4次
                try {
                    TimeUnit.SECONDS.sleep(1);
                    String preData = "From" + getName() + " data" + i;
                    String postData = exchanger.exchange(preData);
                    System.out.println(getName() + " 交換前:" + preData + " 交換後:" + postData);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Exchanger<String> exchanger = new Exchanger<String>();
        new Processer("Processer-1", exchanger).start();
        new Processer("Processer-2", exchanger).start();
        new Processer("Processer-3", exchanger).start();
        // TODO 3個線程 產生 4 * 3 = 12 次交換。因爲兩兩直接才能交換。所以能結束.
        // 如果 這裏產生了奇數個交換,則某個線程將用於處於等等狀態
        TimeUnit.SECONDS.sleep(7);
    }
}

上邊正好構成偶數個交換,兩兩成功。因此可以結束。

 

Lock

Lock經常用來跟synchronized 比較:synchronized 可以加在類、方法、代碼塊上,報錯後自動釋放鎖,可以防止JVM對代碼重排序。而Lock 加在代碼塊上,且需要主動釋放,是Java的類。主要用的是ReentrantLock,即可重入鎖。ReadWriteLock 讀寫鎖。

對比上邊代碼,改成Lock方式:

public class TryLoack {
    private int fromValue;

    private int toValue;

    public TryLoack(int fromValue, int toValue) {
        this.fromValue = fromValue;
        this.toValue = toValue;
    }

    public int balance(int offset) {
        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            fromValue -= offset;
            toValue += offset;
            return fromValue + toValue;
        } finally {
            lock.unlock();
        }
    }
}

volatile

嚴格來說volatile 不能解決多線程併發互斥問題。它只涉及變量在線程中的可見行。

假設變量 var 被A、B兩個線程使用,當A使用並修改var 時,它修改的只是var 在當前線程中的副本,對此線程B是不可見的,直到本次修改被定期同步到主內存。爲了讓 A 對 var  的修改立刻被 B 感知,就需要對 var 加 volatile 修飾符。

單例設計模式中,volatile 與 synchronized 一起使用,雙重檢查來生成單例對象。參考單例模式雙重檢查

 

 

 

 

 

 

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