并发编程与死锁怎么排查、怎么避免
并发和死锁经常绑在一起问。我一般会先讲「并发要解决什么问题」,再讲死锁四个条件、项目里怎么预防和线上怎么定位。
题目
- 并发编程是什么?常见工具有哪些?
- 怎么解决死锁问题?
参考答案(口述要点)
并发:多线程同时访问共享资源,要保证正确性(原子性、可见性、有序性)和吞吐。
工具:synchronized、volatile、Lock、并发集合、线程池、CAS、异步框架。
死锁:四个条件同时满足;预防:固定加锁顺序、超时tryLock、减小锁粒度、避免嵌套锁;排查:jstack/ThreadMXBean.findDeadlockedThreads。
详细解析
并发编程在解决什么
- 提高 CPU 利用率(IO 等待时别的线程干活)
- 但要注意:竞态(两个线程同时改同一变量)、可见性(一个线程改了另一个看不见)、指令重排
常用手段
| 手段 | 典型用途 |
|---|---|
synchronized / Lock | 互斥改共享数据 |
volatile | 状态标志、单例双重检查中的一般变量仍要配合锁 |
ConcurrentHashMap 等 | 高并发 Map/Queue |
| 线程池 | 限制线程数量,复用线程,别 new Thread 满天飞 |
CountDownLatch / Semaphore | 等多任务完成、限流 |
死锁:条件与示例
四个条件:互斥、占有且等待、不可抢占、循环等待。去掉任一即可打破。
// 反例:两个锁交叉获取 → 可能死锁
final Object lockA = new Object();
final Object lockB = new Object();
new Thread(() -> {
synchronized (lockA) {
sleep(100);
synchronized (lockB) { System.out.println("T1"); }
}
}).start();
new Thread(() -> {
synchronized (lockB) {
sleep(100);
synchronized (lockA) { System.out.println("T2"); }
}
}).start();解决思路
- 统一加锁顺序:所有线程都先 A 后 B。
tryLock超时回退(ReentrantLock):拿不到就释放已持有的锁,重试或失败。
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
while (true) {
if (lockA.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
if (lockB.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// 业务
return;
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
Thread.sleep(10); // 退避
}- 降低锁粒度:能无锁用
ConcurrentHashMap.compute就别大锁。 - 数据库死锁:InnoDB 会自动检测回滚一方;应用层缩短事务、按相同顺序更新多行。
代码与实践
线程池(项目标配)
@Bean
public ThreadPoolTaskExecutor bizExecutor() {
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(8);
ex.setMaxPoolSize(16);
ex.setQueueCapacity(200);
ex.setThreadNamePrefix("biz-");
ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
ex.initialize();
return ex;
}队列打满要有策略(降级、拒绝、CallerRuns),否则任务堆积像「假死」。
线上排查死锁
jstack 进程号 | grep -A 20 "Found one Java-level deadlock"或 Arthas:thread -b 看阻塞、-l 看锁。
写在最后
小公司项目里死锁不少是 事务里更新多张表顺序不一致 或 本地锁 + 远程调用 搞出来的。面试说完 JVM 死锁,补一句 DB 和分布式锁,会显得完整。
创作不易,转载请注明出处和作者。
