多线程问题集锦
一、如何保证 T1→T2→T3 的执行顺序?
要确保 T2 在 T1 完成后执行,T3 在 T2 完成后执行,最直观的方案是使用Thread.join()方法。join()的核心作用是让当前线程阻塞,等待目标线程执行完毕后再继续运行,从而强制线程按顺序执行。
实现示例:
1 | public class ThreadOrder { |
执行结果:
1 | T1 执行中... |
其他实现方式:
除了join(),还可通过锁机制(如CountDownLatch)或线程池的提交顺序(execute()按顺序提交)实现,但join()是最简洁的方案。
二、Lock 接口相比 synchronized 的优势
java.util.concurrent.locks.Lock接口(如ReentrantLock)是 JDK 5 引入的同步工具,相比synchronized关键字,它提供了更灵活的同步控制能力,核心优势如下:
支持非阻塞式获取锁
tryLock()方法可尝试获取锁,若获取失败则立即返回false,避免线程无限期阻塞。还可设置超时时间,进一步提升灵活性:
1 | Lock lock = new ReentrantLock(); |
可响应中断的锁获取
lockInterruptibly()允许线程在等待锁的过程中响应中断,避免线程因无法获取锁而永久阻塞:
1 | try { |
读写分离锁,提升并发性能
ReentrantReadWriteLock将锁分为读锁(共享) 和写锁(排他):
- 多个线程可同时获取读锁(适合读多写少场景);
- 写锁与读锁、写锁与写锁互斥。
这种设计比synchronized的全排他锁效率更高,是ConcurrentHashMap等高性能并发容器的核心实现基础。
支持多个条件变量(Condition)
synchronized仅通过wait()/notify()实现线程通信,且所有等待线程共享一个等待队列;而Lock可通过Condition创建多个等待队列,实现更精细的线程唤醒控制:
1 | Lock lock = new ReentrantLock(); |
显式的锁释放机制
synchronized的锁释放是隐式的(代码块执行完毕或异常退出时自动释放),而Lock需通过unlock()显式释放,且必须在finally中调用,避免死锁。这种显式控制让开发者能更灵活地管理锁的生命周期。
三、线程泄漏(Thread Leak)
现象:
- 应用线程数持续增长,最终达到上限(如线程池最大线程数),新任务无法执行;
- 内存占用飙升,频繁 Full GC,甚至 OOM;
- 线程栈中存在大量处于
RUNNABLE或TIMED_WAITING状态的 “僵尸线程”。
常见原因:
- 线程创建后未正确终止(如
while(true)循环中未设置退出条件,或阻塞在wait()/park()后未被唤醒); - 线程池参数配置不合理(如
corePoolSize过大,或allowCoreThreadTimeOut未开启,核心线程长期空闲不销毁); - 异步任务框架(如
CompletableFuture、消息队列消费者)未设置超时,任务长期阻塞导致线程被占用。
排查与解决:
- 用
jstack <pid>dump 线程栈,统计线程名称 / 状态,定位重复创建的线程(如名称含 “Thread-xxx” 的匿名线程); - 检查线程池配置:避免
corePoolSize过大,设置maximumPoolSize上限,配合合理的队列容量和拒绝策略; - 为阻塞操作设置超时(如
Lock.tryLock(timeout)、Future.get(timeout)),避免线程无限期等待; - 用
ThreadFactory自定义线程名称(如包含业务标识),便于定位泄漏来源。
四、锁竞争激烈导致性能暴跌
现象:
- 应用响应时间骤增,CPU 利用率虽高但业务吞吐量低;
- 线程栈中大量线程处于
BLOCKED状态(等待synchronized锁)或WAITING状态(等待Lock锁); - 监控面板显示 “锁等待时间” 指标异常升高。
常见原因:
- 锁粒度太大(如对整个对象加锁,而非具体字段,导致所有线程竞争同一把锁);
- 热点资源竞争(如全局计数器、缓存刷新逻辑未做分片,所有线程争抢同一资源);
- 不当使用
synchronized修饰频繁调用的方法(如工具类的静态方法,导致全量线程竞争)。
排查与解决:
- 用
jstack分析锁持有者:BLOCKED线程的栈信息会显示 “waiting to lock < 对象地址>”,找到持有该锁的线程(通常处于RUNNABLE状态),分析其执行逻辑; - 减小锁粒度:将 “对象锁” 改为 “字段锁”(如用
ConcurrentHashMap的segment锁机制,或ReentrantLock的分段锁); - 热点资源分片:如用
LongAdder替代AtomicLong(将计数器拆分为多个 Cell),或按用户 ID 哈希分片处理; - 非阻塞替代:用 CAS 原子类(
Atomic*)或并发容器(ConcurrentHashMap)替代锁,减少竞争。
五、线程安全问题导致的数据不一致
现象:
- 数据存储 / 计算结果异常(如订单金额错误、库存超卖、统计数据少算 / 多算);
- 偶现
NullPointerException(非线程安全对象被多线程修改,如ArrayList在迭代时被修改); - 日志中出现 “幻象读”(如刚写入的数据立即读取不到)。
常见原因:
- 非线程安全对象被多线程共享(如
HashMap、SimpleDateFormat、ArrayList等,未加同步直接并发操作); - 原子操作被拆分(如
i++、map.putIfAbsent()未用原子类或锁保护); - 错误使用
volatile(误认为volatile能保证原子性,如volatile int count被多线程count++); - 分布式场景下本地缓存未做同步(如多实例修改本地缓存,导致数据不一致)。
排查与解决:
- 复现问题:通过压测工具(如 JMeter)模拟高并发,结合日志定位异常数据的产生时机;
- 代码审计:检查共享变量的操作逻辑,确保非线程安全对象(如
SimpleDateFormat)用ThreadLocal隔离,或替换为线程安全实现(如DateTimeFormatter); - 同步加固:对复合操作(如 “判断 + 修改”)加锁或用原子类(如
AtomicReference的compareAndSet); - 分布式同步:用分布式锁(如 Redis、ZooKeeper)或消息队列保证跨实例的操作原子性。
六、线程死锁(Deadlock)
现象:
- 部分业务突然卡住,无法推进(如订单支付、库存扣减流程无响应);
jstack显示 “Found one Java-level deadlock”,涉及多个线程相互持有对方需要的锁;- 死锁线程持有资源(如数据库连接),导致其他线程无法获取资源,引发连锁阻塞。
常见原因:
- 多线程获取锁的顺序不一致(如线程 A 先锁资源 1 再锁资源 2,线程 B 先锁资源 2 再锁资源 1);
- 锁嵌套过深(如方法 A 调用方法 B,两者都加锁,形成隐性的多锁竞争);
- 资源释放不及时(如
try-finally中遗漏unlock(),导致锁长期被持有)。
排查与解决:
- 用
jstack <pid>直接检测死锁,日志会明确标出相互等待的线程和锁对象; - 统一锁顺序:对所有资源按固定规则排序(如按对象哈希值、资源 ID),强制线程按顺序获取锁;
- 减少锁嵌套:拆分大方法,避免一个方法同时持有多个锁;
- 用带超时的锁(如
tryLock(1, TimeUnit.SECONDS)),超时后释放已获锁并重试,避免永久死锁。
七、线程上下文切换过载
现象:
- CPU 利用率接近 100%,但业务逻辑执行效率低(如每秒处理请求量远低于预期);
- 系统平均负载(load average)远高于 CPU 核心数;
- 监控工具(如
vmstat)显示cs(上下文切换次数)每秒几万甚至几十万。
常见原因:
- 线程数过多(如线程池
maximumPoolSize远大于 CPU 核心数,导致线程频繁切换); - 大量线程处于
RUNNABLE状态(如任务执行时间短,线程频繁争抢 CPU); - 频繁的 IO 操作(如数据库查询、网络请求未做批量处理,线程频繁阻塞 / 唤醒)。
排查与解决:
- 用
vmstat 1监控上下文切换次数(cs列),正常情况下应低于每秒 1 万; - 调整线程池参数:线程数建议设置为
CPU核心数 * 2(CPU 密集型)或CPU核心数 + IO等待数(IO 密集型); - 批量处理任务:如将多次数据库查询合并为批量查询,减少 IO 次数;
- 使用协程(如 Project Loom 的虚拟线程):虚拟线程切换成本极低,适合高并发 IO 场景。
八、ThreadLocal 内存泄漏
现象:
- 内存泄漏,老年代占用持续增长,Full GC 无法回收;
- 堆转储(heap dump)中存在大量
ThreadLocal$ThreadLocalMap和业务对象(如用户会话、数据库连接)。
常见原因:
- 线程池复用线程:
ThreadLocal的ThreadLocalMap随线程生命周期存在,若线程池核心线程不销毁,ThreadLocal值会长期驻留内存; - 未手动清理:
ThreadLocal使用后未调用remove(),导致Entry的key(弱引用)被回收,但value(强引用)仍被持有。
排查与解决:
- 分析 heap dump:用 MAT 工具查看
Thread对象的threadLocals字段,定位未清理的ThreadLocal值; - 强制清理:在
try-finally中调用threadLocal.remove(),确保线程复用后ThreadLocal值被清除; - 避免存储大对象:
ThreadLocal中只放轻量数据(如用户 ID),而非大对象(如完整会话信息)。
九、并发工具使用不当导致的异常
现象:
- 偶现
IllegalMonitorStateException(调用wait()/notify()时未持有锁); ConcurrentModificationException(迭代ArrayList时被其他线程修改);- 线程池任务执行结果丢失(如
submit()的Future未调用get(),异常被吞噬)。
常见原因:
- 同步方法与非同步方法混用(如
HashMap的put加锁,但get未加锁,导致脏读); - 忽略
Future的返回值:submit()提交的任务若抛出异常,需通过get()获取,否则异常会被FutureTask吞噬; - 并发容器使用误区(如
CopyOnWriteArrayList的iterator是快照,迭代时看不到新元素,导致业务逻辑错误)。
排查与解决:
- 日志埋点:在任务执行前后打印日志,记录异常信息(尤其是
CompletableFuture的exceptionally回调); - 替换为线程安全容器:用
ConcurrentHashMap替代HashMap,CopyOnWriteArrayList替代ArrayList(读多写少场景); - 严格遵循同步规范:调用
wait()/notify()前必须持有对象锁,迭代并发容器时避免修改操作。
v1.3.10