一、引言
并发编程就像是在厨房里同时炒 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 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。