现象描述
前段时间,在生产环境的高峰期(上下班时段),大量用户反馈客户端出现异常:聊天界面持续显示“连接中”,消息发送后一直转圈无法成功。该问题通常持续5分钟到半小时后自动恢复,或者通过重启消息服务立即恢复正常。
问题排查与分析
1. 初步监控指标
首先查看了服务器的基础监控:
- CPU:使用率较平时略有升高,峰值达到20%,但仍在正常范围内。
- 内存:物理内存使用平稳,无明显波动。
- 磁盘I/O:正常,无异常读写。
- GC日志:未发现Full GC,Young GC频率正常。
- 网络I/O:出现故障时,网络吞吐量显著高于正常水平(见下图)。
2. 日志与线程堆栈分析
查阅消息服务日志,发现在给客户端发送消息时,耗时长达十几秒,远超正常阈值。随即采集了故障期间的线程堆栈(使用jstack),发现大量线程阻塞在ArrayBlockingQueue.offer方法上,典型堆栈如下:
"hz.amazing_khayyam.cached.thread-2" #20958003 prio=5 ...
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000005c70b8558> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
...
at java.util.concurrent.ArrayBlockingQueue.offer(ArrayBlockingQueue.java:327)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
...
at com.citic.asp.cmc.net.manager.CimioMessageManager.send(CimioMessageManager.java:85)
从堆栈可见,业务线程在调用CimioMessageManager.send方法发送消息时,该方法内部使用了独立的线程池(sendPool)来异步处理消息发送。而线程池提交任务时,需要将任务放入阻塞队列ArrayBlockingQueue,正是在这个offer操作上发生了阻塞。
3. 线程池配置审视
消息服务中定义了多个线程池,分别用于不同的功能:
sendPool:coreSize = 500,maxSize = 500,阻塞队列为ArrayBlockingQueue,容量10000。- 类似还有
connectPool、connClosePool、authPool、receivePool、deplayPool(其中deplayPool的core/maxSize = 1000)。 - 加上其他业务线程,整个JVM进程的线程总数高达4000+。
此时查看sendPool中线程的状态,大多如下:
"send-pool-459" #1842 prio=5 ...
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000005c70b8578> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(...)
at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
...
这表明线程池中大量线程处于空闲等待状态,没有在执行任务。但另一方面,通过MAT(Memory Analyzer Tool)分析堆转储文件,使用OQL查询ArrayBlockingQueue实例,发现sendPool的队列中居然积压了13个待执行任务。
这就出现了矛盾:队列中有任务等待,而线程池中大部分线程却处于空闲状态。任务为什么没有被及时消费?
4. 深入剖析阻塞原因
4.1 线程池工作流程回顾
我们先回顾一下ThreadPoolExecutor处理任务的逻辑(结合下图理解):
- 当工作线程数小于coreSize时,创建新线程执行任务。
- 当工作线程数达到coreSize,后续任务被放入阻塞队列。
- 当队列满且线程数小于maxSize时,继续创建新线程(临时线程)。
- 当队列满且线程数达到maxSize时,执行拒绝策略。
在本案例中,coreSize = maxSize = 500,因此线程池始终维持500个线程。所有提交的任务都会被放入ArrayBlockingQueue,等待这500个线程消费。
4.2 ArrayBlockingQueue的锁竞争
ArrayBlockingQueue内部使用**同一把ReentrantLock**来保护入队(offer/put)和出队(poll/take)操作。这把锁控制着队列的并发访问。当大量线程同时尝试入队或出队时,锁竞争变得异常激烈。
通过MAT进一步查看该ArrayBlockingQueue的lock对象,发现其内部的AQS(AbstractQueuedSynchronizer)等待队列中居然有大量线程在排队等待获取锁(见下图)。
这意味着:虽然线程池中有500个工作线程,但其中大多数都在争夺ArrayBlockingQueue的锁,而不是真正执行任务。真正能拿到锁的线程才能从队列中取出任务执行,而拿不到锁的线程只能阻塞在AQS队列中,造成“线程饥饿”。
4.3 线程数量过多加剧锁竞争
为什么会出现如此严重的锁竞争?关键在于线程总数远超CPU核心数。
假设服务器CPU为8核,那么同时只能有8个线程真正运行。但JVM中却有4000多个可运行线程(包括所有线程池)。操作系统采用时间片轮转调度,每个线程获得CPU时间片的机会均等。当线程数量巨大时,每个线程被调度到的频率极低,导致:
- 持有锁的线程可能刚释放锁就被迫让出CPU,无法及时唤醒下一个等待线程。
- 大量线程在AQS队列中频繁地被挂起和唤醒,上下文切换开销剧增。
- 最终,即使队列中有任务,消费速度也远跟不上生产速度,吞吐量急剧下降。
4.4 代码模拟复现
为了验证上述推理,我们编写了一个Demo程序,模拟500个工作线程竞争ArrayBlockingQueue锁的场景。代码如下:
/**
* ArrayBlockingQueue线程饥饿问题复现Demo
*
* 场景模拟:
* 1. 创建500个工作线程从ArrayBlockingQueue中消费任务
* 2. ArrayBlockingQueue使用单个ReentrantLock控制并发
* 3. 通过监控数据展示锁竞争如何导致吞吐量下降
*/
public class ArrayBlockingQueueStarvationDemo {
// 配置参数
private static final int THREAD_POOL_SIZE = 500; // 线程池大小
private static final int QUEUE_CAPACITY = 10000; // 队列容量
private static final int TOTAL_TASKS = 100000; // 总任务数
private static final int TASK_PROCESS_TIME_MS = 10; // 每个任务处理时间(毫秒)
// 监控指标
private static final AtomicInteger tasksCompleted = new AtomicInteger(0);
private static final AtomicInteger lockContentionCount = new AtomicInteger(0);
private static final AtomicInteger longestWaitTimeMs = new AtomicInteger(0);
private static volatile boolean running = true;
// 自定义ArrayBlockingQueue,用于监控锁竞争
static class MonitoredArrayBlockingQueue<T> extends ArrayBlockingQueue<T> {
private final ReentrantLock monitorLock;
private long lastLockAcquireTime;
public MonitoredArrayBlockingQueue(int capacity) {
super(capacity);
try {
// 通过反射获取父类的lock字段
java.lang.reflect.Field lockField = ArrayBlockingQueue.class.getDeclaredField("lock");
lockField.setAccessible(true);
monitorLock = (ReentrantLock) lockField.get(this);
} catch (Exception e) {
throw new RuntimeException("Failed to get lock field", e);
}
}
@Override
public T poll(long timeout, TimeUnit unit) throws InterruptedException {
long startWait = System.currentTimeMillis();
T result = super.poll(timeout, unit);
long endWait = System.currentTimeMillis();
int waitTime = (int)(endWait - startWait);
if (waitTime > 0) {
lockContentionCount.incrementAndGet();
longestWaitTimeMs.updateAndGet(prev -> Math.max(prev, waitTime));
}
return result;
}
@Override
public boolean offer(T t) {
long startWait = System.currentTimeMillis();
boolean result = super.offer(t);
long endWait = System.currentTimeMillis();
int waitTime = (int)(endWait - startWait);
if (waitTime > 0) {
lockContentionCount.incrementAndGet();
longestWaitTimeMs.updateAndGet(prev -> Math.max(prev, waitTime));
}
return result;
}
@Override
public T take() throws InterruptedException {
long startWait = System.currentTimeMillis();
T result = super.take();
long endWait = System.currentTimeMillis();
int waitTime = (int)(endWait - startWait);
if (waitTime > 0) {
lockContentionCount.incrementAndGet();
longestWaitTimeMs.updateAndGet(prev -> Math.max(prev, waitTime));
}
return result;
}
public int getAQSQueueLength() {
// 尝试获取AQS队列长度(通过反射)
try {
java.lang.reflect.Field syncField = ReentrantLock.class.getDeclaredField("sync");
syncField.setAccessible(true);
Object sync = syncField.get(monitorLock);
java.lang.reflect.Field headField = sync.getClass()
.getSuperclass()
.getSuperclass()
.getDeclaredField("head");
headField.setAccessible(true);
Object head = headField.get(sync);
// 遍历队列计算长度
int length = 0;
java.lang.reflect.Field nextField = null;
while (head != null) {
length++;
if (nextField == null) {
nextField = head.getClass().getDeclaredField("next");
nextField.setAccessible(true);
}
head = nextField.get(head);
}
return length;
} catch (Exception e) {
System.err.println(e.getMessage());
return -1; // 无法获取
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("============================================================================================");
System.out.println("ArrayBlockingQueue 线程饥饿问题复现Demo");
System.out.println("============================================================================================");
System.out.println("配置:");
System.out.println(" 工作线程数: " + THREAD_POOL_SIZE);
System.out.println(" 队列容量: " + QUEUE_CAPACITY);
System.out.println(" 总任务数: " + TOTAL_TASKS);
System.out.println(" 任务处理时间: " + TASK_PROCESS_TIME_MS + "ms");
System.out.println();
// 创建监控队列
MonitoredArrayBlockingQueue<Runnable> taskQueue = new MonitoredArrayBlockingQueue<>(QUEUE_CAPACITY);
// 创建线程池(使用自定义队列)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
THREAD_POOL_SIZE, // 核心线程数
THREAD_POOL_SIZE, // 最大线程数
0L, TimeUnit.MILLISECONDS,
taskQueue,
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 预启动所有核心线程
executor.prestartAllCoreThreads();
// 启动监控线程
Thread monitorThread = startMonitor(executor, taskQueue);
// 记录开始时间
long startTime = System.currentTimeMillis();
// 提交任务(模拟快速提交)
System.out.println("开始提交任务...");
CountDownLatch taskSubmitLatch = new CountDownLatch(TOTAL_TASKS);
Thread producerThread = new Thread(() -> {
for (int i = 0; i < TOTAL_TASKS; i++) {
final int taskId = i;
try {
// 创建任务
Runnable task = () -> {
try {
// 模拟任务处理
Thread.sleep(TASK_PROCESS_TIME_MS);
// 记录完成的任务
int completed = tasksCompleted.incrementAndGet();
if (completed % 1000 == 0) {
System.out.println("已完成任务: " + completed + "/" + TOTAL_TASKS);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
taskSubmitLatch.countDown();
}
};
// 提交任务(如果队列满则阻塞)
executor.submit(task);
// 控制提交速度,快速填满队列
if (i < QUEUE_CAPACITY * 2) {
Thread.sleep(1); // 快速提交
}
} catch (InterruptedException e) {
break;
}
}
}, "Producer-Thread");
producerThread.start();
// 等待所有任务提交
producerThread.join();
System.out.println("所有任务已提交,等待执行完成...");
// 等待所有任务完成
taskSubmitLatch.await();
// 记录结束时间
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
// 停止监控
running = false;
monitorThread.join();
// 关闭线程池
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
// 打印结果分析
printAnalysis(totalTime);
}
private static Thread startMonitor(ThreadPoolExecutor executor,
MonitoredArrayBlockingQueue<Runnable> taskQueue) {
Thread monitorThread = new Thread(() -> {
int lastCompleted = 0;
long lastTime = System.currentTimeMillis();
System.out.println("\n开始监控...");
System.out.println("时间(s) | 队列大小 | 活跃线程 | 已完成 | 吞吐量(任务/秒) | AQS队列长度 | 锁竞争次数");
System.out.println("-------|----------|----------|--------|----------------|------------|----------");
while (running) {
try {
Thread.sleep(1000); // 每秒监控一次
int currentCompleted = tasksCompleted.get();
long currentTime = System.currentTimeMillis();
long elapsedSeconds = (currentTime - lastTime) / 1000;
int throughput = 0;
if (elapsedSeconds > 0) {
throughput = (currentCompleted - lastCompleted) / (int)elapsedSeconds;
}
// 获取AQS队列长度
int aqsQueueLength = taskQueue.getAQSQueueLength();
System.out.printf("%6d | %8d | %8d | %6d | %14d | %10d | %10d\n",
(currentTime - System.currentTimeMillis() + 10000) / 1000, // 运行时间估算
executor.getQueue().size(),
executor.getActiveCount(),
currentCompleted,
throughput,
aqsQueueLength,
lockContentionCount.get()
);
lastCompleted = currentCompleted;
lastTime = currentTime;
// 检测可能的死锁/饥饿
if (executor.getQueue().size() > 0 &&
executor.getActiveCount() < THREAD_POOL_SIZE / 10 &&
throughput < 10) {
System.out.println("\n⚠️ ⚠️ ⚠️ 警告:检测到可能的线程饥饿!");
System.out.println(" 队列中有任务但活跃线程很少,吞吐量极低");
}
} catch (InterruptedException e) {
break;
}
}
}, "Monitor-Thread");
monitorThread.start();
return monitorThread;
}
private static void printAnalysis(long totalTime) {
System.out.println("============================================================================================");
System.out.println("结果分析");
System.out.println("============================================================================================");
System.out.println("总执行时间: " + totalTime + "ms");
System.out.println("总完成任务: " + tasksCompleted.get());
System.out.println("平均吞吐量: " + (TOTAL_TASKS * 1000.0 / totalTime) + " 任务/秒");
System.out.println("锁竞争总次数: " + lockContentionCount.get());
System.out.println("最长等待时间: " + longestWaitTimeMs.get() + "ms");
// 理论计算
double idealTime = TOTAL_TASKS * TASK_PROCESS_TIME_MS / (double)THREAD_POOL_SIZE;
System.out.println("\n理论分析:");
System.out.printf(" 理想执行时间(无竞争): %.2fms\n", idealTime);
System.out.printf(" 实际执行时间: %dms\n", totalTime);
System.out.printf(" 性能下降倍数: %.2fx\n", totalTime / idealTime);
System.out.println("\n结论:");
if (totalTime > idealTime * 5) {
System.out.println("✅ 成功复现线程饥饿问题!");
System.out.println(" 由于ArrayBlockingQueue的单锁设计,500个线程竞争同一把锁");
System.out.println(" 导致大量线程在AQS队列中等待,而不是执行任务");
System.out.println(" 最终造成吞吐量严重下降");
} else {
System.out.println("❌ 未明显检测到线程饥饿问题");
System.out.println(" 可能原因:机器性能过好,或竞争不够激烈");
System.out.println(" 尝试:增加线程数、减少任务处理时间、增加任务数");
}
}
}
运行该Demo(机器为8核),输出显示总执行时间远大于理论理想时间,吞吐量严重下降,且锁竞争次数极高。这复现了生产环境的现象。
结论
本次生产故障的根本原因是:线程池核心线程数设置过大(500),远超CPU核心数,导致大量线程激烈竞争ArrayBlockingQueue的同一把锁,引发线程饥饿,最终造成消息发送吞吐量急剧下降。
即使队列中积压任务不多,但由于锁竞争激烈,大部分工作线程处于锁等待状态,无法有效消费任务,从而拖慢整体处理速度。
解决方案与优化建议
针对上述问题,我们提出以下优化措施:
1. 合理设置线程池大小
根据《Java并发编程实战》的建议,对于CPU密集型任务,线程数应设置为 CPU核心数 + 1;对于I/O密集型任务,可适当增加,但不宜过大。消息服务属于混合型,建议将核心线程数调整为CPU核心数的2倍左右(例如16~32),而非500。
调整后,线程数量大幅减少,锁竞争自然缓解,同时充分利用CPU资源。
2. 选择合适的阻塞队列
ArrayBlockingQueue的单一锁设计在高并发下容易成为瓶颈。可以考虑替换为**LinkedBlockingQueue,其内部使用两把锁(takeLock和putLock),分别控制出队和入队,可减少锁竞争。此外,也可以考虑使用ConcurrentLinkedQueue**(但需注意它不阻塞,需配合SynchronousQueue等使用)。
3. 使用有界队列并监控队列大小
无论使用何种队列,都建议设置为有界,避免任务无限积压导致内存溢出。同时,对队列大小、线程池活跃度等指标进行实时监控,设置合理的告警阈值。
4. 线程池隔离与共享
避免为每个功能创建独立的庞大线程池。可以将一些非关键路径的线程池合并,或采用共享线程池配合任务优先级处理,减少总体线程数。
5. 性能压测与容量规划
在上线前进行充分的压力测试,模拟高峰流量,观察线程池行为、锁竞争情况,从而确定最佳参数。
总结
线程池是Java并发编程中的强大工具,但参数设置不当同样会引发严重性能问题。本文通过一次真实的生产故障,深入剖析了线程数过多与ArrayBlockingQueue锁竞争共同导致的线程饥饿现象。希望这次案例能帮助读者在实际工作中避免类似陷阱,合理配置线程池,确保系统稳定高效。