醋醋百科网

Good Luck To You!

Java必看!多线程踩坑后才懂的3个优化技巧,提升并发性能

上线后就炸了:响应变慢、接口超时飙升,偶尔还来个死锁。日志里一堆线程在等待,用户在抱怨,系统在打酱油。把问题倒回去看,脉络很清楚——并发能力没抓住“资源竞争”和“线程调度效率”这个平衡点,导致看似加了锁、配了线程池,反而越调越乱。

说白了,这类故障常常不是单一原因,而是几样东西凑在一起作怪。先把大方向讲清楚:很多团队把问题归结为“线程池太小”或“锁太多”,一通瞎调之后发现越改越糟。真正的痛点通常是下游资源(像数据库、外部服务、连接池)和线程池配置不匹配,线程被挂在那里等资源,CPU 不忙但响应慢,队列越堆越长,超时、拒绝、死锁接踵而来。

举个我见过的真实例子:促销那天访问量暴涨,代码里用 FixedThreadPool(20) 来并发处理请求。平时没事儿,但流量上去以后,20 个工作线程全都卡在“查数据库”这步不动。进一步看日志和监控,发现数据库连接池只有 10 个连接,线程们都在等连接释放。乍一看像是线程池容量不够,结果是线程在等别的资源。要是盲目把线程池扩成 200,只会让更多线程去排队等连接,CPU 上下文切换更多,延迟更高。最后的可行办法是把 IO 密集型线程池的最大数从 20 临时调到 30,同时把数据库连接池同步放大,避免线程只是白等连接。还得加上动态调整规则和监控告警,别随意无限制放大,免得引出新的瓶颈。

细节上很多容易被忽略的小坑,堆在一起就变灾难。有人把 ArrayList 当共享容器,外面套了个锁就完事;却忘了 ArrayList 扩容时会做分段拷贝,高并发下可能引起并发修改的问题。还有人用 ThreadLocal 存用户上下文,任务结束忘记清理,线程池复用线程时下一任务可能读到上一个任务的数据,问题很难重现也难排查。再说锁的使用,很多地方把整个方法标成 synchronized,把写日志、格式化字符串这些慢操作也都锁起来,结果一个慢 IO 把后面的线程都堵着。有人以为换成 ReentrantLock 就没事,事实是锁的粒度和持有时间决定了争用严重程度,不是换个锁就能万事大吉。

要落地解决,这里给出三类能马上用的办法,讲清楚怎么做,怎么看数据,怎么避免做死。

一招:按任务类型来配线程池。任务是 CPU 密集型还是 IO 密集型完全不一样。一个简单的经验法是根据机器 CPU 核数和平均等待时间/执行时间比来估算并发度。不要把 FixedThreadPool(10) 这类硬编码当成万能解,实际要把线程池接入监控。比如在 Spring 的 ThreadPoolTaskExecutor 加个 TaskDecorator,用来记录任务开始和结束时间;再把活跃线程数、队列长度、拒绝数上报到 Micrometer 或 Prometheus。监控上如果队列一直涨,就说明下游资源压力或线程数不够;如果活跃线程总是满载,则要么扩容,要么优化下游。促销期处理手法可以是:短期把 IO 线程池最大数从 20 提到 30,同时把数据库连接池也同步放大,避免线程纯粹在等连接。并且设置明确的触发阈值和退回策略,不要盲目永久加大。

二招:把锁的粒度和持有时间降下来。用个具体案例说明:有个用户信息更新流程,原来把整个方法都上锁,里面既更新数据库又写日志。写日志是同步 IO,占了 50ms,整方法被锁 60ms,别的线程在外面排队。改法是只把真正更新数据库的那段上锁,把日志写放到异步队列或独立线程去处理,锁持有时间从 60ms 直接降到 10ms,争用明显下降。更进一步可以做分段锁,比如按用户 ID 哈希分段,不同段用不同锁,这样不同用户的并发更新互不干扰。重点不是换什么锁,而是让锁只保护必须的临界区,把网络、IO、格式化这些慢操作都移出锁外。

三招:ThreadLocal 一定要用对且用完就清。很多服务把 token、traceId、用户上下文放在 ThreadLocal,调用链访问方便。但在线程池里这玩意儿如果在 finally 里不 remove,会把上一次任务的上下文带给下一次任务。固定流程是:请求入口 set 到 ThreadLocal,处理完在 finally 里 remove。别偷懒。更稳妥的做法是在 ThreadPool 的 TaskDecorator 里做统一清理,确保任何异常路径都能走到清理逻辑。实务中看到的故障里,ThreadLocal 泄露导致的数据串行读写很难复现,却很容易在高峰时刻出问题。

还有一堆常见但容易被忽略的点:并发场景选集合别随便拿 ArrayList,应该用并发集合(ConcurrentLinkedQueue、ConcurrentHashMap),或者考虑扩容带来的竞态。CopyOnWriteArrayList 适合读多写少,不适合写多场景。无锁设计(像 CAS)能提高并发,但实现复杂,维护成本高,要权衡。把业务数据放线程私有变量图方便,但在序列化、线程切换、异步回调时要特别小心。日志写入不要放进锁里——把慢操作放进临界区等于把其他线程当肉签。

监控上别只盯线程池大小。要同时看活跃线程数、队列长度、任务平均等待时间、拒绝策略触发次数以及下游资源指标(比如数据库连接使用率、外部服务延迟)。一个靠谱的排查流程可以是:先看到超时,检查线程池队列是不是堆积;如果是,抓几份栈快照看线程都在干嘛;再看下游资源使用情况,是不是数据库连接耗尽、还是外部接口慢。这一步步倒推,比较省力,不会盲目加线程又扯出更多问题。

最后补充几条实践建议,都是现场能用的:别把线程池参数写死在代码里,放到配置中心或配置文件,支持热改和回滚;给关键阈值(队列长度、平均等待时长、下游连接使用率)设告警;对长时间处于 RUNNING 的线程做快照和分析,看看它们卡在哪一层;对那些有强一致性需求的操作,考虑分段锁或悲观锁结合异步补偿;测试环境要跑接近真实流量的压测,别只在低负载下验证逻辑。

说到底,处理并发问题是一门细活,得把线程调度和下游资源看作一套系统来调,而不是单点地增减线程或换个锁。把监控、排查和改进结合起来,问题往往能被一步步拆开、定位、解决。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言