0%

多线程问题集锦

多线程问题集锦

一、如何保证 T1→T2→T3 的执行顺序?

要确保 T2 在 T1 完成后执行,T3 在 T2 完成后执行,最直观的方案是使用Thread.join()方法。join()的核心作用是让当前线程阻塞,等待目标线程执行完毕后再继续运行,从而强制线程按顺序执行。

实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ThreadOrder {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("T1 执行中...");
try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("T1 执行完毕");
}, "T1");

Thread t2 = new Thread(() -> {
try {
t1.join(); // 等待T1执行完毕
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("T2 执行中...");
try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("T2 执行完毕");
}, "T2");

Thread t3 = new Thread(() -> {
try {
t2.join(); // 等待T2执行完毕
} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("T3 执行中...");
try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("T3 执行完毕");
}, "T3");

// 启动顺序不影响最终执行顺序
t3.start();
t2.start();
t1.start();
}
}

执行结果:

1
2
3
4
5
6
T1 执行中...
T1 执行完毕
T2 执行中...
T2 执行完毕
T3 执行中...
T3 执行完毕

其他实现方式:

除了join(),还可通过锁机制(如CountDownLatch)或线程池的提交顺序execute()按顺序提交)实现,但join()是最简洁的方案。

二、Lock 接口相比 synchronized 的优势

java.util.concurrent.locks.Lock接口(如ReentrantLock)是 JDK 5 引入的同步工具,相比synchronized关键字,它提供了更灵活的同步控制能力,核心优势如下:

支持非阻塞式获取锁

tryLock()方法可尝试获取锁,若获取失败则立即返回false,避免线程无限期阻塞。还可设置超时时间,进一步提升灵活性:

1
2
3
4
5
6
7
8
9
10
Lock lock = new ReentrantLock();
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试1秒内获取锁
try {
// 获得锁后执行逻辑
} finally {
lock.unlock(); // 显式释放锁
}
} else {
// 获取锁失败,执行备选逻辑
}

可响应中断的锁获取

lockInterruptibly()允许线程在等待锁的过程中响应中断,避免线程因无法获取锁而永久阻塞:

1
2
3
4
5
6
try {
lock.lockInterruptibly(); // 可被中断的锁获取
} catch (InterruptedException e) {
// 处理中断(如释放资源、记录日志)
Thread.currentThread().interrupt(); // 恢复中断状态
}

读写分离锁,提升并发性能

ReentrantReadWriteLock将锁分为读锁(共享)写锁(排他)

  • 多个线程可同时获取读锁(适合读多写少场景);
  • 写锁与读锁、写锁与写锁互斥。
    这种设计比synchronized的全排他锁效率更高,是ConcurrentHashMap等高性能并发容器的核心实现基础。

支持多个条件变量(Condition)

synchronized仅通过wait()/notify()实现线程通信,且所有等待线程共享一个等待队列;而Lock可通过Condition创建多个等待队列,实现更精细的线程唤醒控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // 非空条件队列
Condition notFull = lock.newCondition(); // 非满条件队列

// 生产者线程:添加元素后唤醒"非空"等待的消费者
lock.lock();
try {
queue.add(item);
notEmpty.signal(); // 仅唤醒等待"非空"的线程
} finally {
lock.unlock();
}

// 消费者线程:等待队列"非空"
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 进入"非空"条件队列等待
}
} finally {
lock.unlock();
}

显式的锁释放机制

synchronized的锁释放是隐式的(代码块执行完毕或异常退出时自动释放),而Lock需通过unlock()显式释放,且必须在finally中调用,避免死锁。这种显式控制让开发者能更灵活地管理锁的生命周期。

三、线程泄漏(Thread Leak)

现象

  • 应用线程数持续增长,最终达到上限(如线程池最大线程数),新任务无法执行;
  • 内存占用飙升,频繁 Full GC,甚至 OOM;
  • 线程栈中存在大量处于RUNNABLETIMED_WAITING状态的 “僵尸线程”。

常见原因

  1. 线程创建后未正确终止(如while(true)循环中未设置退出条件,或阻塞在wait()/park()后未被唤醒);
  2. 线程池参数配置不合理(如corePoolSize过大,或allowCoreThreadTimeOut未开启,核心线程长期空闲不销毁);
  3. 异步任务框架(如CompletableFuture、消息队列消费者)未设置超时,任务长期阻塞导致线程被占用。

排查与解决

  • jstack <pid> dump 线程栈,统计线程名称 / 状态,定位重复创建的线程(如名称含 “Thread-xxx” 的匿名线程);
  • 检查线程池配置:避免corePoolSize过大,设置maximumPoolSize上限,配合合理的队列容量和拒绝策略;
  • 为阻塞操作设置超时(如Lock.tryLock(timeout)Future.get(timeout)),避免线程无限期等待;
  • ThreadFactory自定义线程名称(如包含业务标识),便于定位泄漏来源。

四、锁竞争激烈导致性能暴跌

现象

  • 应用响应时间骤增,CPU 利用率虽高但业务吞吐量低;
  • 线程栈中大量线程处于BLOCKED状态(等待synchronized锁)或WAITING状态(等待Lock锁);
  • 监控面板显示 “锁等待时间” 指标异常升高。

常见原因

  1. 锁粒度太大(如对整个对象加锁,而非具体字段,导致所有线程竞争同一把锁);
  2. 热点资源竞争(如全局计数器、缓存刷新逻辑未做分片,所有线程争抢同一资源);
  3. 不当使用synchronized修饰频繁调用的方法(如工具类的静态方法,导致全量线程竞争)。

排查与解决

  • jstack分析锁持有者:BLOCKED线程的栈信息会显示 “waiting to lock < 对象地址>”,找到持有该锁的线程(通常处于RUNNABLE状态),分析其执行逻辑;
  • 减小锁粒度:将 “对象锁” 改为 “字段锁”(如用ConcurrentHashMapsegment锁机制,或ReentrantLock的分段锁);
  • 热点资源分片:如用LongAdder替代AtomicLong(将计数器拆分为多个 Cell),或按用户 ID 哈希分片处理;
  • 非阻塞替代:用 CAS 原子类(Atomic*)或并发容器(ConcurrentHashMap)替代锁,减少竞争。

五、线程安全问题导致的数据不一致

现象

  • 数据存储 / 计算结果异常(如订单金额错误、库存超卖、统计数据少算 / 多算);
  • 偶现NullPointerException(非线程安全对象被多线程修改,如ArrayList在迭代时被修改);
  • 日志中出现 “幻象读”(如刚写入的数据立即读取不到)。

常见原因

  1. 非线程安全对象被多线程共享(如HashMapSimpleDateFormatArrayList等,未加同步直接并发操作);
  2. 原子操作被拆分(如i++map.putIfAbsent()未用原子类或锁保护);
  3. 错误使用volatile(误认为volatile能保证原子性,如volatile int count被多线程count++);
  4. 分布式场景下本地缓存未做同步(如多实例修改本地缓存,导致数据不一致)。

排查与解决

  • 复现问题:通过压测工具(如 JMeter)模拟高并发,结合日志定位异常数据的产生时机;
  • 代码审计:检查共享变量的操作逻辑,确保非线程安全对象(如SimpleDateFormat)用ThreadLocal隔离,或替换为线程安全实现(如DateTimeFormatter);
  • 同步加固:对复合操作(如 “判断 + 修改”)加锁或用原子类(如AtomicReferencecompareAndSet);
  • 分布式同步:用分布式锁(如 Redis、ZooKeeper)或消息队列保证跨实例的操作原子性。

六、线程死锁(Deadlock)

现象

  • 部分业务突然卡住,无法推进(如订单支付、库存扣减流程无响应);
  • jstack显示 “Found one Java-level deadlock”,涉及多个线程相互持有对方需要的锁;
  • 死锁线程持有资源(如数据库连接),导致其他线程无法获取资源,引发连锁阻塞。

常见原因

  1. 多线程获取锁的顺序不一致(如线程 A 先锁资源 1 再锁资源 2,线程 B 先锁资源 2 再锁资源 1);
  2. 锁嵌套过深(如方法 A 调用方法 B,两者都加锁,形成隐性的多锁竞争);
  3. 资源释放不及时(如try-finally中遗漏unlock(),导致锁长期被持有)。

排查与解决

  • jstack <pid>直接检测死锁,日志会明确标出相互等待的线程和锁对象;
  • 统一锁顺序:对所有资源按固定规则排序(如按对象哈希值、资源 ID),强制线程按顺序获取锁;
  • 减少锁嵌套:拆分大方法,避免一个方法同时持有多个锁;
  • 用带超时的锁(如tryLock(1, TimeUnit.SECONDS)),超时后释放已获锁并重试,避免永久死锁。

七、线程上下文切换过载

现象

  • CPU 利用率接近 100%,但业务逻辑执行效率低(如每秒处理请求量远低于预期);
  • 系统平均负载(load average)远高于 CPU 核心数;
  • 监控工具(如vmstat)显示cs(上下文切换次数)每秒几万甚至几十万。

常见原因

  1. 线程数过多(如线程池maximumPoolSize远大于 CPU 核心数,导致线程频繁切换);
  2. 大量线程处于RUNNABLE状态(如任务执行时间短,线程频繁争抢 CPU);
  3. 频繁的 IO 操作(如数据库查询、网络请求未做批量处理,线程频繁阻塞 / 唤醒)。

排查与解决

  • vmstat 1监控上下文切换次数(cs列),正常情况下应低于每秒 1 万;
  • 调整线程池参数:线程数建议设置为CPU核心数 * 2(CPU 密集型)或CPU核心数 + IO等待数(IO 密集型);
  • 批量处理任务:如将多次数据库查询合并为批量查询,减少 IO 次数;
  • 使用协程(如 Project Loom 的虚拟线程):虚拟线程切换成本极低,适合高并发 IO 场景。

八、ThreadLocal 内存泄漏

现象

  • 内存泄漏,老年代占用持续增长,Full GC 无法回收;
  • 堆转储(heap dump)中存在大量ThreadLocal$ThreadLocalMap和业务对象(如用户会话、数据库连接)。

常见原因

  1. 线程池复用线程:ThreadLocalThreadLocalMap随线程生命周期存在,若线程池核心线程不销毁,ThreadLocal值会长期驻留内存;
  2. 未手动清理:ThreadLocal使用后未调用remove(),导致Entrykey(弱引用)被回收,但value(强引用)仍被持有。

排查与解决

  • 分析 heap dump:用 MAT 工具查看Thread对象的threadLocals字段,定位未清理的ThreadLocal值;
  • 强制清理:在try-finally中调用threadLocal.remove(),确保线程复用后ThreadLocal值被清除;
  • 避免存储大对象:ThreadLocal中只放轻量数据(如用户 ID),而非大对象(如完整会话信息)。

九、并发工具使用不当导致的异常

现象

  • 偶现IllegalMonitorStateException(调用wait()/notify()时未持有锁);
  • ConcurrentModificationException(迭代ArrayList时被其他线程修改);
  • 线程池任务执行结果丢失(如submit()Future未调用get(),异常被吞噬)。

常见原因

  1. 同步方法与非同步方法混用(如HashMapput加锁,但get未加锁,导致脏读);
  2. 忽略Future的返回值:submit()提交的任务若抛出异常,需通过get()获取,否则异常会被FutureTask吞噬;
  3. 并发容器使用误区(如CopyOnWriteArrayListiterator是快照,迭代时看不到新元素,导致业务逻辑错误)。

排查与解决

  • 日志埋点:在任务执行前后打印日志,记录异常信息(尤其是CompletableFutureexceptionally回调);
  • 替换为线程安全容器:用ConcurrentHashMap替代HashMapCopyOnWriteArrayList替代ArrayList(读多写少场景);
  • 严格遵循同步规范:调用wait()/notify()前必须持有对象锁,迭代并发容器时避免修改操作。

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10