CountDownLatch 與信號量
一、簡介
在 Java 多執行緒中,執行緒之間的有效協調對於確保正確同步和防止資料損壞至關重要。兩種常用的執行緒協調機制是CountDownLatch
和Semaphore
。在本教程中,我們將探討CountDownLatch
和Semaphore
之間的差異,並討論何時使用它們。
2. 背景
讓我們探討這些同步機制背後的基本概念。
2.1. CountDownLatch
CountDownLatch
可讓一個或多個執行緒優雅地暫停,直到完成一組指定的任務。它透過遞減計數器直到其達到零來進行操作,這表示所有先決任務都已完成。
2.2. Semaphore
Semaphore
是一種同步工具,透過使用許可證來控制對共享資源的存取。與CountDownLatch
相比, Semaphore
許可可以在整個應用程式中多次釋放和獲取,從而允許對並發管理進行更細粒度的控制。
3. CountDownLatch
和Semaphore
的差別
在本節中,我們將深入研究這些同步機制之間的關鍵差異。
3.1.計數機制
CountDownLatch
從初始計數開始運行,隨著任務完成該計數會遞減。一旦計數達到零,等待的線程就會被釋放。
Semaphore
維護一組許可,其中每個許可代表存取共享資源的權限。執行緒取得存取資源的許可並在完成後釋放它們。
3.2.可重置性
信號量許可可以多次釋放和獲取,從而實現動態資源管理。例如,如果我們的應用程式突然需要更多的資料庫連接,我們可以釋放額外的許可來動態增加可用連接的數量。
而在CountDownLatch
中,一旦計數達到零,就無法重置或重新用於另一個同步事件。它專為一次性用例而設計。
3.3.動態許可計數
可以使用acquire()
和release()
方法在運行時動態調整Semaphore
許可。這允許動態變更允許同時存取共享資源的執行緒數量。
另一方面,一旦用計數初始化CountDownLatch
,它就保持固定並且在運行時不能更改。
3.4.公平
Semaphore
支援公平的概念,確保等待獲取許可的執行緒按照它們到達的順序(先進先出)獲得服務。這有助於防止高爭用場景中的線程飢餓。
相較之下, CountDownLatch
沒有公平概念。它通常用於一次性同步事件,其中執行緒執行的特定順序不太重要。
3.5.用例
CountDownLatch
通常用於協調多個執行緒的啟動、等待並行操作完成或在繼續主任務之前同步系統的初始化等場景。例如,在並發資料處理應用程式中, CountDownLatch
可以確保在資料分析開始之前完成所有資料載入任務。
另一方面, Semaphore
適合管理對共享資源的訪問,實現資源池,控制對程式碼關鍵部分的訪問,或限制並發資料庫連接的數量。例如,在資料庫連接池系統中, Semaphore
可以限制並發資料庫連接的數量,以防止資料庫伺服器不堪負荷。
3.6.表現
由於CountDownLatch
主要涉及遞減計數器,因此它在處理和資源利用方面產生的開銷最小。此外, Semaphore
在管理許可方面帶來了開銷,特別是在頻繁取得和釋放許可時。每次呼叫acquire()
和release()
都會涉及額外的處理來管理許可計數,這可能會影響效能,尤其是在高並發的情況下。
3.7.概括
下表總結了CountDownLatch
和Semaphore
在各方面的主要差異:
特徵 | CountDownLatch |
Semaphore |
---|---|---|
目的 | 同步執行緒直到一組任務完成 | 控制對共享資源的訪問 |
計數機制 | 遞減計數器 | 管理許可證(令牌) |
可重置性 | 不可重置(一次性同步) | 可重置(可以多次釋放和獲取許可證) |
動態許可計數 | 不 | 是(可以在運行時調整許可) |
公平 | 沒有具體的公平保證 | 提供公平性(先進先出順序) |
表現 | 低開銷(最少的處理) | 由於許可證管理,開銷略高 |
4. 實施比較
在本節中,我們將重點放在CountDownLatch
和Semaphore
在語法和功能上實現方式的差異。
4.1. CountDownLatch
實現
首先,我們建立一個CountDownLatch
,其初始計數等於要完成的任務數。每個工作執行緒模擬一個任務,並在任務完成時使用countDown()
方法減少鎖存器計數。主執行緒使用await()
方法等待所有任務完成:
int numberOfTasks = 3;
CountDownLatch latch = new CountDownLatch(numberOfTasks);
for (int i = 1; i <= numberOfTasks; i++) {
new Thread(() -> {
System.out.println("Task completed by Thread " + Thread.currentThread().getId());
latch.countDown();
}).start();
}
latch.await();
System.out.println("All tasks completed. Main thread proceeds.");
所有任務完成且鎖存器計數達到零後,嘗試呼叫countDown()
將無效。此外,由於鎖存器計數已經為零,因此對await()
任何後續呼叫都會立即返回,而不會阻塞執行緒:
latch.countDown();
latch.await(); // This line won't block
System.out.println("Latch is already at zero and cannot be reset.");
現在讓我們觀察程式的執行並檢查輸出:
Task completed by Thread 11
Task completed by Thread 12
Task completed by Thread 13
All tasks completed. Main thread proceeds.
Latch is already at zero and cannot be reset.
4.2. Semaphore
實現
在此範例中,我們建立一個具有固定數量的許可NUM_PERMITS
的Semaphore
。每個工作執行緒在存取資源之前透過使用acquire()
方法取得許可來模擬資源存取。需要注意的一點是,當執行緒呼叫acquire()
方法取得許可時,它可能會在等待許可時中斷。因此,必須在try
– catch
區塊中捕獲InterruptedException
,以優雅地處理此中斷。
完成資源存取後,線程使用release()
方法釋放許可:
int NUM_PERMITS = 3;
Semaphore semaphore = new Semaphore(NUM_PERMITS);
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println("Thread " + Thread.currentThread().getId() + " accessing resource.");
Thread.sleep(2000); // Simulating resource usage
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
接下來,我們透過釋放額外的許可來模擬重置Semaphore
,以使計數回到初始許可值。這表示Semaphore
量允許在運行時動態調整或重置:
try {
Thread.sleep(5000);
semaphore.release(NUM_PERMITS); // Resetting the semaphore permits to the initial count
System.out.println("Semaphore permits reset to initial count.");
} catch (InterruptedException e) {
e.printStackTrace();
}
以下是程式運行後的輸出:
Thread 11 accessing resource.
Thread 12 accessing resource.
Thread 13 accessing resource.
Thread 14 accessing resource.
Thread 15 accessing resource.
Semaphore permits reset to initial count.
5. 結論
在本文中,我們探討了CountDownLatch
和Semaphore
的關鍵特徵。 CountDownLatch
非常適合在允許執行緒繼續之前需要完成一組固定任務的場景,使其適合一次性同步事件。相較之下, Semaphore
用於透過限制可以同時存取共享資源的執行緒數量來控制對共享資源的訪問,從而對並發管理提供更細粒度的控制。
與往常一樣,範例的原始程式碼可在 GitHub 上取得。