{T}

线程池监控调优与排查案例

五、线程池监控和调优

5.1 线程池核心参数详解

java
public ThreadPoolExecutor(
    int corePoolSize,              // 核心线程数
    int maximumPoolSize,           // 最大线程数
    long keepAliveTime,            // 空闲线程存活时间
    TimeUnit unit,                 // 时间单位
    BlockingQueue<Runnable> workQueue,  // 工作队列
    ThreadFactory threadFactory,   // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)

参数详解

  1. corePoolSize

    • 核心线程数,线程池维护的最小线程数
    • 即使空闲也不会被回收(除非设置 allowCoreThreadTimeOut)
    • 建议:CPU 密集型 = CPU 核心数 + 1,IO 密集型 = 2 * CPU 核心数
  2. maximumPoolSize

    • 最大线程数,队列满时创建的最大线程数
    • 建议:根据业务流量峰值设置,不要过大
  3. keepAliveTime

    • 空闲线程存活时间,超过核心线程数的线程空闲时会被回收
    • 建议:根据任务提交频率设置
  4. workQueue

    • 工作队列,存储等待执行的任务
    • 建议:使用有界队列,避免 OOM
  5. threadFactory

    • 线程工厂,自定义线程名称、优先级、守护线程等
    • 建议:使用 Guava 的 ThreadFactoryBuilder
  6. handler

    • 拒绝策略,队列满且线程数达到最大时的处理策略
    • 建议:根据业务需求选择合适的策略

拒绝策略

策略说明适用场景
AbortPolicy抛出 RejectedExecutionException默认策略,需要处理异常
CallerRunsPolicy由调用线程执行任务不希望丢失任务
DiscardPolicy直接丢弃任务,不抛异常允许丢弃任务
DiscardOldestPolicy丢弃队列中最老的任务允许丢弃旧任务

5.2 线程池监控指标

java
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) + "%");
    }
}

自定义监控线程池

java
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;
    }
}

接入监控系统

java
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. 合理设置线程数

java
// 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. 选择合适的队列

java
// 有界队列: 防止 OOM,但可能触发拒绝策略
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);

// 无界队列: 不会触发拒绝策略,但可能导致 OOM
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

// 同步队列: 直接提交,不缓冲
SynchronousQueue<Runnable> queue = new SynchronousQueue<>();

// 优先级队列: 按优先级执行
PriorityBlockingQueue<Runnable> queue = new PriorityBlockingQueue<>();

3. 合理设置拒绝策略

java
// 自定义拒绝策略
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. 线程池隔离

java
// 不同业务使用不同的线程池
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. 动态调整线程池参数

java
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%+
  • 接口响应时间变长
  • 用户反馈页面加载慢

排查步骤

bash
# 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. 分析线程堆栈

线程堆栈分析

java
"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) 循环没有退出条件

问题代码

java
public int calculateScore(Long userId) {
    int score = 0;
    while (true) {  // 死循环
        score += getUserScore(userId);
        if (score > 1000) {
            break;
        }
    }
    return score;
}

修复方案

java
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:死锁问题排查

现象

  • 某个转账接口偶发卡死
  • 接口一直等待,没有响应
  • 重启后问题消失,一段时间后又出现

排查步骤

bash
# 1. 使用 jstack 检测死锁
jstack -l <pid> | grep -A 10 "Found one Java-level deadlock"

jstack 输出

code
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)

问题代码

java
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:按相同顺序加锁

java
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:使用全局锁

java
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

java
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:线程池堆积问题排查

现象

  • 接口响应时间逐渐变长
  • 线程池队列持续增长
  • 最终触发拒绝策略

排查步骤

bash
# 1. 使用 Arthas 查看线程池状态
thread -n 3

# 2. 查看线程堆栈
jstack <pid> | grep -A 20 "http-nio"

# 3. 使用 Arthas 监控方法执行时间
trace com.example.UserService getUser

问题定位

java
// 线程池配置
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

修复方案

java
// 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

排查步骤

bash
# 1. 使用 jmap 查看堆内存使用
jmap -heap <pid>

# 2. 导出堆 dump
jmap -dump:format=b,file=heap.hprof <pid>

# 3. 使用 VisualVM 或 MAT 分析堆 dump

VisualVM 分析

code
1. 打开 VisualVM,加载 heap.hprof
2. 点击"类"标签,按实例数排序
3. 发现 UserSession 实例数异常多
4. 右键点击 UserSession,选择"在堆中显示"
5. 查看引用链,找到 GC Root

问题代码

java
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 不会被清理
}

修复方案

java
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: 什么是线程安全?如何判断代码是否线程安全?

线程安全是指:当多个线程访问同一个对象时,如果不需要考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果。

判断标准:

  1. 是否有共享可变状态
  2. 是否有复合操作
  3. 是否有可见性问题
  4. 是否有有序性问题

Q2: synchronized 和 Lock 有什么区别?

特性synchronizedLock
实现方式JVM 关键字API 调用
锁获取/释放自动手动
可中断性不可中断可中断
公平性非公平可选
条件变量单一多个
性能优化后差距不大竞争激烈时略优

Q3: volatile 能保证线程安全吗?

volatile 只能保证可见性和有序性,不能保证原子性。适用场景:

  • 单一变量的读写
  • 状态标记位
  • 双重检查锁定

Q4: ThreadLocal 会导致内存泄漏吗?如何避免?

会。ThreadLocal 的 Entry 中 key 是弱引用,value 是强引用。如果线程长期存活(如线程池),value 不会被回收。

避免方法:

  1. 使用完调用 remove()
  2. 在 finally 块中清理
  3. 线程池中要特别注意

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 飙高问题?

bash
# 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 排查慢接口?

bash
# 1. 监控方法执行时间
trace com.example.UserService getUser

# 2. 查看方法调用参数和返回值
watch com.example.UserService getUser '{params, returnObj}' -x 2

# 3. 查看线程堆栈
thread -n 3

Q13: 如何检测死锁?

bash
# 方法 1: jstack
jstack -l <pid> | grep -A 10 "Found one Java-level deadlock"

# 方法 2: jconsole
# 点击"线程"标签,点击"检测死锁"

# 方法 3: Arthas
thread -b

7.5 高级问题

Q14: 什么是 ABA 问题?如何解决?

ABA 问题:变量值从 A 变为 B,又变回 A,CAS 操作无法检测到变化。

解决方法:使用 AtomicStampedReference,添加版本号

java
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: 如何实现一个线程安全的单例?

java
// 方法 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: 如何实现一个线程安全的计数器?

java
// 方法 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 线程安全核心要点

  1. 三大特性:原子性、可见性、有序性
  2. 实现方式:不可变对象、同步机制、并发容器、ThreadLocal
  3. 问题类型:竞态条件、死锁、活锁、饥饿、线程池堆积
  4. 排查工具:jstack、jconsole、VisualVM、Arthas

8.2 最佳实践

  1. 尽量减少共享可变状态
  2. 使用高层并发工具而非底层同步原语
  3. 用状态机、唯一键、原子操作代替脆弱的先查后改
  4. 锁范围只覆盖真正的临界区
  5. 对线程池、队列、拒绝策略做指标和告警
  6. 不要把"加锁"当成唯一并发治理手段

8.3 排查思路

code
并发问题排查思路:
    1. 看现象
       ├── 数据不一致 → 竞态条件、原子性
       ├── RT 飙升 → 死锁、阻塞、线程池堆积
       ├── CPU 高但吞吐下降 → 锁竞争、资源争抢
       └── 内存持续增长 → 内存泄漏
    
    2. 找原因
       ├── jstack → 线程堆栈分析
       ├── jconsole/VisualVM → 图形化监控
       ├── Arthas → 动态诊断
       └── jmap → 内存分析
    
    3. 定位代码
       ├── 分析线程堆栈
       ├── 追踪方法调用
       ├── 查看对象引用链
       └── 分析锁竞争
    
    4. 修复问题
       ├── 加锁/优化锁
       ├── 使用并发工具
       ├── 调整线程池参数
       └── 修复内存泄漏

8.4 常见代码味道

java
// × 危险代码
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/101112755https://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-lockhttps://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/cf93314488f9https://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 引用链的图片,网络中使用该图片的博文众多,未能找到真正的创作者。

若认为本文部分内容与原创内容相似,或认为涉嫌侵犯著作权,或希望本文进一步细致标明出处,或存在其他诉求,请联系作者,将以诚恳的姿态及时沟通处理。