最近在重构公司的一个高并发系统,说实话,真是被各种并发问题折磨得不轻。今天趁着周末有空,把这段时间踩过的坑和一些心得整理下,希望对大家有所帮助。
从一次线上事故说起上个月我们线上出现了一次严重的性能问题,用户反馈系统响应特别慢,甚至有些请求直接超时了。排查后发现是并发控制没做好导致的。这让我深刻认识到,并发编程真不是简单地开几个线程那么简单。
并发控制到底在控制什么?很多刚接触并发的同学可能会问,为啥要控制?让程序跑得越快不是越好吗?其实不然。打个比方,这就像高速公路,车越多不代表通行效率越高,没有红绿灯和交通规则,反而会造成拥堵甚至事故。
在我们的系统中,主要用到了以下几种并发控制手段:
控制方式
适用场景
优点
缺点
实际使用频率
互斥锁(Mutex)
临界区保护
简单直观
可能造成死锁
★★★★★
读写锁(RWLock)
读多写少场景
读操作可并发
实现复杂
★★★☆☆
信号量(Semaphore)
限制资源访问数
灵活控制并发度
容易用错
★★☆☆☆
条件变量
线程间通信
避免忙等待
使用门槛高
★★☆☆☆
原子操作:最轻量级的同步机制说到并发控制,不得不提原子操作。刚开始写并发代码时,我特别喜欢用锁,感觉锁住了就安全了。后来发现很多场景下,一个简单的原子操作就能搞定,性能还更好。
比如统计请求数这种场景:
代码语言:java复制// 以前我会这么写
private int count = 0;
private Object lock = new Object();
public void increment() {
synchronized(lock) {
count++;
}
}
// 现在我会用原子类
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}性能测试结果让我大吃一惊:
并发线程数
加锁方式耗时(ms)
原子操作耗时(ms)
性能提升
10
125
45
2.8x
50
680
120
5.7x
100
1850
235
7.9x
500
9200
890
10.3x
死锁:并发编程的噩梦要说并发编程最怕什么,死锁绝对排得上号。我记得有一次,系统突然挂了,所有请求都卡住不动。查了半天日志,最后用jstack一看,好家伙,十几个线程互相等待,形成了一个完美的死锁环。
死锁是怎么产生的?理论上说,死锁需要四个条件同时满足。但实际编码中,最容易出问题的是加锁顺序不一致。举个我们实际遇到的例子:
线程A:先锁用户数据,再锁订单数据
线程B:先锁订单数据,再锁用户数据
这种情况下,如果时机不巧,就会死锁。
我们的死锁检测方案后来我们实现了一套死锁检测机制,主要思路是:
预防为主:统一加锁顺序,比如按照资源ID从小到大检测为辅:定期扫描线程状态,发现死锁立即处理快速恢复:检测到死锁后,选择牺牲代价最小的线程检测算法的核心是构建等待图:
检测指标
阈值
说明
处理方式
线程等待时间
30秒
可能死锁
记录日志
等待环路
存在
确定死锁
中断线程
CPU使用率
<5%
可能全部阻塞
人工介入
等待线程数
10
并发过高
限流降级
线程池:不是越大越好很多人觉得线程池设置得越大,处理能力就越强。我以前也这么想,直到有一次把线程池设置成1000,结果系统直接OOM了。
线程池大小的学问经过多次调优,我总结出了一套经验公式:
CPU密集型任务:线程数 = CPU核心数 + 1IO密集型任务:线程数 = CPU核心数 × 2混合型任务:需要根据实际情况测试我们系统不同模块的线程池配置:
模块名称
任务类型
核心线程数
最大线程数
队列大小
拒绝策略
API网关
IO密集
20
50
1000
CallerRuns
数据处理
CPU密集
8
10
100
Abort
日志收集
混合型
15
30
500
Discard
定时任务
IO密集
10
20
200
DiscardOldest
线程池的监控指标光配置好还不够,还得实时监控。我们主要关注这些指标:
活跃线程数:判断负载情况队列积压数:发现处理瓶颈拒绝任务数:评估容量是否足够平均等待时间:用户体验的直接体现一些实战经验1. 能不用锁就不用锁这是我的第一原则。很多时候通过合理的设计可以避免加锁,比如:
使用ThreadLocal避免共享采用无锁数据结构(ConcurrentHashMap等)函数式编程,减少可变状态2. 加锁要细粒度如果必须加锁,锁的范围要尽可能小。我见过有人在方法级别加synchronized,结果性能惨不忍睹。
3. 注意锁的公平性非公平锁性能更好,但可能造成线程饥饿。我们的原则是:
高频操作用非公平锁低频但重要的操作用公平锁4. 做好降级预案并发高的时候,要有降级方案。比如:
限流:超过阈值直接拒绝熔断:错误率高时暂停服务降级:返回缓存或默认值总结并发编程确实是个技术活,需要大量的实践才能掌握。这篇文章分享的都是我在实际工作中积累的经验,希望能帮大家少踩些坑。
记住,并发控制的目的不是限制性能,而是在保证正确性的前提下提高效率。过度设计和不够重视都是问题,找到平衡点才是关键。
最后想说,遇到并发问题不要慌,先冷静分析,很多时候问题没有想象的那么复杂。当然,如果真的搞不定,记得找身边的大佬帮忙,毕竟独行快,众行远嘛。
有什么问题欢迎留言讨论,大家一起进步!