一、引言

并发编程就像是在厨房里同时炒 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 并发问题排查与解决方案总结表

问题类型表现症状线程状态排查工具解决方案
死锁服务无响应,线程阻塞BLOCKEDjstack, VisualVM统一锁获取顺序,使用锁超时机制
活锁CPU 高负载,业务无进展RUNNABLE线程 Dump 分析,日志模式指数退避+随机策略,限制重试次数
线程饥饿低优先级任务长时间不执行RUNNABLE 但无进展线程池监控,任务完成率资源隔离,公平调度策略
资源耗尽线程数过多,OOM 异常各种状态JVM 监控,GC 日志线程池大小限制,资源池化
并发度过高系统抖动,超时增多主要是 RUNNABLE 和 WAITING系统负载监控限流,队列缓冲,扩容
分布式活锁服务间调用成功率低跨进程问题调用链追踪,日志分析熔断降级,异步补偿

并发编程是一门艺术,需要不断实践和积累经验。希望本文的案例分析能帮助你更好地理解和处理 Java 并发问题!


感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!

如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~


异常君
4 声望3 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!