动态

详情 返回 返回

Java併發問題排查實戰手冊:死鎖與活鎖診斷與解決全流程 - 动态 详情

一、引言

併發編程就像是在廚房裏同時炒 10 道菜 - 看似效率提高了,但一不小心就會手忙腳亂。作為 Java 後端開發,我們經常為併發問題頭疼不已:生產環境突然卡死,線程 CPU 使用率飆升卻沒有業務進展,各種監控工具報警...而當你想復現問題時,它又像幽靈一樣"按鬧分配",讓人抓狂。

併發 BUG 難以排查的原因主要有三:

  • 不確定性:同樣的代碼,運行 10 次可能只出現 1 次問題
  • 複雜性:多線程交互關係複雜,排查難度指數級增長
  • 上下文依賴:問題往往與特定負載、數據狀態相關

在本文中,我將分享兩個真實項目中遇到的併發問題案例:一個是經典的死鎖導致服務無響應,另一個是不那麼常見但同樣致命的活鎖問題。我們將深入分析問題根源,並展示完整的排查思路和解決方案。

死鎖與活鎖的區別

graph TD
    A[線程狀態異常] --> B{問題類型}
    B -->|無法繼續執行, BLOCKED狀態| C[死鎖]
    B -->|持續執行但無進展, RUNNABLE狀態| D[活鎖]
    B -->|長時間無法獲取資源| E[線程飢餓]

    C -->|特徵| C1[線程互相等待對方釋放資源]
    D -->|特徵| D1[線程能執行但一直處理相同任務]
    E -->|特徵| E1[低優先級線程長時間無法獲得執行]

二、併發問題概覽

常見併發問題類型解析

  1. 死鎖(Deadlock): 兩個或多個線程互相持有對方需要的資源,導致所有線程永久阻塞。想象兩個人分別拿着筷子和碗,都在等對方先放下自己手中的物品。

    死鎖形成的四個必要條件:

    • 互斥條件:資源只能被一個線程佔用
    • 持有並等待:線程已持有一些資源,同時等待其他資源
    • 不可搶佔:資源只能由持有者主動釋放,不能被其他線程強制剝奪
    • 循環等待:線程之間形成環形的資源等待關係
  2. 活鎖(Livelock): 線程雖然沒有阻塞(仍處於 RUNNABLE 狀態),但一直在重複相同的操作而無法推進。就像兩個人在走廊相遇,雙方都想讓對方先過,結果兩人一直左右閃躲但誰都無法通過。

    與死鎖的本質區別:

    • 死鎖中線程處於 BLOCKED 狀態,完全停止活動,CPU 使用率較低
    • 活鎖中線程處於 RUNNABLE 狀態,持續消耗 CPU 資源但無實際進展,導致 CPU 使用率高
  3. 線程飢餓(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");
            }
        }
    }
}

問題分析:在高併發場景下,createOrdercancelOrder方法可能同時執行。當線程 A 執行createOrder並獲取了orderLock鎖,同時線程 B 執行cancelOrder並獲取了inventoryLock鎖時,兩個線程都在等待對方釋放鎖,形成死鎖。回顧死鎖的四個必要條件,這裏全部滿足:

  1. 互斥條件:synchronized 鎖具有互斥性
  2. 持有並等待:兩個線程都持有一個鎖並等待另一個
  3. 不可搶佔:synchronized 鎖不可被搶佔
  4. 循環等待:兩個線程形成了環形等待關係

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 中的十六進制地址0x00000000f4b9dd200x00000000f4b9ddf0分別對應到orderLockinventoryLock,確認具體是哪些鎖對象造成了死鎖。

步驟 2:使用 VisualVM 可視化分析

通過 VisualVM 的線程面板,我們可以更直觀地分析死鎖情況:

  1. 啓動 VisualVM 並連接目標 JVM
  2. 點擊"線程"選項卡
  3. 在右上角過濾器中選擇"檢測死鎖"功能
  4. 查看自動檢測到的死鎖線程組

VisualVM 會以圖形方式展示線程之間的鎖依賴關係:

graph LR
    A[order-processing-thread-12] -->|持有 0x00000000f4b9dd20<br>orderLock| B[orderLock]
    A -->|等待 0x00000000f4b9ddf0<br>inventoryLock| C[inventoryLock]
    D[order-cancel-thread-5] -->|持有 0x00000000f4b9ddf0<br>inventoryLock| C
    D -->|等待 0x00000000f4b9dd20<br>orderLock| B

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 使用率降至正常水平,交易成功率恢復正常。退避策略解釋:

  1. 指數遞增的基礎退避時間:2^attempts 毫秒,隨着重試次數增加而指數增長
  2. 隨機抖動成分:避免多個線程同時重試導致新一輪衝突,抖動範圍隨重試次數增加
  3. 最大退避限制:確保極端情況下等待時間不會超過業務可接受的範圍

臨時止損措施

在代碼修復部署前,可採取以下臨時應急措施:

  1. 服務限流:使用 Sentinel、Hystrix 等工具對支付服務進行限流
  2. 分批處理:調整業務流程,將大批量交易分批次處理
  3. 手動數據修復:對卡在活鎖狀態的交易進行手動狀態修正

五、線程飢餓案例分析

線程飢餓案例

某數據分析系統中,低優先級的報表生成任務長時間無法執行,排查發現原因是線程池配置不當:

// 問題代碼:使用優先級隊列但未考慮低優先級任務長期無法執行的問題
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 參數。公平性主要通過以下兩種方式處理:

  1. 在 Comparable 的實現中,考慮等待時間或飢餓計數,如上例所示
  2. 將優先級和公平性分開處理,例如用獨立線程池或調度器確保各優先級任務都能獲取資源

"飢餓計數器"方案適用於:優先級任務量分佈不均衡,但低優先級任務仍需保證最終執行的場景。例如,實時分析優先,但報表任務不能無限期推遲。

六、分佈式場景中的併發問題

在微服務架構中,併發問題不僅存在於單個 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. 建立全面的監控指標

graph TD
    A[系統監控指標] --> B[線程相關]
    A --> C[資源相關]
    A --> D[業務相關]

    B --> B1[線程數]
    B --> B2[線程狀態分佈]
    B --> B3[線程池隊列長度]

    C --> C1[CPU使用率]
    C --> C2[內存使用]
    C --> C3[I/O等待時間]

    D --> D1[請求響應時間]
    D --> D2[業務處理成功率]
    D --> D3[異常/錯誤數量]

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-paramsConcurrentTestRunner
  • 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 技術深度解析,歡迎點擊頭像關注我,後續會每日更新高質量技術文章,陪您一起進階成長~

user avatar u_16847549 头像 zhouzhenchao 头像 mirrorship 头像 zeran 头像 zjkal 头像 vivo_tech 头像 xiaolvshikong 头像 lenve 头像 zengjingaiguodelang 头像 renzhendezicai 头像
点赞 10 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.