-
事故背景
晚上六点左右CPU告警,从日志量判断是在这段时间进行了大量的业务处理,追踪到是有人触发了订单推送的方法,时间跨度22天,订单量40w左右。 -
CPU高使用率的本质
表面现象:当线程池的线程数接近或超过CPU核心数时,操作系统的线程调度器需要频繁在不同线程之间切换。此时CPU的利用率较高,但不代表有效计算时间的占比高。
实际原因:主要是调度开销。两个线程在同一个CPU核心上交替执行时,每次切换都需要保存当前线程的寄存器状态、内存信息等。这种切换需要耗费CPU时间。 -
配置线程数理论
CPU密集型任务:建议线程数=CPU核心数+1,避免过多线程争夺计算资源。
I/O密集型任务:公式为 核心数 * (1 + I/O等待时间/计算时间)。例如,若I/O耗时占比50%,则线程数可设为 2×核心数。 -
定位原因
stationIds.parallelStream().forEach(stationId -> {
try {
ContextHolder.set(data); // threadlocal存储上下文配置
doSomething();
} catch (Exception e) {
log.error("error", e);
} finally {
ContextHolder.remove(); // 清理上下文配置
}
});
原因分析:
a. 当业务高峰期遇上并发流,形成多个线程在CPU交替执行的局面,上下文切换导致CPU飙高。
b. 多核 CPU 的缓存一致性协议(如 MESI)会因内存屏障触发缓存行(Cache Line)的同步。若多个线程频繁修改各自的 ThreadLocalMap(例如在同一个线程中反复 set/remove),可能导致缓存行频繁失效,增加 CPU 核心间的数据同步开销。
虽然 ThreadLocal 变量本身是线程隔离的,但底层 ThreadLocalMap 的读写操作仍需要维护线程内部的可见性。
parallelStream并发流的注意事项:
parallelStream()底层采用 ForkJoinPool 线程池,默认线程数与 CPU 核心数相同。
适合数学运算、复杂过滤等CPU密集型任务,而非I/O阻塞操作。
数据量小于1万时,线程调度开销可能抵消并行收益。
I/O、网络请求会阻塞ForkJoinPool线程,影响其他并行任务,需改用独立线程池。