Java 中的循環法和 AtomicInteger
一、簡介
自 Java 誕生以來,多執行緒一直是 Java 的一部分。然而,在多執行緒環境中管理並發任務仍然具有挑戰性,特別是當多個執行緒競爭共享資源時。這種競爭通常會導致阻塞、效能瓶頸和資源使用效率低下。
在本教程中,我們將使用強大的AtomicInteger類別在 Java 中建立循環負載平衡器,以確保執行緒安全、非阻塞操作。在此過程中,我們將探索循環調度、上下文切換和原子操作等關鍵概念——所有這些對於高效多執行緒都至關重要。
2. 循環和上下文切換
在我們繼續使用AtomicInteger類別實現循環調度和上下文切換之前,了解循環調度和上下文切換非常重要。
2.1.輪詢調度機制
在開始實作之前,我們先探討一下負載平衡器背後的核心概念:循環調度。這種搶佔式線程調度機制允許單一 CPU 架構使用調度程序來管理多個線程,該調度程序在給定的時間量內執行每個線程。時間量定義了每個執行緒在移動到佇列末端之前接收的固定 CPU 時間量。
例如,如果池中有五台伺服器,第一個請求將發送到伺服器一,第二個請求將發送到伺服器二,依此類推。一旦伺服器 5 處理了請求,循環就會從伺服器 1 開始。這種簡單的機制可確保工作負載的均勻分配。
2.2.上下文切換
當系統暫停一個執行緒、儲存其狀態並載入另一個執行緒來執行時,就會發生上下文切換。儘管對於多任務處理是必要的,但頻繁的上下文切換會帶來開銷並降低系統效率。該過程包括三個步驟:
- 儲存狀態:系統將執行緒的狀態(程式計數器、暫存器、堆疊和參考)保存在進程控制塊(PCB)或執行緒控制塊(TCB)中。
- 載入狀態:調度程序從 PCB 或 TCB 檢索下一個執行緒的狀態。
- 恢復執行:執行緒從中斷處恢復執行。
在我們的負載平衡器中使用像AtomicInteger這樣的非阻塞機制有助於最大限度地減少上下文切換。這樣,多個執行緒可以同時處理請求,而不會產生效能瓶頸。
3.並發性
並發性是指程式透過以看似非阻塞的方式交錯執行來管理和執行多個任務的能力。並發系統中的任務不一定同時執行,但它們看起來是同時執行的,因為它們的執行被建構為獨立且有效率地運作。
在單CPU架構中,上下文切換允許多個任務透過時間片共享CPU時間。在多核心 CPU 架構中,執行緒分佈在 CPU 核心上,並且可以真正並行和並發運行。因此,並發可以廣義地定義為單一CPU看似同時執行多個執行緒或任務的一種方式。
4. 並發實用程式簡介
Java 的並發模型隨著 Java 5 中並發實用程式的引入而得到改進。
憑藉線程池、鎖和原子操作等功能,它們可以幫助開發人員在多執行緒環境中更有效地管理共享資源。讓我們探討一下 Java 引入並發實用程式的原因和方式。
4.1.並發實用程式概述
儘管 Java 具有強大的多執行緒功能,但透過將任務分解為可以並發執行的更小的原子單元來管理任務卻提出了挑戰。隨後,這一差距導致了 Java 中並發實用程式的開發,以更好地利用系統資源。 Java 在 JDK 5 中引入了並發實用程序,提供了一系列同步器、線程池、執行管理器、鎖定和並發集合。此 API 透過 JDK 7 中的Fork/Join 框架進一步擴展。
| 包裹 | 描述 |
|---|---|
java.util.concurrent |
提供類別和介面來取代內建同步機制。 |
java.util.concurrent.locks |
透過Lock介面提供同步方法的替代方法。 |
java.util.concurrent.atomic |
為共享變數提供非阻塞操作,取代了volatile關鍵字。 |
4.2.通用同步器和線程池
Java 並發 API 提供了一組常見的同步器,例如Semaphore 、 CyclicBarrier 、 Phaser等,作為實現這些同步器的傳統方法的替代方案。此外,它在ExecutorService內部提供了一個執行緒池來管理工作執行緒的集合。事實證明,這對於資源密集型平台來說是有效的。
執行緒池是一種管理工作執行緒集合的軟體設計模式。它還提供線程可重用性,可以動態調整活動線程的數量以節省資源。在ExecutorService的基礎上使用這種設計模式,Java 確保每個任務/執行緒都可以在沒有可用執行緒時排隊,並且在工作執行緒釋放後可以執行該執行緒。
5.什麼是AtomicInteger ?
AtomicInteger允許對整數值進行原子操作,使多個執行緒能夠安全地更新整數,而無需明確同步。
5.1. AtomicInteger與同步區塊
使用同步區塊會鎖定共用變數以進行明確訪問,從而導致上下文切換開銷。相比之下, AtomicInteger提供了無鎖機制,提高了多執行緒應用程式的吞吐量。
5.2.非阻塞操作和比較和交換演算法
AtomicInteger的基礎是一種稱為比較和交換 (CAS) 的機制,這就是AtomicInteger中的操作是非阻塞的原因。
與使用鎖來確保線程安全的傳統同步不同, CAS 利用硬體級原子指令來實現相同的目標,而無需鎖定整個資源。
5.3. CAS 機制
CAS 演算法是一種原子操作,用於檢查變數是否保存特定值(期望值)。如果是,則該值會更新為新值。此過程以原子方式發生,不會被其他執行緒中斷。它的工作原理如下:
- 比較:演算法將變數中的當前值與期望值進行比較
- Swap :如果值匹配,則將目前值與新值交換
- 失敗重試:如果值不匹配,則操作會循環重試,直到成功
6. 使用AtomicInteger實現循環
是時候將這些概念付諸實現了。讓我們建立一個循環負載平衡器,將傳入請求分配給伺服器。為此,我們將使用AtomicInteger來追蹤目前伺服器索引,確保即使多個執行緒同時處理請求也能正確路由請求:
private List<String> serverList;
private AtomicInteger counter = new AtomicInteger(0);
我們有一個包含五個伺服器的List和一個初始化為零的AtomicInteger 。此外,計數器將負責將傳入請求分配給適當的伺服器:
public AtomicLoadBalancer(List<String> serverList) {
this.serverList = serverList;
}
public String getServer() {
int index = counter.get() % serverList.size();
counter.incrementAndGet();
return serverList.get(index);
}
getServer()方法以循環方式主動將傳入請求分發到伺服器,同時確保執行緒安全。首先,它使用當前counter值並應用伺服器列表大小的模運算來計算下一個伺服器,以在到達末尾時回繞。然後,它使用incrementAndGet()以原子方式遞增counter ,確保高效、非阻塞更新。由於每個執行緒並行運行,執行順序可能會有所不同。
現在,我們也建立一個擴展Thread類別的IncomingRequest類,將請求導向到正確的伺服器:
class IncomingRequest extends Thread {
private final AtomicLoadBalancer balancer;
private final int requestId;
private Logger logger = Logger.getLogger(IncomingRequest.class.getName());
public IncomingRequest(AtomicLoadBalancer balancer, int requestId) {
this.balancer = balancer;
this.requestId = requestId;
}
@Override
public void run() {
String assignedServer = balancer.getServer();
logger.log(Level.INFO, String.format("Dispatched request %d to %s", requestId, assignedServer));
}
}
由於執行緒同時執行,因此輸出順序可能會有所不同。
7. 驗證實施
現在我們要驗證AtomicLoadBalancer是否在伺服器清單中均勻分配請求。因此,我們首先建立一個包含五台伺服器的列表,並用它初始化負載平衡器。然後,我們使用IncomingRequest執行緒模擬十個請求,這些請求代表請求伺服器的客戶端:
@Test
public void givenBalancer_whenDispatchingRequests_thenServersAreSelectedExactlyTwice() throws InterruptedException {
List<String> serverList = List.of("Server 1", "Server 2", "Server 3", "Server 4", "Server 5");
AtomicLoadBalancer balancer = new AtomicLoadBalancer(serverList);
int numberOfRequests = 10;
Map<String, Integer> serverCounts = new HashMap<>();
List<IncomingRequest> requestThreads = new ArrayList<>();
for (int i = 1; i <= numberOfRequests; i++) {
IncomingRequest request = new IncomingRequest(balancer, i);
requestThreads.add(request);
request.start();
}
for (IncomingRequest request : requestThreads) {
request.join();
String assignedServer = balancer.getServer();
serverCounts.put(assignedServer, serverCounts.getOrDefault(assignedServer, 0) + 1);
}
for (String server : serverList) {
assertEquals(2, serverCounts.get(server), server + " should be selected exactly twice.");
}
}
處理請求後,我們會收集每個伺服器被分配的次數。目標是確保負載平衡器均勻分配負載,因此我們希望每個伺服器恰好分配兩次。最後,我們透過檢查每個伺服器的計數來驗證這一點。如果計數匹配,則確認負載平衡器按預期工作並均勻分配請求。
八、結論
透過使用AtomicInteger和循環演算法,我們建立了一個執行緒安全、非阻塞的負載平衡器,可以有效地跨多個伺服器分發請求。 AtomicInteger的無鎖操作確保我們的負載平衡器避免上下文切換和線程爭用的陷阱,使其成為高效能、多執行緒應用程式的理想選擇。
透過這個實現,我們看到了Java的並發實用程式如何簡化執行緒的管理並提高整體系統效能。無論我們是建立負載平衡器、管理 Web 伺服器中的任務,還是開發任何多執行緒系統,這裡探討的概念都將幫助我們設計更有效率且可擴展的應用程式。
與往常一樣,這些範例的完整實作可以在 GitHub 上找到。