通訊工具類

N1ce2cu發表於2024-07-27
作用
Semaphore 限制執行緒的數量
Exchanger 兩個執行緒交換資料
CountDownLatch 執行緒等待直到計數器減為 0 時開始工作
CyclicBarrier 作用跟 CountDownLatch 類似,但是可以重複使用
Phaser 增強的 CyclicBarrier

Semaphore


Semaphore 翻譯過來是訊號的意思。顧名思義,這個工具類提供的功能就是多個執行緒彼此“傳訊號”。而這個“訊號”是一個int型別的資料,也可以看成是一種“資源”。

// 傳入初始資源總數,預設情況下,是非公平的同步器
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

最主要的方法是 acquire 方法和 release 方法。acquire()方法會申請一個 permit,而 release 方法會釋放一個 permit。當然,你也可以申請多個 acquire(int permits)或者釋放多個 release(int permits)

每次 acquire,permits 就會減少一個或者多個。如果減少到了 0,再有其他執行緒來 acquire,那就要阻塞這個執行緒直到有其它執行緒 release permit 為止。

Semaphore 往往用於資源有限的場景中,去限制執行緒的數量。舉個例子,我想限制同時只能有 3 個執行緒在工作:

public class Main {
    static class MyThread implements Runnable {

        private final int value;
        private final Semaphore semaphore;

        public MyThread(int value, Semaphore semaphore) {
            this.value = value;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                // 獲取 permit
                semaphore.acquire();
                System.out.printf("當前執行緒是%d, 還剩%d個資源,還有%d個執行緒在等待%n",
                        value, semaphore.availablePermits(), semaphore.getQueueLength());
                // 睡眠隨機時間,打亂釋放順序
                Random random = new Random();
                Thread.sleep(random.nextInt(1000));
                System.out.printf("執行緒%d釋放了資源%n", value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 釋放 permit
                semaphore.release();
            }
        }
    }

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(new MyThread(i, semaphore)).start();
        }
    }
}

Semaphore 預設的 acquire 方法是會讓執行緒進入等待佇列,且丟擲異常中斷。但它還有一些方法可以忽略中斷或不進入阻塞佇列:

// 忽略中斷
public void acquireUninterruptibly()
public void acquireUninterruptibly(int permits)

// 不進入等待佇列,底層使用CAS
public boolean tryAcquire
public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
        throws InterruptedException
public boolean tryAcquire(long timeout, TimeUnit unit)

Semaphore 內部有一個繼承了 AQS 的同步器 Sync,重寫了tryAcquireShared方法。在這個方法裡,會去嘗試獲取資源。如果獲取失敗(想要的資源數量小於目前已有的資源數量),就會返回一個負數(代表嘗試獲取資源失敗)。然後當前執行緒就會進入 AQS 的等待佇列。

Exchanger


Exchanger 類用於兩個執行緒交換資料。它支援泛型,也就是說可以在兩個執行緒之間傳送任何資料。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Exchanger<String> exchanger = new Exchanger<>();

        new Thread(() -> {
            try {
                System.out.println("這是執行緒A,得到了另一個執行緒的資料:"
                        + exchanger.exchange("這是來自執行緒A的資料"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // 當一個執行緒呼叫 exchange 方法後,會處於阻塞狀態,只有當另一個執行緒也呼叫了 exchange 方法,它才會繼續執行
        System.out.println("這個時候執行緒A是阻塞的,在等待執行緒B的資料");
        Thread.sleep(1000);

        new Thread(() -> {
            try {
                System.out.println("這是執行緒B,得到了另一個執行緒的資料:"
                        + exchanger.exchange("這是來自執行緒B的資料"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

根據 JDK 裡面註釋的說法,可以總結為一下特性:

  • 此類提供對外的操作是同步的;
  • 用於成對出現的執行緒之間交換資料;
  • 可以視作雙向的同步佇列;
  • 可應用於基因演算法、流水線設計等場景。

Exchanger 類還有一個有超時引數的方法,如果在指定時間內沒有另一個執行緒呼叫 exchange,就會丟擲一個超時異常。

public V exchange(V x, long timeout, TimeUnit unit)

Exchanger 是可以重複使用的。也就是說。兩個執行緒可以使用 Exchanger 在記憶體中不斷地再交換資料。

CountDownLatch


public class CountDownLatch {

    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        // 計數值(count)實際上就是閉鎖需要等待的執行緒數量。這個值只能被設定一次,而且 CountDownLatch沒有提供任何機制去重新設定這個計數值
        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

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

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

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

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

    // 超時等待
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

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

    // 獲取當前還有多少count
    public long getCount() {
        return sync.getCount();
    }

    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}
public class Main {
    // 定義前置任務執行緒
    static class PreTaskThread implements Runnable {
        private final String task;
        private final CountDownLatch countDownLatch;

        public PreTaskThread(String task, CountDownLatch countDownLatch) {
            this.task = task;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                Random random = new Random();
                Thread.sleep(random.nextInt(1000));
                System.out.println(task + " - 任務完成");
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 假設有三個模組需要載入
        CountDownLatch countDownLatch = new CountDownLatch(3);

        // 主任務
        new Thread(() -> {
            try {
                System.out.println("等待資料載入...");
                System.out.printf("還有%d個前置任務%n", countDownLatch.getCount());
                countDownLatch.await();
                System.out.println("資料載入完成,正式開始遊戲!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // 前置任務
        new Thread(new PreTaskThread("載入地圖資料", countDownLatch)).start();
        new Thread(new PreTaskThread("載入人物模型", countDownLatch)).start();
        new Thread(new PreTaskThread("載入背景音樂", countDownLatch)).start();
    }
}

CyclicBarrier


CyclicBarrirer 從名字上來理解是“迴圈屏障”的意思。前面提到了 CountDownLatch 一旦計數值count被降為 0 後,就不能再重新設定了,它只能起一次“屏障”的作用。而 CyclicBarrier 擁有 CountDownLatch 的所有功能,還可以使用reset()方法重置屏障。

如果參與者(執行緒)在等待的過程中,Barrier 被破壞,就會丟擲 BrokenBarrierException。可以用isBroken()方法檢測 Barrier 是否被破壞。

  1. 如果有執行緒已經處於等待狀態,呼叫 reset 方法會導致已經在等待的執行緒出現 BrokenBarrierException 異常。並且由於出現了 BrokenBarrierException,將會導致始終無法等待。
  2. 如果在等待的過程中,執行緒被中斷,會丟擲 InterruptedException 異常,並且這個異常會傳播到其他所有的執行緒。
  3. 如果在執行屏障操作過程中發生異常,則該異常將傳播到當前執行緒中,其他執行緒會丟擲 BrokenBarrierException,屏障被損壞。
  4. 如果超出指定的等待時間,當前執行緒會丟擲 TimeoutException 異常,其他執行緒會丟擲 BrokenBarrierException 異常。
// 構造方法
public CyclicBarrier(int parties) {
    this(parties, null);
}

public CyclicBarrier(int parties, Runnable barrierAction) {
    // 具體實現
}
public class Main {
    static class PreTaskThread implements Runnable {

        private final String task;
        private final CyclicBarrier cyclicBarrier;

        public PreTaskThread(String task, CyclicBarrier cyclicBarrier) {
            this.task = task;
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            // 假設總共三個關卡
            for (int i = 1; i < 4; i++) {
                try {
                    Random random = new Random();
                    Thread.sleep(random.nextInt(1000));
                    System.out.printf("關卡%d的任務%s完成%n", i, task);
                    // 一旦呼叫 await 方法的執行緒數量等於構造方法中傳入的任務總量(這裡是 3),就代表達到屏障了。
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        // CyclicBarrier 允許我們在達到屏障的時候可以執行一個任務,可以在構造方法傳入一個 Runnable 型別的物件。
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
            System.out.println("本關卡所有前置任務完成,開始遊戲...");
        });

        new Thread(new PreTaskThread("載入地圖資料", cyclicBarrier)).start();
        new Thread(new PreTaskThread("載入人物模型", cyclicBarrier)).start();
        new Thread(new PreTaskThread("載入背景音樂", cyclicBarrier)).start();
    }
}

CyclicBarrier 內部使用的是 Lock + Condition 實現的等待/通知模式。詳情可以檢視這個方法的原始碼:

private int dowait(boolean timed, long nanos)

Phaser


Phaser 是 Java 7 中引入的一個併發同步工具,它提供了對動態數量的執行緒的同步能力,這與 CyclicBarrier 和 CountDownLatch 不同,因為它們都需要預先知道等待的執行緒數量。Phaser 是多階段的,意味著它可以同步不同階段的多個操作。

前面我們介紹了 CyclicBarrier,可以發現它在構造方法裡傳入了“任務總量”parties之後,就不能修改這個值了,並且每次呼叫await()方法也只能消耗一個parties計數。但 Phaser 可以動態地調整任務總量!

Phaser 是階段性的,所以它有一個內部的階段計數器。每當我們到達一個階段的結尾時,Phaser 會自動前進到下一個階段。

名詞解釋:

  • Party:Phaser 的上下文中,一個 party 可以是一個執行緒,也可以是一個任務。當我們在 Phaser 上註冊一個 party 時,Phaser 會遞增它的參與者數量。
  • arrive:對應一個 party 的狀態,初始時是 unarrived,當呼叫arriveAndAwaitAdvance()或者 arriveAndDeregister()進入 arrive 狀態,可以透過getUnarrivedParties()獲取當前未到達的數量。
  • register:註冊一個新的 party 到 Phaser。
  • deRegister:減少一個 party。
  • phase:階段,當所有註冊的 party 都 arrive 之後,將會呼叫 Phaser 的onAdvance()方法來判斷是否要進入下一階段。

Phaser 的終止有兩種途徑,Phaser 維護的執行緒執行完畢或者onAdvance()返回true

public class Main {
    static class PreTaskThread implements Runnable {
        private final String task;
        private final Phaser phaser;

        public PreTaskThread(String task, Phaser phaser) {
            this.task = task;
            this.phaser = phaser;
        }

        @Override
        public void run() {
            for (int i = 1; i < 4; i++) {
                try {
                    // 從第二個關卡起,不載入新手教程
                    if (i >= 2 && "載入新手教程".equals(task)) {
                        continue;
                    }
                    Random random = new Random();
                    Thread.sleep(random.nextInt(1000));
                    System.out.printf("關卡%d,需要載入%d個模組,當前模組【%s】%n",
                            i, phaser.getRegisteredParties(), task);

                    // 從第二個關卡起,不載入新手教程
                    if (i == 1 && "載入新手教程".equals(task)) {
                        System.out.println("下次關卡移除載入【新手教程】模組");
                        // 移除一個模組
                        phaser.arriveAndDeregister();
                    } else {
                        phaser.arriveAndAwaitAdvance();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Phaser phaser = new Phaser(4) {
            @Override
            protected boolean onAdvance(int phase, int registeredParties) {
                System.out.printf("第%d次關卡準備完成%n", phase + 1);
                return phase == 3 || registeredParties == 0;
            }
        };

        new Thread(new PreTaskThread("載入地圖資料", phaser)).start();
        new Thread(new PreTaskThread("載入人物模型", phaser)).start();
        new Thread(new PreTaskThread("載入背景音樂", phaser)).start();
        new Thread(new PreTaskThread("載入新手教程", phaser)).start();
    }
}

這裡要注意關卡 1 的輸出,在“載入新手教程”執行緒中呼叫了arriveAndDeregister()減少一個 party 之後,後面的執行緒使用getRegisteredParties()得到的是已經被修改後的 parties 了。但是當前這個階段(phase),仍然是需要 4 個 parties 都 arrive 才觸發屏障的。從下一個階段開始,才需要 3 個 parties 都 arrive 就觸發屏障。

Phaser 類用來控制某個階段的執行緒數量很有用,但它並不在意這個階段具體有哪些執行緒 arrive,只要達到它當前階段的 parties 值,就觸發屏障。所以我這裡的案例雖然制定了特定的執行緒(載入新手教程)來更直觀地表述 Phaser 的功能,但其實 Phaser 是沒有分辨具體是哪個執行緒的功能的,它在意的只是數量。

它內部使用了兩個基於 Fork-Join 框架的原子類輔助:

private final AtomicReference<QNode> evenQ;
private final AtomicReference<QNode> oddQ;

static final class QNode implements ForkJoinPool.ManagedBlocker {
	// 實現程式碼
}

相關文章