本文目录导读:
线程等待(如 Thread.sleep()、wait()、join()、或阻塞 I/O)导致 CPU 空闲,是性能优化的直接目标,优化线程等待时长的核心思路是 “能不等的尽量不等,必须等的缩短等的时间,不能缩短的让 CPU 干别的事”。
以下是具体的优化策略,从代码层面到设计层面,按优先级排列:
根本性优化:消除等待
-
用“异步”替代“同步等待”
-
现象:主线程调用
future.get()或join()等待子线程完成。 -
优化:改为主线程注册回调(
CompletableFuture.thenApply),或者直接提交给事件循环(Netty、Vert.x),主线程不需要阻塞,去做其他事了。 -
示例:
// 优化前:阻塞等待 Future<String> future = executor.submit(task); String result = future.get(); // 主线程卡在这里 // 优化后:异步回调 CompletableFuture.supplyAsync(task).thenAccept(result -> { // 无需等待,任务完成后自动处理 });
-
-
用“轮询”替代“死等”
- 现象:
while (!condition) { Thread.sleep(10); }。 - 优化:使用
wait()/notify()或Condition.await()/signal(),当条件满足时立即唤醒,无需睡眠轮询。 - 原理:轮询是“我猜你好了”;等待/通知是“你好了直接叫我”,效率天差地别。
- 现象:
-
消除不必要的锁竞争
- 现象:多个线程频繁获取同一把锁,导致大量线程挂起(阻塞)。
- 优化:
- 使用无锁数据结构(
AtomicLong、ConcurrentHashMap)。 - 减小锁粒度(分桶锁/分段锁)。
- 使用读写锁(
ReadWriteLock),读多写少时效果显著。 - 使用 StampedLock(乐观读锁)。
- 使用无锁数据结构(
缩短等待时间
-
批量提交任务,减少单次等待开销
- 现象:循环中逐条提交数据库更新,每次提交都等待一次网络 IO。
- 优化:积累一批(比如100条)后,批量提交,单次等待的时长不变,但总等待次数减少了99%。
- 适用:数据库批量写入、MQ批量发送、日志批量刷盘。
-
超时控制
- 现象:
future.get()、lock.lock()或 Http 请求没有超时设置,网络抖一下就卡死。 - 优化:设置合理的超时时间。
// 不要无限等待 future.get(100, TimeUnit.MILLISECONDS); // 或者设置 Lock 的 tryLock if (lock.tryLock(50, TimeUnit.MILLISECONDS)) { ... } - 作用:即使必须等待,也仅限于“合理的时长”,避免毛刺扩大。
- 现象:
-
预加载 / 预热
- 现象:请求来了才去加载配置、建立连接池(首次加载很慢)。
- 优化:系统启动时(或第一次请求前)异步预加载到缓存,后续请求直接命中,零等待。
并发分流:让等待不阻塞整体
-
引入线程池隔离 / 异步处理队列
- 现象:一个慢操作(写日志/发邮件)阻塞了用户的请求线程。
- 优化:将非关键路径(日志、统计、通知)放入另一个独立线程池异步处理,用户线程只处理核心业务,无需等待。
- 适用:缓存穿透场景:一个请求在等待数据库加载,其他请求可以继续处理。
-
并行化:分而治之
- 现象:顺序查询三个 API 获取结果,总耗时 = T1 + T2 + T3。
- 优化:并行请求。
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> fetchUser()); CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> fetchOrder()); String r = f1.thenCombine(f2, (u, o) -> u + o).get();
- 效果:总耗时 = max(T1, T2, T3) + 极短的调度开销。
系统/框架层面调优
-
IO模型的根本性改变
- 现象:大量线程因为网络 IO 阻塞(Java BIO 模型)。
- 优化:使用 NIO 多路复用(Selector)或 协程(Java 21 虚拟线程 / Kotlin / Go),一个线程可以管理成千上万个连接,无需为每个连接创建一个等待线程。
- Java 21+:直接使用虚拟线程(
Thread.ofVirtual()),虚拟线程挂起开销极低(纳秒级),相当于“无限线程池”,不必再担忧线程创建和切换开销。
-
使用内存队列 + 批处理
- 现象:每次查询都走网络。
- 优化:对于对一致性要求不高的数据(如计数器、埋点、实时指标),先写入内存队列,后台线程批量刷盘/发送,请求线程几乎是瞬间完成(毫秒级),等待的是后台刷盘的线程,而不是用户线程。
诊断工具:找到等待原因
在优化之前,需要先确认到底是“哪种等待”:
- 线程转储(jstack):看到线程状态是
WAITING (parking)或BLOCKED,说明是锁竞争。 - 火焰图(async-profiler):看到
Thread.sleep占比很高,说明是轮询或主动睡眠。 - Arthas(
thread -b):可以直接找出阻塞其他线程的线程。
优化步骤
- 定位:用工具找出具体哪个线程在等、等什么(锁?IO?sleep?)。
- 识别赛道:
- 是锁竞争 → 上无锁/减粒度/读写锁。
- 是等待数据 → 上异步回调/预加载/缓存。
- 是等待系统 → 改 IO 模型/批量/分流。
- 动手:从消除等待(优先级最高)开始,不行再考虑缩短和分流。
- 压测验证:优化后,用压测确认等待时长和吞吐量是否有明显改善。
最极致的优化:让等待线程变为0,或者让等待的线程去跑其他任务(协程/虚拟线程/回调)。
标签: 上下文切换