前言
多线程是 Java 进阶的必经之路,也是后端开发中最容易踩坑的领域。很多开发者在单线程环境下写的代码运行正常,一到并发场景就出现各种诡异问题:数据不一致、死锁、内存泄漏、线程静默终止…… 这些问题往往复现困难,排查成本极高。
本文整理了 Java 多线程开发中 10 个最常见的坑点,结合代码示例分析问题原因,并给出对应的最佳实践,帮你避开并发编程中的陷阱。
一、对象发布逸出:不当初始化导致线程安全问题
问题描述
“对象逸出” 指的是对象还未完成初始化,就被其他线程访问到。最常见的场景是在构造方法中启动线程,或在构造方法中注册事件监听器,导致this引用逸出。
// 反例:构造方法中启动线程,this引用逸出
public class UnsafeTask {
private final int number;

public UnsafeTask(int number) {
    new Thread(() -> {
        // 其他线程可能访问到未初始化完成的对象
        System.out.println("读取到的数值:" + this.number);
    }).start();
    this.number = number; // 线程启动后才完成赋值
}

}
上述代码中,子线程可能在number赋值完成前就读取,导致拿到默认值 0,出现数据错误。
最佳实践
不要在构造方法中启动线程或注册监听器;
使用工厂方法,对象初始化完成后再启动线程:
public class SafeTask {
private final int number;

private SafeTask(int number) {
    this.number = number;
}

// 工厂方法:对象构造完成后再启动线程
public static SafeTask newInstance(int number) {
    SafeTask task = new SafeTask(number);
    new Thread(() -> System.out.println(task.number)).start();
    return task;
}

}
二、volatile 关键字误用:不能保证原子性
问题描述
很多初学者认为volatile是线程安全的,用它修饰变量后就可以放心地在多线程中做运算。实际上volatile只能保证可见性和禁止指令重排序,无法保证原子性。
// 反例:volatile修饰的变量做自增,线程不安全
public class VolatileDemo {
private static volatile int count = 0;

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                count++; // 自增是读-改-写三步操作,非原子
            }
        }).start();
    }
    Thread.sleep(2000);
    System.out.println("最终结果:" + count); // 结果大概率小于10000
}

}
最佳实践
纯赋值操作(如boolean flag = true)可以用volatile保证可见性;
计数、累加等复合操作,使用AtomicInteger等原子类,或加锁实现。
三、非线程安全集合的并发修改异常
问题描述
ArrayList、HashMap 等非线程安全集合,在多线程并发修改时,会抛出ConcurrentModificationException;即使不抛异常,也可能出现数据丢失、死循环等严重问题。
最佳实践
并发读多写少场景:使用CopyOnWriteArrayList、CopyOnWriteArraySet;
并发键值对存储:使用ConcurrentHashMap替代 HashMap;
高频写场景:使用加锁的方式,或选择对应并发集合类;
遍历集合时修改元素,使用迭代器的remove()方法,而非集合自身的 remove。
四、线程池不规范创建:隐藏的资源耗尽风险
问题描述
很多图方便直接用Executors工具类创建线程池,比如Executors.newFixedThreadPool()、Executors.newCachedThreadPool()。但这些默认实现都有资源风险:
FixedThreadPool和SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能堆积大量请求,导致 OOM;
CachedThreadPool和ScheduledThreadPool:允许创建的线程数为 Integer.MAX_VALUE,可能创建大量线程,导致 CPU 和内存耗尽。
最佳实践
手动通过ThreadPoolExecutor构造线程池,明确指定核心线程数、最大线程数、队列类型、拒绝策略,根据业务场景合理配置:
// 规范的线程池创建示例
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5, // 核心线程数
20, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadFactoryBuilder().setNameFormat(“biz-thread-%d”).build(), // 自定义线程名
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用方线程执行
);
额外建议:给线程池自定义线程名称,方便后续排查问题时定位线程来源。
五、锁对象不当导致的死锁
问题描述
死锁是多线程中最经典的问题,当两个线程互相持有对方需要的锁,且都不释放时,就会造成程序永久阻塞。最常见的场景是多个锁获取顺序不一致。
// 死锁示例
public class DeadLockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();

public static void main(String[] args) {
    new Thread(() -> {
        synchronized (LOCK_A) {
            System.out.println("线程1拿到锁A");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (LOCK_B) { // 等待锁B
                System.out.println("线程1拿到锁B");
            }
        }
    }).start();

    new Thread(() -> {
        synchronized (LOCK_B) {
            System.out.println("线程2拿到锁B");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (LOCK_A) { // 等待锁A
                System.out.println("线程2拿到锁A");
            }
        }
    }).start();
}

}
最佳实践
保证多个线程获取锁的顺序一致;
使用tryLock()方法设置超时时间,避免无限等待;
尽量减少锁的嵌套使用,缩小锁的粒度。
六、ThreadLocal 内存泄漏问题
问题描述
ThreadLocal 可以实现线程级别的变量隔离,但使用不当很容易造成内存泄漏。ThreadLocal 的 key 是弱引用,当 ThreadLocal 对象被回收后,key 变为 null,但 value 是强引用,不会被回收。如果线程长期存活(比如线程池中的核心线程),value 就会一直占用内存,最终导致内存泄漏。
最佳实践
每次使用完 ThreadLocal 后,必须调用remove()方法清理数据;
尽量避免存储大对象;
放在 try-finally 块中保证清理执行:
ThreadLocal threadLocal = new ThreadLocal<>();
try {
threadLocal.set(new UserContext());
// 业务逻辑
} finally {
threadLocal.remove(); // 确保清理
}
七、wait/notify 使用的常见错误
问题描述
wait()和notify()是 Object 类的方法,用于线程间通信,但很多开发者使用时容易踩坑:
不在同步代码块中调用,抛出IllegalMonitorStateException;
用if判断等待条件,被唤醒后不重新校验条件,导致程序异常(虚假唤醒);
notify()随机唤醒一个线程,可能唤醒的不是目标线程,导致信号丢失。
最佳实践
wait/notify 必须在 synchronized 同步块中使用;
永远在循环中调用 wait (),唤醒后重新判断条件;
优先使用notifyAll()唤醒所有等待线程,避免信号丢失。
// 规范写法
synchronized (lock) {
while (条件不满足) {
lock.wait();
}
// 执行业务逻辑
}
八、线程优先级的不可靠性
很多人喜欢通过setPriority()设置线程优先级,认为优先级高的线程一定会先执行。实际上 Java 线程优先级依赖操作系统的线程调度,优先级设置只是 “建议”,操作系统可能完全忽略,不能依赖优先级来保证业务执行顺序。
最佳实践:不要依赖线程优先级做业务逻辑控制,需要顺序执行的场景用锁、同步工具类(CountDownLatch、CyclicBarrier)来实现。
九、异常捕获缺失导致线程静默终止
问题描述
线程的 run 方法中如果抛出未捕获的运行时异常,线程会直接终止,且默认不会打印任何日志,上层代码也感知不到,最终表现为 “业务莫名不执行了”,排查非常困难。
最佳实践
run 方法内用 try-catch 捕获所有异常,做好日志记录;
给线程池或线程设置UncaughtExceptionHandler,统一处理未捕获异常;
使用submit()提交任务时,通过 Future 的 get 方法捕获异常。
十、锁粒度不当:性能与安全的失衡
问题描述
锁粒度的把控是多线程开发的难点:
锁粒度太大:同一时间只有一个线程能执行,并发度低,性能差;
锁粒度太小:锁的次数过多,锁切换开销大,还容易出现死锁。
最佳实践
只对必要的代码块加锁,不要给整个方法加锁;
能局部变量实现的,就不要加锁;
读写分离场景,使用ReentrantReadWriteLock读写锁,读共享、写互斥,提升并发性能。
总结
并发编程的坑点远不止这 10 个,本质上都是对 Java 内存模型、线程调度、锁机制的理解不到位导致的。开发中我们要始终对并发保持敬畏之心,遵循最佳实践,做好异常处理和日志打印,上线前做好并发压测,才能最大程度避免线上问题。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐