一、引言
併發編程就像是在廚房裏同時炒 10 道菜 - 看似效率提高了,但一不小心就會手忙腳亂。作為 Java 後端開發,我們經常為併發問題頭疼不已:生產環境突然卡死,線程 CPU 使用率飆升卻沒有業務進展,各種監控工具報警...而當你想復現問題時,它又像幽靈一樣"按鬧分配",讓人抓狂。
併發 BUG 難以排查的原因主要有三:
- 不確定性:同樣的代碼,運行 10 次可能只出現 1 次問題
- 複雜性:多線程交互關係複雜,排查難度指數級增長
- 上下文依賴:問題往往與特定負載、數據狀態相關
在本文中,我將分享兩個真實項目中遇到的併發問題案例:一個是經典的死鎖導致服務無響應,另一個是不那麼常見但同樣致命的活鎖問題。我們將深入分析問題根源,並展示完整的排查思路和解決方案。
死鎖與活鎖的區別
二、併發問題概覽
常見併發問題類型解析
-
死鎖(Deadlock): 兩個或多個線程互相持有對方需要的資源,導致所有線程永久阻塞。想象兩個人分別拿着筷子和碗,都在等對方先放下自己手中的物品。
死鎖形成的四個必要條件:
- 互斥條件:資源只能被一個線程佔用
- 持有並等待:線程已持有一些資源,同時等待其他資源
- 不可搶佔:資源只能由持有者主動釋放,不能被其他線程強制剝奪
- 循環等待:線程之間形成環形的資源等待關係
-
活鎖(Livelock): 線程雖然沒有阻塞(仍處於 RUNNABLE 狀態),但一直在重複相同的操作而無法推進。就像兩個人在走廊相遇,雙方都想讓對方先過,結果兩人一直左右閃躲但誰都無法通過。
與死鎖的本質區別:
- 死鎖中線程處於 BLOCKED 狀態,完全停止活動,CPU 使用率較低
- 活鎖中線程處於 RUNNABLE 狀態,持續消耗 CPU 資源但無實際進展,導致 CPU 使用率高
- 線程飢餓(Starvation): 某些線程因無法獲取所需資源而無法推進。例如高優先級線程持續執行導致低優先級線程長時間無法獲得 CPU 時間片。
Java 提供了豐富的併發工具來協調線程間的交互:
- synchronized 關鍵字: 最基礎的同步機制
- Lock 接口及實現: 比 synchronized 更靈活的鎖機制
- Condition: 實現線程間的精確通知
- 併發集合: 如 ConcurrentHashMap、CopyOnWriteArrayList 等
- 線程池: ExecutorService 及其實現
- 原子類: AtomicInteger 等保證原子性操作
三、死鎖實戰案例分析
1. 線上事故背景
某電商平台的訂單處理服務,在雙 11 活動期間突然無法響應,系統監控顯示:
- 服務器 CPU 使用率不高(約 20%)
- 線程數持續增加
- 請求平均響應時間從 200ms 飆升到 30 秒以上
- 數據庫連接池告警
緊急重啓服務後恢復正常,但半小時後問題再次出現。這次我們決定不立即重啓,而是深入排查根本原因。
2. 死鎖代碼還原
問題出在訂單處理和庫存更新的核心業務邏輯中:
public class OrderService {
private final Object orderLock = new Object();
private final Object inventoryLock = new Object();
public void createOrder(String userId, String productId) {
synchronized(orderLock) {
// 1. 創建訂單記錄
createOrderRecord(userId, productId);
// 2. 更新庫存
synchronized(inventoryLock) {
updateInventory(productId);
}
}
}
public void cancelOrder(String orderId) {
synchronized(inventoryLock) {
// 1. 恢復庫存
restoreInventory(orderId);
// 2. 更新訂單狀態
synchronized(orderLock) {
updateOrderStatus(orderId, "CANCELED");
}
}
}
}
問題分析:在高併發場景下,createOrder和cancelOrder方法可能同時執行。當線程 A 執行createOrder並獲取了orderLock鎖,同時線程 B 執行cancelOrder並獲取了inventoryLock鎖時,兩個線程都在等待對方釋放鎖,形成死鎖。回顧死鎖的四個必要條件,這裏全部滿足:
- 互斥條件:synchronized 鎖具有互斥性
- 持有並等待:兩個線程都持有一個鎖並等待另一個
- 不可搶佔:synchronized 鎖不可被搶佔
- 循環等待:兩個線程形成了環形等待關係
3. 死鎖排查步驟
步驟 1:使用 jstack 獲取線程轉儲信息
jstack -l <PID> > thread_dump.txt
在轉儲信息中,我們發現了明確的死鎖警告:
Found one Java-level deadlock:
=============================
"order-processing-thread-12":
waiting to lock monitor 0x00007f9a8c0a4580 (object 0x00000000f4b9ddf0, a java.lang.Object),
which is held by "order-cancel-thread-5"
"order-cancel-thread-5":
waiting to lock monitor 0x00007f9a8c0a4300 (object 0x00000000f4b9dd20, a java.lang.Object),
which is held by "order-processing-thread-12"
Java stack information for the threads listed above:
===================================================
"order-processing-thread-12":
at com.example.OrderService.createOrder(OrderService.java:15)
- waiting to lock <0x00000000f4b9ddf0> (a java.lang.Object)
- locked <0x00000000f4b9dd20> (a java.lang.Object)
"order-cancel-thread-5":
at com.example.OrderService.cancelOrder(OrderService.java:25)
- waiting to lock <0x00000000f4b9dd20> (a java.lang.Object)
- locked <0x00000000f4b9ddf0> (a java.lang.Object)
如何將 jstack 輸出的十六進制內存地址與代碼中的具體對象關聯?可以通過以下方法:
// 在代碼中添加臨時日誌,輸出鎖對象的內存哈希碼
System.out.println("orderLock identity: 0x" +
Integer.toHexString(System.identityHashCode(orderLock)));
System.out.println("inventoryLock identity: 0x" +
Integer.toHexString(System.identityHashCode(inventoryLock)));
這樣就能將 jstack 中的十六進制地址0x00000000f4b9dd20與0x00000000f4b9ddf0分別對應到orderLock和inventoryLock,確認具體是哪些鎖對象造成了死鎖。
步驟 2:使用 VisualVM 可視化分析
通過 VisualVM 的線程面板,我們可以更直觀地分析死鎖情況:
- 啓動 VisualVM 並連接目標 JVM
- 點擊"線程"選項卡
- 在右上角過濾器中選擇"檢測死鎖"功能
- 查看自動檢測到的死鎖線程組
VisualVM 會以圖形方式展示線程之間的鎖依賴關係:
Java Mission Control(JMC)同樣提供了有力的死鎖分析工具。JMC 的"鎖競爭分析"功能可生成火焰圖,直觀顯示哪些鎖被頻繁爭用及等待時間分佈,幫助發現潛在的死鎖風險點。
4. 死鎖修復方案
解決死鎖問題可以通過破壞四個必要條件之一來實現。在實際操作中,破壞"循環等待"條件通常是最簡單可行的方法,因為:
- 互斥條件:大多數業務場景必須保持資源互斥訪問以保證數據一致性
- 持有並等待:預先一次性申請所有資源會降低併發度
- 不可搶佔:Java 內置鎖(synchronized)本身不支持搶佔
所以我們選擇確保所有線程按照相同的順序獲取鎖:
public class OrderService {
private final Object orderLock = new Object();
private final Object inventoryLock = new Object();
// 提取共同的鎖獲取邏輯,確保一致的鎖順序
private void executeWithOrderedLocks(Runnable action) {
synchronized(orderLock) {
synchronized(inventoryLock) {
action.run();
}
}
}
public void createOrder(String userId, String productId) {
executeWithOrderedLocks(() -> {
createOrderRecord(userId, productId);
updateInventory(productId);
});
}
public void cancelOrder(String orderId) {
executeWithOrderedLocks(() -> {
restoreInventory(orderId);
updateOrderStatus(orderId, "CANCELED");
});
}
}
通用鎖順序解決方案:
在實際項目中,有時難以硬編碼鎖順序,特別是當鎖對象數量很多或動態創建時。可以採用基於系統標識的順序策略:
public void operateOnResources(Object resource1, Object resource2) {
Object firstLock, secondLock;
// 根據對象的系統哈希碼確定鎖的獲取順序
// System.identityHashCode返回的是對象的內存地址相關的整數
// 在JVM生命週期內保持唯一,不同於Object.hashCode()可能被重寫
if (System.identityHashCode(resource1) < System.identityHashCode(resource2)) {
firstLock = resource1;
secondLock = resource2;
} else if (System.identityHashCode(resource1) > System.identityHashCode(resource2)) {
firstLock = resource2;
secondLock = resource1;
} else {
// 處理哈希碼相同的極端情況,使用額外的唯一標識
// 雖然identityHashCode碰撞概率很低,但理論上可能發生
String id1 = resource1.getClass().getName() + "@" + Integer.toHexString(resource1.hashCode());
String id2 = resource2.getClass().getName() + "@" + Integer.toHexString(resource2.hashCode());
if (id1.compareTo(id2) <= 0) {
firstLock = resource1;
secondLock = resource2;
} else {
firstLock = resource2;
secondLock = resource1;
}
}
synchronized(firstLock) {
synchronized(secondLock) {
// 執行需要同時持有兩把鎖的操作
performOperation(resource1, resource2);
}
}
}
使用顯式鎖和超時機制:
我們可以進一步提高代碼質量,使用 Lock 接口提供的 tryLock 方法和超時機制來防止死鎖:
public class OrderService {
private final ReentrantLock orderLock = new ReentrantLock();
private final ReentrantLock inventoryLock = new ReentrantLock();
public boolean createOrder(String userId, String productId) {
try {
// 嘗試獲取鎖,設置超時時間
// 業務可接受的最大阻塞時間是5秒
if (orderLock.tryLock(5, TimeUnit.SECONDS)) {
try {
if (inventoryLock.tryLock(5, TimeUnit.SECONDS)) {
try {
createOrderRecord(userId, productId);
updateInventory(productId);
return true;
} finally {
inventoryLock.unlock();
}
}
} finally {
orderLock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 獲取鎖失敗,可以進行重試或其他處理
log.warn("Failed to acquire locks for order creation: userId={}, productId={}", userId, productId);
return false;
}
// cancelOrder方法類似實現
}
四、活鎖實戰案例分析
1. 線上事故背景
某支付系統在交易高峯期出現異常:某些交易流程 CPU 使用率異常高(接近 100%),但交易完成率急劇下降。系統日誌中充斥着大量重試日誌,監控顯示:
- 特定微服務 CPU 使用持續高負載
- 事務處理超時數量激增
- 系統吞吐量大幅下降
2. 活鎖代碼還原
問題出在支付確認環節的樂觀鎖重試機制:
public class PaymentProcessor {
@Autowired
private TransactionRepository transactionRepo;
public boolean processPayment(String transactionId) {
boolean success = false;
// 無限重試直到成功
while (!success) {
Transaction tx = transactionRepo.findById(transactionId);
if (tx.getStatus() == TransactionStatus.PENDING) {
// 更新交易狀態,使用版本號進行樂觀鎖控制
success = transactionRepo.updateStatus(
transactionId,
TransactionStatus.COMPLETED,
tx.getVersion()
);
if (!success) {
// 版本衝突,記錄日誌
log.warn("Transaction version conflict, retrying: {}", transactionId);
}
} else {
// 交易已完成或失敗
return tx.getStatus() == TransactionStatus.COMPLETED;
}
}
return true;
}
}
問題分析:當多個服務實例同時處理同一筆交易時,由於採用了無限重試且沒有退避策略,多個線程會持續競爭同一個資源,導致大量的版本衝突和無效重試,形成活鎖。
為什麼活鎖會導致 CPU 高負載?
與死鎖不同,活鎖中的線程一直處於 RUNNABLE 狀態,會被操作系統不斷調度執行。每個線程都在不斷執行代碼(查詢數據庫、嘗試更新、檢查結果),即使這些操作沒有實際業務進展,也會持續消耗 CPU 資源。在高併發環境下,多個線程同時執行無效重試,就會導致 CPU 使用率飆升。
此代碼還有另一個問題:它假設交易狀態只會從 PENDING 變為 COMPLETED,沒有考慮可能出現的 FAILED 狀態,在某些異常場景可能導致無法退出循環。
3. 活鎖排查步驟
步驟 1:分析線程棧和日誌模式
通過 jstack 獲取線程棧,發現大量線程在執行相同代碼段,且狀態為 RUNNABLE:
"payment-thread-15" #45 daemon prio=5 os_prio=0 tid=0x00007f9a8c0b4000 nid=0x1a3b runnable [0x00007f9a7bb3f000]
java.lang.Thread.State: RUNNABLE
at com.example.PaymentProcessor.processPayment(PaymentProcessor.java:24)
"payment-thread-16" #46 daemon prio=5 os_prio=0 tid=0x00007f9a8c0b6000 nid=0x1a3c runnable [0x00007f9a7ba3e000]
java.lang.Thread.State: RUNNABLE
at com.example.PaymentProcessor.processPayment(PaymentProcessor.java:24)
分析日誌發現特定交易 ID 出現異常頻繁的重試日誌,且持續時間長:
2023-11-11 10:15:32.142 WARN [payment-thread-15] Transaction version conflict, retrying: TX123456
2023-11-11 10:15:32.157 WARN [payment-thread-16] Transaction version conflict, retrying: TX123456
2023-11-11 10:15:32.168 WARN [payment-thread-15] Transaction version conflict, retrying: TX123456
// 持續數百次甚至更多...
步驟 2:使用 Arthas 分析方法執行情況
使用 Arthas 的 trace 命令跟蹤方法執行:
trace com.example.PaymentProcessor processPayment
Arthas 輸出示例:
Traced method: processPayment
Press Q or Ctrl+C to abort.
Affect(class count: 1, method count: 1) cost in 84 ms, listenerId: 1
`---ts=2023-11-11 10:20:15;thread_name=http-nio-8080-exec-3;id=31;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader
`---[2.786ms] com.example.PaymentProcessor:processPayment()
+---[0.045ms] com.example.repo.TransactionRepository:findById()
+---[0.012ms] com.example.model.Transaction:getStatus()
+---[2.318ms] com.example.repo.TransactionRepository:updateStatus()
`---[0.021ms] com.example.model.Transaction:getStatus()
`---ts=2023-11-11 10:20:15;thread_name=http-nio-8080-exec-5;id=33;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader
`---[2.964ms] com.example.PaymentProcessor:processPayment()
+---[0.053ms] com.example.repo.TransactionRepository:findById()
+---[0.015ms] com.example.model.Transaction:getStatus()
+---[2.421ms] com.example.repo.TransactionRepository:updateStatus()
`---[0.024ms] com.example.model.Transaction:getStatus()
注意特徵:相同交易 ID 的方法執行耗時很短(僅幾毫秒),但調用頻率極高,每個方法執行完成後立即又執行一次,這是活鎖的典型表現。數據庫操作(updateStatus)佔用了大部分執行時間,表明瓶頸在數據競爭上。
步驟 3:復現問題
使用 JMeter 創建測試場景或編寫多線程測試:
@Test
public void testConcurrentPaymentProcessing() throws Exception {
String transactionId = "TX123456";
// 創建一個固定大小的線程池,模擬多個服務實例
ExecutorService executor = Executors.newFixedThreadPool(10);
// 創建10個併發任務
List<Future<Boolean>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
futures.add(executor.submit(() -> paymentProcessor.processPayment(transactionId)));
}
// 等待所有任務完成
for (Future<Boolean> future : futures) {
future.get(30, TimeUnit.SECONDS);
}
}
4. 活鎖修復方案
引入指數退避策略和最大重試次數,同時完善狀態轉換處理:
public class PaymentProcessor {
// 最大重試次數,根據業務容忍度設置
private static final int MAX_RETRIES = 10;
// 最大退避時間60秒,根據業務處理超時時間選擇
// 大多數支付系統的前端超時為90秒,預留30秒處理餘地
private static final long MAX_BACKOFF_TIME = 60000;
// 使用ThreadLocalRandom代替Random,減少多線程競爭
// Random在多線程環境下會因為種子競爭導致性能下降
private final ThreadLocalRandom random = ThreadLocalRandom.current();
public boolean processPayment(String transactionId) {
boolean success = false;
int attempts = 0;
while (!success && attempts < MAX_RETRIES) {
attempts++;
Transaction tx = transactionRepo.findById(transactionId);
// 完善狀態機處理,明確各狀態的業務含義
switch (tx.getStatus()) {
case PENDING: // 交易待處理狀態
success = transactionRepo.updateStatus(
transactionId,
TransactionStatus.COMPLETED,
tx.getVersion()
);
if (!success) {
// 指數退避策略
long baseBackoff = Math.min(
MAX_BACKOFF_TIME,
(long) Math.pow(2, attempts)
);
// 隨機抖動隨重試次數增加而擴大範圍,避免驚羣效應
// 初次衝突抖動小,重試多次後抖動範圍變大,增加錯開概率
long randomJitter = random.nextInt(Math.min(1000 * attempts, 10000));
long backoffTime = baseBackoff + randomJitter;
log.warn("Transaction conflict, attempt {}/{}, backing off for {} ms: {}",
attempts, MAX_RETRIES, backoffTime, transactionId);
try {
Thread.sleep(backoffTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
break;
case COMPLETED: // 交易已完成狀態
return true;
case FAILED: // 交易失敗狀態
case CANCELLED: // 交易已取消狀態
log.info("Transaction {} is in state {}, no processing needed",
transactionId, tx.getStatus());
return false;
default:
log.warn("Transaction {} is in unexpected state: {}",
transactionId, tx.getStatus());
return false;
}
}
if (!success) {
// 達到最大重試次數,記錄錯誤並通知系統進行人工干預
log.error("Failed to process transaction after {} attempts: {}",
MAX_RETRIES, transactionId);
notifyTransactionFailure(transactionId);
}
return success;
}
}
修復後,系統在高負載下表現穩定,CPU 使用率降至正常水平,交易成功率恢復正常。退避策略解釋:
- 指數遞增的基礎退避時間:2^attempts 毫秒,隨着重試次數增加而指數增長
- 隨機抖動成分:避免多個線程同時重試導致新一輪衝突,抖動範圍隨重試次數增加
- 最大退避限制:確保極端情況下等待時間不會超過業務可接受的範圍
臨時止損措施
在代碼修復部署前,可採取以下臨時應急措施:
- 服務限流:使用 Sentinel、Hystrix 等工具對支付服務進行限流
- 分批處理:調整業務流程,將大批量交易分批次處理
- 手動數據修復:對卡在活鎖狀態的交易進行手動狀態修正
五、線程飢餓案例分析
線程飢餓案例
某數據分析系統中,低優先級的報表生成任務長時間無法執行,排查發現原因是線程池配置不當:
// 問題代碼:使用優先級隊列但未考慮低優先級任務長期無法執行的問題
ExecutorService executor = new ThreadPoolExecutor(
10, 20, 60, TimeUnit.SECONDS,
new PriorityBlockingQueue<>());
// 提交的任務
public class PriorityTask implements Runnable, Comparable<PriorityTask> {
private final Priority priority;
private final Runnable task;
public PriorityTask(Priority priority, Runnable task) {
this.priority = priority;
this.task = task;
}
@Override
public void run() {
task.run();
}
@Override
public int compareTo(PriorityTask other) {
// 高優先級任務在隊列前面,會優先執行
return other.priority.value() - this.priority.value();
}
}
// 使用示例
executor.submit(new PriorityTask(Priority.HIGH, () -> processRealTimeData()));
executor.submit(new PriorityTask(Priority.LOW, () -> generateReport()));
由於使用了優先級隊列,且系統持續產生高優先級任務,導致低優先級任務永遠無法執行。
解決方案:為不同優先級的任務創建獨立的線程池,並設置合理的資源分配:
// 分離關注點,為不同任務類型創建專用線程池
// CPU密集型任務(如實時數據處理)線程數 = CPU核心數 + 1
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService highPriorityExecutor = Executors.newFixedThreadPool(cpuCores + 1);
// I/O密集型任務(如數據庫操作)線程數 = CPU核心數 * 2
ExecutorService normalPriorityExecutor = Executors.newFixedThreadPool(cpuCores * 2);
// 低優先級批處理任務,使用有界隊列避免資源耗盡
ThreadPoolExecutor lowPriorityExecutor = new ThreadPoolExecutor(
2, 5, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 有界隊列限制等待任務數量
new ThreadPoolExecutor.CallerRunsPolicy() // 隊列滿時讓調用者線程執行任務
);
// 確保報表任務執行的公平性,使用定時調度
ScheduledExecutorService reportScheduler = Executors.newScheduledThreadPool(2);
reportScheduler.scheduleAtFixedRate(() -> {
generateReports();
}, 0, 1, TimeUnit.HOURS);
另一種優先級隊列的公平性解決方案是引入"飢餓計數器":
public class FairnessTask implements Runnable, Comparable<FairnessTask> {
private final Priority priority;
private final Runnable task;
private final AtomicLong starvationCounter = new AtomicLong(0);
private final long creationTime = System.currentTimeMillis();
@Override
public void run() {
task.run();
}
@Override
public int compareTo(FairnessTask other) {
// 飢餓計數達到閾值的低優先級任務可以提升優先級
if (this.starvationCounter.get() > 1000) {
return -1; // 放到隊列前面
}
// 優先級相同時按創建時間排序(先進先出)
if (this.priority.value() == other.priority.value()) {
return Long.compare(this.creationTime, other.creationTime);
}
// 每次比較都增加飢餓計數
this.starvationCounter.incrementAndGet();
other.starvationCounter.incrementAndGet();
return other.priority.value() - this.priority.value();
}
}
注意:PriorityBlockingQueue本身不支持 fair 參數。公平性主要通過以下兩種方式處理:
- 在 Comparable 的實現中,考慮等待時間或飢餓計數,如上例所示
- 將優先級和公平性分開處理,例如用獨立線程池或調度器確保各優先級任務都能獲取資源
"飢餓計數器"方案適用於:優先級任務量分佈不均衡,但低優先級任務仍需保證最終執行的場景。例如,實時分析優先,但報表任務不能無限期推遲。
六、分佈式場景中的併發問題
在微服務架構中,併發問題不僅存在於單個 JVM 內,還會跨服務邊界產生更復雜的分佈式併發問題。
分佈式活鎖案例
某微服務系統中,訂單服務和庫存服務之間出現了"重試風暴":
// 訂單服務中的庫存確認方法
public boolean confirmInventory(String orderId) {
int retries = 0;
while (retries < MAX_RETRIES) {
try {
// 調用庫存服務
return inventoryClient.reserve(orderId);
} catch (TemporaryException e) {
retries++;
// 簡單的固定間隔重試,沒有退避策略
Thread.sleep(100);
}
}
// 失敗後拋出異常
throw new ServiceException("Failed to confirm inventory");
}
// 支付服務中的訂單確認方法
public boolean confirmOrder(String paymentId) {
int retries = 0;
while (retries < MAX_RETRIES) {
try {
// 調用訂單服務
return orderClient.confirm(paymentId);
} catch (TemporaryException e) {
retries++;
// 同樣簡單的固定間隔重試
Thread.sleep(100);
}
}
throw new ServiceException("Failed to confirm order");
}
問題:在高峯期服務間調用開始超時,導致多個服務同時觸發重試機制。由於重試策略過於簡單,所有服務幾乎同時重試,引發雪崩效應,系統請求量劇增但成功率極低——典型的分佈式活鎖。
解決方案:
public boolean confirmInventory(String orderId) {
// 使用指數退避+熔斷器模式
return circuitBreaker.executeWithFallback(
// 主要邏輯
() -> {
return retryTemplate.execute(context -> {
return inventoryClient.reserve(orderId);
});
},
// 降級邏輯
e -> {
// 記錄異常並進入補償流程
log.error("Inventory service unavailable, entering compensating process", e);
compensationService.scheduleInventoryCheck(orderId);
return false;
}
);
}
配置説明:
// 指數退避重試模板
RetryTemplate retryTemplate = RetryTemplate.builder()
.maxAttempts(5)
.exponentialBackoff(100, 2, 30000) // 初始100ms,乘數2,最大30秒
.retryOn(TemporaryException.class)
.build();
// 熔斷器配置
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50%失敗率觸發熔斷
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔斷後30秒嘗試半開
.permittedNumberOfCallsInHalfOpenState(10) // 半開狀態允許10次調用測試
.slidingWindowSize(100) // 基於最近100次調用統計
.build();
最終一致性與強一致性對比
不同的一致性模型對併發控制有重大影響:
強一致性方案(容易產生活鎖):
// 訂單狀態更新(同步處理)
public void updateOrderStatus(String orderId, OrderStatus newStatus) {
// 獲取分佈式鎖
RLock lock = redisson.getLock("order:" + orderId);
try {
// 等待鎖30秒,持有鎖10秒
if (lock.tryLock(30, 10, TimeUnit.SECONDS)) {
try {
// 1. 更新訂單狀態
orderRepository.updateStatus(orderId, newStatus);
// 2. 調用支付服務更新支付狀態(可能阻塞或失敗)
paymentClient.updateStatus(orderId, mapToPaymentStatus(newStatus));
// 3. 調用庫存服務更新庫存(可能阻塞或失敗)
inventoryClient.updateForOrder(orderId, newStatus);
// 4. 發送通知(可能阻塞或失敗)
notificationService.sendUpdate(orderId, newStatus);
} finally {
lock.unlock();
}
} else {
throw new TimeoutException("Failed to acquire lock for order " + orderId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("Operation interrupted", e);
}
}
最終一致性方案(減少活鎖風險):
// 訂單狀態更新(異步補償)
public void updateOrderStatus(String orderId, OrderStatus newStatus) {
// 1. 先更新自身狀態
boolean updated = orderRepository.updateStatus(orderId, newStatus);
if (updated) {
// 2. 發佈領域事件到消息隊列,由其他服務異步處理
OrderStatusChangedEvent event = new OrderStatusChangedEvent(orderId, newStatus);
eventPublisher.publishEvent(event);
// 3. 記錄需要異步確認的操作
asyncOperationTracker.track(
orderId,
"ORDER_STATUS_UPDATE",
Map.of("status", newStatus)
);
}
}
// 異步補償服務定期檢查未完成的操作
@Scheduled(fixedRate = 60000) // 每分鐘執行
public void processIncompleteOperations() {
List<AsyncOperation> pendingOps = asyncOperationTracker
.findIncompleteOperations("ORDER_STATUS_UPDATE");
for (AsyncOperation op : pendingOps) {
if (op.getRetryCount() < 10) {
try {
// 重試相關服務調用
retryOrderStatusUpdate(op.getEntityId(),
(OrderStatus)op.getParams().get("status"));
// 標記成功
asyncOperationTracker.markAsComplete(op.getId());
} catch (Exception e) {
// 記錄重試並增加退避時間
asyncOperationTracker.incrementRetryCount(op.getId());
}
} else {
// 達到最大重試次數,標記為需要人工干預
asyncOperationTracker.markAsNeedsAttention(op.getId());
alertService.sendAlert("Order update failed after maximum retries: " + op.getEntityId());
}
}
}
最終一致性方案通過異步處理和冪等操作,大大降低了分佈式活鎖的風險。當服務間調用出現波動時,系統不會立即產生大量重試,而是通過可靠的消息隊列和定時補償機制,在較長時間窗口內完成最終一致。
七、併發問題排查通用方法
1. 建立全面的監控指標
2. 掌握線程轉儲分析技術
命令行工具:
jstack -l <PID>: 獲取包含鎖信息的線程轉儲jcmd <PID> Thread.print -l: 同樣可獲取線程轉儲kill -3 <PID>: 在 Linux/Unix 系統中生成線程轉儲
可視化工具:
- VisualVM: 提供圖形化線程分析
- Java Mission Control: 高級監控和分析工具
- Arthas: 阿里開源的 Java 診斷工具
3. 關鍵日誌埋點策略
在易發生併發問題的關鍵點添加日誌:
- 資源獲取前後
- 鎖獲取與釋放
- 狀態變更
- 關鍵業務操作
- 重試操作
確保日誌包含:
- 線程 ID 和名稱:
Thread.currentThread().getId()和Thread.currentThread().getName() - 操作類型
- 資源標識
- 時間戳(精確到毫秒)
- 請求鏈路 ID (使用 MDC 保存和記錄)
// 使用MDC記錄請求鏈路ID,方便分佈式追蹤
// 在微服務架構中,traceId應從上游服務傳遞而來
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
MDC.put("threadId", String.valueOf(Thread.currentThread().getId()));
log.info("Attempting to acquire lock for resource: {}", resourceId);
// 日誌輸出格式:[traceId=abc123][threadId=42] Attempting to acquire lock for resource: order123
4. 併發問題復現技術
單元測試工具:
- JUnit 併發測試工具:
junit-jupiter-params和ConcurrentTestRunner - TestNG 併發測試: 支持
@Test(threadPoolSize=n, invocationCount=m)
@Test
@Execution(ExecutionMode.CONCURRENT)
public void testConcurrentAccess() {
IntStream.range(0, 1000).parallel().forEach(i -> {
sharedService.performOperation(i);
});
}
壓測工具:
- JMeter: 模擬大量用户併發請求
- Gatling: 高性能負載測試工具
- wrk/wrk2: 輕量級 HTTP 基準測試工具
八、Java 併發問題排查與解決方案總結表
| 問題類型 | 表現症狀 | 線程狀態 | 排查工具 | 解決方案 |
|---|---|---|---|---|
| 死鎖 | 服務無響應,線程阻塞 | BLOCKED | jstack, VisualVM | 統一鎖獲取順序,使用鎖超時機制 |
| 活鎖 | CPU 高負載,業務無進展 | RUNNABLE | 線程 Dump 分析,日誌模式 | 指數退避+隨機策略,限制重試次數 |
| 線程飢餓 | 低優先級任務長時間不執行 | RUNNABLE 但無進展 | 線程池監控,任務完成率 | 資源隔離,公平調度策略 |
| 資源耗盡 | 線程數過多,OOM 異常 | 各種狀態 | JVM 監控,GC 日誌 | 線程池大小限制,資源池化 |
| 併發度過高 | 系統抖動,超時增多 | 主要是 RUNNABLE 和 WAITING | 系統負載監控 | 限流,隊列緩衝,擴容 |
| 分佈式活鎖 | 服務間調用成功率低 | 跨進程問題 | 調用鏈追蹤,日誌分析 | 熔斷降級,異步補償 |
併發編程是一門藝術,需要不斷實踐和積累經驗。希望本文的案例分析能幫助你更好地理解和處理 Java 併發問題!
感謝您耐心閲讀到這裏!如果覺得本文對您有幫助,歡迎點贊 👍、收藏 ⭐、分享給需要的朋友,您的支持是我持續輸出技術乾貨的最大動力!
如果想獲取更多 Java 技術深度解析,歡迎點擊頭像關注我,後續會每日更新高質量技術文章,陪您一起進階成長~