线程池监控调优与排查案例
五、线程池监控和调优
5.1 线程池核心参数详解
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)参数详解:
-
corePoolSize:
- 核心线程数,线程池维护的最小线程数
- 即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut)
- 建议:CPU 密集型 = CPU 核心数 + 1,IO 密集型 = 2 * CPU 核心数
-
maximumPoolSize:
- 最大线程数,队列满时创建的最大线程数
- 建议:根据业务流量峰值设置,不要过大
-
keepAliveTime:
- 空闲线程存活时间,超过核心线程数的线程空闲时会被回收
- 建议:根据任务提交频率设置
-
workQueue:
- 工作队列,存储等待执行的任务
- 建议:使用有界队列,避免 OOM
-
threadFactory:
- 线程工厂,自定义线程名称、优先级、守护线程等
- 建议:使用 Guava 的 ThreadFactoryBuilder
-
handler:
- 拒绝策略,队列满且线程数达到最大时的处理策略
- 建议:根据业务需求选择合适的策略
拒绝策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| AbortPolicy | 抛出 RejectedExecutionException | 默认策略,需要处理异常 |
| CallerRunsPolicy | 由调用线程执行任务 | 不希望丢失任务 |
| DiscardPolicy | 直接丢弃任务,不抛异常 | 允许丢弃任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务 | 允许丢弃旧任务 |
5.2 线程池监控指标
public class ThreadPoolMonitor {
private ThreadPoolExecutor executor;
public void printThreadPoolStats() {
System.out.println("核心线程数: " + executor.getCorePoolSize());
System.out.println("最大线程数: " + executor.getMaximumPoolSize());
System.out.println("当前线程数: " + executor.getPoolSize());
System.out.println("活跃线程数: " + executor.getActiveCount());
System.out.println("历史最大线程数: " + executor.getLargestPoolSize());
System.out.println("已完成任务数: " + executor.getCompletedTaskCount());
System.out.println("总任务数: " + executor.getTaskCount());
System.out.println("队列大小: " + executor.getQueue().size());
System.out.println("队列剩余容量: " + executor.getQueue().remainingCapacity());
// 计算线程池使用率
double usage = (double) executor.getActiveCount() / executor.getMaximumPoolSize() * 100;
System.out.println("线程池使用率: " + String.format("%.2f", usage) + "%");
// 计算队列使用率
double queueUsage = (double) executor.getQueue().size() /
(executor.getQueue().size() + executor.getQueue().remainingCapacity()) * 100;
System.out.println("队列使用率: " + String.format("%.2f", queueUsage) + "%");
}
}自定义监控线程池:
public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {
private final AtomicLong totalTaskTime = new AtomicLong();
private final AtomicLong totalTaskCount = new AtomicLong();
public MonitoredThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
System.out.println("任务开始执行: " + t.getName() + ", 时间: " + new Date());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("任务执行完成, 时间: " + new Date());
if (t != null) {
System.out.println("任务执行异常: " + t.getMessage());
t.printStackTrace();
}
}
@Override
protected void terminated() {
super.terminated();
System.out.println("线程池已终止");
}
public long getTotalTaskTime() {
return totalTaskTime.get();
}
public long getTotalTaskCount() {
return totalTaskCount.get();
}
public double getAverageTaskTime() {
long count = totalTaskCount.get();
return count == 0 ? 0 : (double) totalTaskTime.get() / count;
}
}接入监控系统:
public class ThreadPoolMetrics {
private final ThreadPoolExecutor executor;
private final String poolName;
public ThreadPoolMetrics(ThreadPoolExecutor executor, String poolName) {
this.executor = executor;
this.poolName = poolName;
startMonitor();
}
private void startMonitor() {
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
// 上报到监控系统(如 Prometheus、Micrometer)
Metrics.gauge("thread.pool.active", executor.getActiveCount(), "pool", poolName);
Metrics.gauge("thread.pool.size", executor.getPoolSize(), "pool", poolName);
Metrics.gauge("thread.pool.queue.size", executor.getQueue().size(), "pool", poolName);
Metrics.gauge("thread.pool.completed", executor.getCompletedTaskCount(), "pool", poolName);
// 告警逻辑
if (executor.getQueue().size() > 100) {
AlertService.sendAlert("线程池[" + poolName + "]队列积压: " + executor.getQueue().size());
}
double usage = (double) executor.getActiveCount() / executor.getMaximumPoolSize();
if (usage > 0.8) {
AlertService.sendAlert("线程池[" + poolName + "]使用率过高: " + usage);
}
}, 0, 1, TimeUnit.SECONDS);
}
}5.3 线程池调优策略
1. 合理设置线程数:
// CPU 密集型任务
int cpuCount = Runtime.getRuntime().availableProcessors();
int corePoolSize = cpuCount + 1;
// IO 密集型任务
int corePoolSize = cpuCount * 2;
// 混合型任务
// 需要根据 CPU 时间和 IO 时间的比例调整
// 公式: 线程数 = CPU 核心数 * (1 + 等待时间 / 计算时间)
int corePoolSize = (int) (cpuCount * (1 + waitTime / computeTime));2. 选择合适的队列:
// 有界队列: 防止 OOM,但可能触发拒绝策略
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
// 无界队列: 不会触发拒绝策略,但可能导致 OOM
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
// 同步队列: 直接提交,不缓冲
SynchronousQueue<Runnable> queue = new SynchronousQueue<>();
// 优先级队列: 按优先级执行
PriorityBlockingQueue<Runnable> queue = new PriorityBlockingQueue<>();3. 合理设置拒绝策略:
// 自定义拒绝策略
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志
System.out.println("任务被拒绝: " + r.toString());
// 尝试重新提交
try {
if (!executor.getQueue().offer(r, 5, TimeUnit.SECONDS)) {
// 最终处理: 记录到数据库或消息队列
saveToDatabase(r);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
saveToDatabase(r);
}
}
private void saveToDatabase(Runnable r) {
// 保存到数据库,后续重试
}
}
// 使用自定义拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new CustomRejectedExecutionHandler()
);4. 线程池隔离:
// 不同业务使用不同的线程池
public class ThreadPoolConfig {
// 订单业务线程池
public ThreadPoolExecutor orderPool = new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build()
);
// 支付业务线程池
public ThreadPoolExecutor paymentPool = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(50),
new ThreadFactoryBuilder().setNameFormat("payment-pool-%d").build()
);
// 通知业务线程池
public ThreadPoolExecutor notificationPool = new ThreadPoolExecutor(
3, 5, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(20),
new ThreadFactoryBuilder().setNameFormat("notification-pool-%d").build()
);
}5. 动态调整线程池参数:
public class DynamicThreadPool {
private ThreadPoolExecutor executor;
public void adjustThreadPool(int corePoolSize, int maxPoolSize) {
// 先调整最大线程数(必须大于等于核心线程数)
if (maxPoolSize < executor.getCorePoolSize()) {
executor.setMaximumPoolSize(executor.getCorePoolSize());
executor.setCorePoolSize(corePoolSize);
executor.setMaximumPoolSize(maxPoolSize);
} else {
executor.setCorePoolSize(corePoolSize);
executor.setMaximumPoolSize(maxPoolSize);
}
System.out.println("线程池调整: corePoolSize=" + corePoolSize + ", maxPoolSize=" + maxPoolSize);
}
// 根据负载动态调整
public void autoAdjust() {
double usage = (double) executor.getActiveCount() / executor.getMaximumPoolSize();
if (usage > 0.8 && executor.getQueue().size() > 50) {
// 负载高,扩容
int newCorePoolSize = Math.min(executor.getCorePoolSize() * 2, 50);
int newMaxPoolSize = Math.min(executor.getMaximumPoolSize() * 2, 100);
adjustThreadPool(newCorePoolSize, newMaxPoolSize);
} else if (usage < 0.3) {
// 负载低,缩容
int newCorePoolSize = Math.max(executor.getCorePoolSize() / 2, 5);
int newMaxPoolSize = Math.max(executor.getMaximumPoolSize() / 2, 10);
adjustThreadPool(newCorePoolSize, newMaxPoolSize);
}
}
}六、完整的排查案例
6.1 案例 1:CPU 飙高问题排查
现象:
- 线上服务 CPU 使用率突然飙升至 90%+
- 接口响应时间变长
- 用户反馈页面加载慢
排查步骤:
# 1. 找到 CPU 使用率高的进程
top
# 假设 PID 为 12345
# 2. 查看该进程中的线程
top -H -p 12345
# 发现 PID 为 12350、12351 的线程 CPU 使用率高
# 3. 将线程 PID 转换为十六进制
printf "%x\n" 12350
# 输出: 303e
# 4. 使用 jstack 查看线程堆栈
jstack 12345 | grep -A 20 303e
# 5. 分析线程堆栈线程堆栈分析:
"http-nio-8080-exec-1" #25 daemon prio=5 os_prio=0 tid=0x00007f8a9c001000 nid=0x303e runnable [0x00007f8a8c1f8000]
java.lang.Thread.State: RUNNABLE
at com.example.UserService.calculateScore(UserService.java:123)
at com.example.ScoreController.getScore(ScoreController.java:45)
...问题定位:
calculateScore方法存在死循环或复杂计算- 代码中有
while(true)循环没有退出条件
问题代码:
public int calculateScore(Long userId) {
int score = 0;
while (true) { // 死循环
score += getUserScore(userId);
if (score > 1000) {
break;
}
}
return score;
}修复方案:
public int calculateScore(Long userId) {
int score = 0;
int retryCount = 0;
int maxRetry = 10; // 添加最大重试次数
while (retryCount < maxRetry) {
score += getUserScore(userId);
if (score > 1000) {
break;
}
retryCount++;
}
return score;
}6.2 案例 2:死锁问题排查
现象:
- 某个转账接口偶发卡死
- 接口一直等待,没有响应
- 重启后问题消失,一段时间后又出现
排查步骤:
# 1. 使用 jstack 检测死锁
jstack -l <pid> | grep -A 10 "Found one Java-level deadlock"jstack 输出:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8a9c0034c8 (object 0x00000000e1a2a8c0, a com.example.Account),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00007f8a9c0032c8 (object 0x00000000e1a2a8d0, a com.example.Account),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.example.TransferService.transfer(TransferService.java:25)
- waiting to lock <0x00000000e1a2a8c0> (a com.example.Account)
- locked <0x00000000e1a2a8d0> (a com.example.Account)
"Thread-2":
at com.example.TransferService.transfer(TransferService.java:25)
- waiting to lock <0x00000000e1a2a8d0> (a com.example.Account)
- locked <0x00000000e1a2a8c0> (a com.example.Account)问题代码:
public class TransferService {
public void transfer(Account from, Account to, BigDecimal amount) {
synchronized (from) {
synchronized (to) {
from.debit(amount);
to.credit(amount);
}
}
}
}问题分析:
- 线程 1: A → B 转账,先锁 A,再锁 B
- 线程 2: B → A 转账,先锁 B,再锁 A
- 形成循环等待,导致死锁
修复方案 1:按相同顺序加锁:
public void transfer(Account from, Account to, BigDecimal amount) {
// 按 ID 排序,确保所有线程按相同顺序获取锁
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
from.debit(amount);
to.credit(amount);
}
}
}修复方案 2:使用全局锁:
private final Object globalLock = new Object();
public void transfer(Account from, Account to, BigDecimal amount) {
synchronized (globalLock) {
synchronized (from) {
synchronized (to) {
from.debit(amount);
to.credit(amount);
}
}
}
}修复方案 3:使用 Lock 的 tryLock:
public void transfer(Account from, Account to, BigDecimal amount) {
Lock fromLock = from.getLock();
Lock toLock = to.getLock();
while (true) {
try {
if (fromLock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (toLock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
from.debit(amount);
to.credit(amount);
return;
} finally {
toLock.unlock();
}
}
} finally {
fromLock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("转账被中断", e);
}
}
}6.3 案例 3:线程池堆积问题排查
现象:
- 接口响应时间逐渐变长
- 线程池队列持续增长
- 最终触发拒绝策略
排查步骤:
# 1. 使用 Arthas 查看线程池状态
thread -n 3
# 2. 查看线程堆栈
jstack <pid> | grep -A 20 "http-nio"
# 3. 使用 Arthas 监控方法执行时间
trace com.example.UserService getUser问题定位:
// 线程池配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数
10, // 最大线程数
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), // 无界队列
new ThreadPoolExecutor.AbortPolicy()
);
// 任务提交速度 > 处理速度
for (int i = 0; i < 100000; i++) {
executor.submit(() -> {
// 慢任务
Thread.sleep(1000);
});
}问题分析:
- 使用无界队列,任务不断积压
- 任务处理慢,队列无限增长
- 最终导致 OOM
修复方案:
// 1. 使用有界队列
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,
20, // 增加最大线程数
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadFactoryBuilder().setNameFormat("worker-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 调用者执行策略
);
// 2. 添加监控
new Thread(() -> {
while (true) {
System.out.println("活跃线程: " + executor.getActiveCount());
System.out.println("队列大小: " + executor.getQueue().size());
if (executor.getQueue().size() > 80) {
AlertService.sendAlert("线程池队列积压告警");
}
Thread.sleep(1000);
}
}).start();
// 3. 添加熔断
if (executor.getQueue().size() > 90) {
throw new RuntimeException("系统繁忙,请稍后重试");
}6.4 案例 4:内存泄漏问题排查
现象:
- JVM 内存使用持续增长
- GC 频繁,但内存不下降
- 最终 OOM
排查步骤:
# 1. 使用 jmap 查看堆内存使用
jmap -heap <pid>
# 2. 导出堆 dump
jmap -dump:format=b,file=heap.hprof <pid>
# 3. 使用 VisualVM 或 MAT 分析堆 dumpVisualVM 分析:
1. 打开 VisualVM,加载 heap.hprof
2. 点击"类"标签,按实例数排序
3. 发现 UserSession 实例数异常多
4. 右键点击 UserSession,选择"在堆中显示"
5. 查看引用链,找到 GC Root问题代码:
public class SessionManager {
private static final Map<String, UserSession> sessions = new HashMap<>();
public void createSession(String sessionId, UserSession session) {
sessions.put(sessionId, session);
}
// 没有 remove 方法,session 不会被清理
}修复方案:
public class SessionManager {
// 使用 ConcurrentHashMap + 定时清理
private static final ConcurrentHashMap<String, UserSession> sessions = new ConcurrentHashMap<>();
public void createSession(String sessionId, UserSession session) {
sessions.put(sessionId, session);
}
public void removeSession(String sessionId) {
sessions.remove(sessionId);
}
// 定时清理过期 session
public void cleanExpiredSessions() {
sessions.entrySet().removeIf(entry ->
entry.getValue().isExpired()
);
}
// 使用 Guava Cache
private static final Cache<String, UserSession> sessions = CacheBuilder.newBuilder()
.expireAfterAccess(30, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
}七、常见问题和面试要点
7.1 线程安全基础问题
Q1: 什么是线程安全?如何判断代码是否线程安全?
线程安全是指:当多个线程访问同一个对象时,如果不需要考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果。
判断标准:
- 是否有共享可变状态
- 是否有复合操作
- 是否有可见性问题
- 是否有有序性问题
Q2: synchronized 和 Lock 有什么区别?
| 特性 | synchronized | Lock |
|---|---|---|
| 实现方式 | JVM 关键字 | API 调用 |
| 锁获取/释放 | 自动 | 手动 |
| 可中断性 | 不可中断 | 可中断 |
| 公平性 | 非公平 | 可选 |
| 条件变量 | 单一 | 多个 |
| 性能 | 优化后差距不大 | 竞争激烈时略优 |
Q3: volatile 能保证线程安全吗?
volatile 只能保证可见性和有序性,不能保证原子性。适用场景:
- 单一变量的读写
- 状态标记位
- 双重检查锁定
Q4: ThreadLocal 会导致内存泄漏吗?如何避免?
会。ThreadLocal 的 Entry 中 key 是弱引用,value 是强引用。如果线程长期存活(如线程池),value 不会被回收。
避免方法:
- 使用完调用 remove()
- 在 finally 块中清理
- 线程池中要特别注意
7.2 并发工具问题
Q5: ConcurrentHashMap 如何保证线程安全?
Java 7: 分段锁(Segment),默认 16 个段 Java 8+: CAS + synchronized,锁粒度更细
Q6: CopyOnWriteArrayList 的原理和适用场景?
原理:写时复制,每次写操作都复制整个数组 适用场景:读多写少 缺点:写性能低、内存占用高、数据一致性弱
Q7: BlockingQueue 的常用实现有哪些?
- ArrayBlockingQueue: 有界阻塞队列
- LinkedBlockingQueue: 可选有界/无界
- SynchronousQueue: 无缓冲,直接传递
- PriorityBlockingQueue: 优先级队列
- DelayQueue: 延迟队列
7.3 线程池问题
Q8: 线程池的核心参数有哪些?如何配置?
核心参数:corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler
配置建议:
- CPU 密集型: CPU 核心数 + 1
- IO 密集型: 2 * CPU 核心数
- 混合型: 根据等待时间/计算时间比例调整
Q9: 线程池的拒绝策略有哪些?
- AbortPolicy: 抛异常
- CallerRunsPolicy: 调用者执行
- DiscardPolicy: 直接丢弃
- DiscardOldestPolicy: 丢弃最老任务
Q10: 如何监控线程池?
监控指标:
- activeCount: 活跃线程数
- poolSize: 当前线程数
- queueSize: 队列大小
- completedTaskCount: 已完成任务数
监控方式:
- 自定义 ThreadPoolExecutor,重写 beforeExecute、afterExecute
- 定时任务采集指标
- 接入监控系统(Prometheus、Micrometer)
7.4 排查工具问题
Q11: 如何使用 jstack 排查 CPU 飙高问题?
# 1. 找到 CPU 使用率高的进程
top
# 2. 查看进程中的线程
top -H -p <pid>
# 3. 将线程 PID 转为十六进制
printf "%x\n" <tid>
# 4. 使用 jstack 查看
jstack <pid> | grep -A 20 <hex_tid>Q12: 如何使用 Arthas 排查慢接口?
# 1. 监控方法执行时间
trace com.example.UserService getUser
# 2. 查看方法调用参数和返回值
watch com.example.UserService getUser '{params, returnObj}' -x 2
# 3. 查看线程堆栈
thread -n 3Q13: 如何检测死锁?
# 方法 1: jstack
jstack -l <pid> | grep -A 10 "Found one Java-level deadlock"
# 方法 2: jconsole
# 点击"线程"标签,点击"检测死锁"
# 方法 3: Arthas
thread -b7.5 高级问题
Q14: 什么是 ABA 问题?如何解决?
ABA 问题:变量值从 A 变为 B,又变回 A,CAS 操作无法检测到变化。
解决方法:使用 AtomicStampedReference,添加版本号
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);
// 更新时检查版本
int[] stampHolder = new int[1];
Integer value = ref.get(stampHolder);
ref.compareAndSet(value, 2, stampHolder[0], stampHolder[0] + 1);Q15: 如何实现一个线程安全的单例?
// 方法 1: 双重检查锁定
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
// 方法 2: 静态内部类
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
// 方法 3: 枚举
public enum Singleton {
INSTANCE;
public void doSomething() {
// ...
}
}Q16: 如何实现一个线程安全的计数器?
// 方法 1: AtomicInteger
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
// 方法 2: LongAdder(高并发场景)
public class LongAdderCounter {
private LongAdder count = new LongAdder();
public void increment() {
count.increment();
}
public long getCount() {
return count.sum();
}
}
// 方法 3: synchronized
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}八、总结
8.1 线程安全核心要点
- 三大特性:原子性、可见性、有序性
- 实现方式:不可变对象、同步机制、并发容器、ThreadLocal
- 问题类型:竞态条件、死锁、活锁、饥饿、线程池堆积
- 排查工具:jstack、jconsole、VisualVM、Arthas
8.2 最佳实践
- 尽量减少共享可变状态
- 使用高层并发工具而非底层同步原语
- 用状态机、唯一键、原子操作代替脆弱的先查后改
- 锁范围只覆盖真正的临界区
- 对线程池、队列、拒绝策略做指标和告警
- 不要把"加锁"当成唯一并发治理手段
8.3 排查思路
并发问题排查思路:
1. 看现象
├── 数据不一致 → 竞态条件、原子性
├── RT 飙升 → 死锁、阻塞、线程池堆积
├── CPU 高但吞吐下降 → 锁竞争、资源争抢
└── 内存持续增长 → 内存泄漏
2. 找原因
├── jstack → 线程堆栈分析
├── jconsole/VisualVM → 图形化监控
├── Arthas → 动态诊断
└── jmap → 内存分析
3. 定位代码
├── 分析线程堆栈
├── 追踪方法调用
├── 查看对象引用链
└── 分析锁竞争
4. 修复问题
├── 加锁/优化锁
├── 使用并发工具
├── 调整线程池参数
└── 修复内存泄漏8.4 常见代码味道
// × 危险代码
if (!orderRepository.exists(orderNo)) {
orderRepository.save(new Order(orderNo));
}
// 安全代码
// 方法 1: 数据库唯一键
// 方法 2: 分布式锁
// 方法 3: 原子操作
// 方法 4: 状态机并发编程是 Java 开发中最重要的技能之一,掌握线程安全和并发问题排查方法,对于构建高性能、高可用的系统至关重要。希望本文能够帮助读者深入理解并发编程的核心概念和实战技巧。
版本差异(旧版 → Java 21)
| 特性 | 旧版(Java 8/11) | Java 21 |
|---|---|---|
| 线程池监控 | 线程数/队列/拒绝数 | 不变;新增虚拟线程调度器指标 |
| 高并发 IO 排查 | 线程池堆积排查 | 新增虚拟线程耗尽/针垫效应排查 |
| 断点工具 | jstack/jmap | 不变;jcmd 能力增强 |
| 堆栈识别 | 平台线程 | jstack 标记虚拟线程(virtual) |
| 调优方向 | 池参数/队列 | 可引入虚拟线程执行器替代池化 |
参考文献、版权声明、致谢
本文的部分内容、思路、代码,参考或借鉴了诸多前辈的著作、博客、课程、视频等内容。以下对各章节内容的来源进行说明。
各章节参考来源
第 06 讲:一共有哪 3 类线程安全问题?WrongInit 的代码参考自《Java 并发编程实战》讲安全发布的小节。
第 10 讲:线程池的各个参数的含义?线程数增加的流程图参考自网上,但因出处较多,原始出处不可考,原作者若看到本文,请联系,将增加标注。
第 12 讲:有哪 6 种常见的线程池?什么是 Java8 的 ForkJoinPool?线程池结构图和 forkjoinpool 的思路来自 defog tech;JorkJoin 参考了 Doug Lea 的 http://gee.cs.oswego.edu/dl/papers/fj.pdf。
第 13 讲:线程池常用的阻塞队列有哪些?“线程池内部结构”这里的思路来自 https://www.cnblogs.com/joeman/p/3730397.html。
第 21 讲:如何看到 synchronized 背后的“monitor 锁”?“会发生以下这三种情况之一”这里参考了 https://blog.csdn.net/weixin_30702887/article/details/101112755 和 https://blog.csdn.net/b13001216978/article/details/109624782。
第 24 讲:讲一讲公平锁和非公平锁,为什么要“非公平”?这一小节的代码来自《Java 并发编程实战手册》2.3 小节。
第 25 节:读写锁 ReadWriteLock 获取锁有哪些规则?读写规则参考自 https://www.cnblogs.com/dolphin0520/p/3923167.html,本文部分思路参考自 defog tech。
第 26 讲:读锁应该插队吗?什么是读写锁的升降级?“为什么不支持锁的升级?”参考自 https://stackoverflow.com/questions/26110579/reentrantreadwritelock-java-nest-write-lock-inside-read-lock 和 https://stackoverflow.com/questions/464784/java-reentrantreadwritelocks-how-to-safely-acquire-write-lock。其他部分思路来自 defog tech。升降级代码案例参考自该类的 javadoc 描述。
第 27 讲:什么是自旋锁?自旋的好处和后果是什么呢?流程图参考自 https://tech.meituan.com/2018/11/15/java-lock.html;自旋锁实现的代码来自 https://www.fatalerrors.org/a/java-implementation-of-spin-lock.html。
第 28 讲:JVM 对锁进行了哪些优化?Person 和 MultiSyn 的代码来自 Java 官方文档,最后一个图来自美团技术博客的《不得不说的“锁”事》。
第 29 讲:HashMap 为什么是线程不安全的?实验:扩容期间取出的值不准确的代码例子来自 Artem Novikov http://stackoverflow.com/questions/18542037/how-to-prove-that-hashmap-in-java-is-not-thread-safe。
第 30 讲:ConcurrentHashMap 在 Java7 和 8 有何不同?数据结构的图片、源码解析思路参考自 https://javadoop.com/post/hashmap,感谢 hongjie。
第 33 讲:CopyOnWriteArrayList 有什么特点?CopyOnWrite 容器的特点参考自相关的 Java 并发书籍;迭代器代码来自 https://howtodoinjava.com/java/collections/java-copyonwritearrayset/。
第 34 讲:什么是阻塞队列?3 个图片思路参考自 defog tech。
第 35 讲:阻塞队列包含哪些常用的方法?add、offer、put 等方法的区别?倒数第 2、3 张图片参考自 defog tech。
第 36 讲:有哪几种常见的阻塞队列?图片参考自 defog tech。
第 37 讲:阻塞和非阻塞队列的并发安全原理是什么?前两段代码的注释和源码分析借鉴自 https://javadoop.com/post/java-concurrent-queue。
第 39 讲:原子类是如何利用 CAS 保证线程安全的?参考了 https://www.jianshu.com/p/cf93314488f9 和 https://www.jianshu.com/p/fb6e91b013cc。
第 40 讲:AtomicInteger 在高并发下性能不好,如何解决?为什么?图翻译自 defog tech。文中回答的问题出处:https://www.cnblogs.com/thisiswhy/p/13176237.html。
第 41 讲:原子类和 volatile 有什么异同?“案例说明 volatile 和原子类的异同”这部分参考自 defog tech。
第 43 讲:Java 8 中 Adder 和 Accumulator 有什么区别?部分思路参考了 defog tech。
第 44 讲:ThreadLocal 适合用在哪些实际生产的场景中?思路和图片参考了 defog tech。
第 46 讲:多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的?结构图片来自网上,由于该图在网络中广泛流传,原始出处不可考,原作者若看到本文,请联系,将增加标注。
第 47 讲:内存泄漏——为何每次用完 ThreadLocal 都要调用 remove()?引用链的图片和文字“我们重点看一下下面这条链路:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的 value 实例”借鉴自 https://juejin.cn/post/6844903683751149582。
第 49 讲:Future 的主要功能是什么?第一个图思路参考自 defog tech。
第 50 讲:使用 Future 有哪些注意点?Future 产生新的线程了吗?第一个图思路参考自 defog tech。
第 51 讲:如何利用 CompletableFuture 实现“旅游平台”问题?第 2、3、4 张图片思路参考自 defog tech。
第 52 讲:信号量能被 FixedThreadPool 替代吗?图片和图片相关的思路参考自 defog tech。
第 53 讲:CountDownLatch 是如何安排线程执行顺序的?图片参考自 Benjaminwhx。
第 57 讲:什么是指令重排序?为什么要重排序?重排序的 3 种情况参考自程晓明《深入理解 Java 内存模型》https://www.infoq.cn/article/java-memory-model-1/;重排序的指令的例子参考自 defog tech。
第 59 讲:什么是“内存可见性”问题?案例一思路来自 defog tech。
第 60 讲:主内存和工作内存的关系?“JMM 有以下规定”这三点来自前辈对 JMM 的翻译和理解;第一个 CPU 的图参考自 defog tech;第二个“主内存和工作内存”的图来自程晓明《深入理解 Java 内存模型》https://www.infoq.cn/article/java-memory-model-1/。
第 61 讲:什么是 happens-before 规则?加解锁的 happen-before 参考自《Java并发编程实战》,其他图片参考自 LogicBig.com。
第 62 讲:volatile 的作用是什么?与 synchronized 有什么异同?“volatile 和 synchronized 的关系”这一块参考了 https://zhuanlan.zhihu.com/p/55167585。
第 63 讲:单例模式的双重检查锁模式为什么必须加 volatile?参考了 小宝马的爸爸 - 梦想的家园《单例模式(Singleton)》:https://www.cnblogs.com/BoyXiao/archive/2010/05/07/1729376.html; Jark's Blog《如何正确地写出单例模式》:http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/; Hollis Chuang《为什么我墙裂建议大家使用枚举来实现单例》:https://www.hollischuang.com/archives/2498; Hollis Chuang《深度分析 Java 的枚举类型——枚举的线程安全性及序列化问题》:https://www.hollischuang.com/archives/197。
第 64 讲:你知道什么是 CAS 吗?“CAS 的思路”未找到原始出处;“CAS 的语义”参考自 http://java.boot.by/ocpjp7-upgrade/ch04s03.html。
第 67 讲:如何写一个必然死锁的例子?“数据库中”参考自《Java 并发编程实战》;必然死锁的代码是非常经典的案例,网上版本很多,参考自 https://www.cnblogs.com/baizhanshi/p/5437933.html。
第 69 讲:如何用命令行和代码定位死锁?发生死锁的代码是非常经典的代码。
第 70 讲:有哪些解决死锁问题的策略?转账和 hashcode 的例子思路来自《Java 并发编程实战》和死锁相关的小节;死锁的“三种主要的修复策略”借鉴自清华大学向勇的操作系统课程,中国大学 mooc。
第 71 讲:讲一讲经典的哲学家就餐问题。伪代码参考了 https://phoenix.goucher.edu/~kelliher/cs42/oct11.html。
第 72 讲:final 的三种用法是什么?“如果必须使用 final 方法或类,请说明原因”是翻译自外国人的某篇文章。
第 73 讲:为什么加了 final 却依然无法拥有“不变性”?两个 Test 类代码引用自 https://www.geeksforgeeks.org/final-arrays-in-java/。
第 74 讲:为什么 String 被设计为是不可变的?“字符串常量池”参考了王磊老师的《Java 源码剖析 34 讲》的 01 讲的部分内容;“缓存 HashCode”和“多线程安全”思路参考自 Deecyn:https://juejin.cn/post/6844904006909689864。
其他学习或参考过的内容
《Java 并发编程之美》翟陆续 / 薛宾田:https://book.douban.com/subject/30351286/ 《Java 并发编程实战》译者: 童云兰:https://book.douban.com/subject/10484692/ 《Java 核心技术 卷I》作者: [美] 凯 S.霍斯特曼(Cay S.Horstmann)译者: 林琪 / 苏钰涵:https://book.douban.com/subject/34898994/ 《深入理解 Java 内存模型》程晓明:https://www.infoq.cn/article/java-memory-model-1/系列 《Java 并发编程的艺术》作者: 方腾飞 / 魏鹏 / 程晓明:https://book.douban.com/subject/26591326/ 《Java 高并发编程详解-多线程与架构设计》作者: 汪文君,https://book.douban.com/subject/30255689/ 《Java 多线程编程实战指南》核心篇和设计模式篇:作者: 黄文海:https://book.douban.com/subject/26642317/和 https://book.douban.com/subject/27034721/ 《Java 高并发程序设计》葛一鸣、郭超:https://book.douban.com/subject/26663605/ 《Java 多线程编程核心技术》高洪岩:https://book.douban.com/subject/26555197/ 《Java 7 并发编程实战手册》作者: [西]Javier Fernández González:https://book.douban.com/subject/25844475/ 《Java 并发编程设计原则与模式》作者: (美)Doug Lea:https://book.douban.com/subject/1244021/ 《精通 Java 并发编程》作者: [西] 哈维尔·费尔南德斯·冈萨雷斯:https://book.douban.com/subject/30327401/ 《线程八大核心》:https://coding.imooc.com/class/362.html 《玩转 Java 并发工具》:https://coding.imooc.com/class/409.html 《Java 并发编程学习宝典》:https://www.imooc.com/read/49 《面试官系统精讲 Java 源码及大厂真题》:https://www.imooc.com/read/47 《Java 并发编程实战》:https://time.geekbang.org/column/intro/100023901 《打通 Java 任督二脉——并发数据结构的基石》:https://juejin.cn/post/6844903736578408461#heading-2 javadoop 并发系列文章:https://javadoop.com/
版权声明
本文的大部分内容为原创,但部分文字、思路与代码参考了上述资料。在校对过程中已尽量对所有来源(包括图片、思路、文字等)进行标注,但仍可能存在部分内容未能精确记录具体引用点的情况,同时也存在部分内容找不到原作者的情况,例如 ThreadLocal 引用链的图片,网络中使用该图片的博文众多,未能找到真正的创作者。
若认为本文部分内容与原创内容相似,或认为涉嫌侵犯著作权,或希望本文进一步细致标明出处,或存在其他诉求,请联系作者,将以诚恳的姿态及时沟通处理。