Skip to content

并发陷阱

并发编程的坑,比普通编程多得多。

有些错误,一眼就能看出来;有些错误,藏在深处,测试环境好好的,生产环境就崩了。本文总结最常见的并发陷阱,帮你避开。

五大陷阱

陷阱后果常见程度
竞态条件结果不确定非常高
死锁程序卡死
内存可见性数据不一致
线程泄漏资源耗尽
对象逸出线程不安全

陷阱一:竞态条件

最常见的并发 bug。

场景:延迟初始化

java
// ❌ 不安全的延迟初始化
public class UnsafeLazyInit {

    private Object resource;

    public Object getResource() {
        if (resource == null) {           // 线程A、B 可能同时通过
            resource = new Object();        // 两个线程各创建一个对象
        }
        return resource;                   // 可能返回不同的对象
    }
}

// ❌ 同步但低效
public class SyncLazyInit {

    private Object resource;

    public synchronized Object getResource() {
        if (resource == null) {
            resource = new Object();
        }
        return resource;
    }
}

// ✅ 双重检查锁定
public class SafeLazyInit {

    private volatile Object resource;

    public Object getResource() {
        if (resource == null) {
            synchronized (this) {
                if (resource == null) {
                    resource = new Object();
                }
            }
        }
        return resource;
    }
}

场景:先检查后执行

java
public class CheckThenAct {

    private int value = 0;

    // ❌ 先检查后执行,不是原子操作
    public void increment() {
        if (value < 100) {       // 检查
            value++;               // 执行(可能超出限制)
        }
    }

    // ✅ 使用原子类
    public void incrementSafe() {
        while (true) {
            int current = value;
            if (current >= 100) {
                break;
            }
            if (compareAndSet(current, current + 1)) {
                break;
            }
        }
    }
}

陷阱二:死锁

程序卡死,无法自动恢复。

场景:嵌套锁顺序不一致

java
public class DeadlockRisk {

    private final Object lockA = new Object();
    private final Object lockB = new Object();

    // ❌ 两个方法的锁顺序相反
    public void method1() {
        synchronized (lockA) {
            synchronized (lockB) {
                // 操作
            }
        }
    }

    public void method2() {
        synchronized (lockB) {   // 顺序相反!
            synchronized (lockA) {
                // 操作
            }
        }
    }
}

// ✅ 统一锁顺序
public class SafeLockOrder {

    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void method1() {
        synchronized (lockA) {
            synchronized (lockB) {
                // 操作
            }
        }
    }

    public void method2() {
        synchronized (lockA) {   // 同样先 A 后 B
            synchronized (lockB) {
                // 操作
            }
        }
    }
}

场景:调用外部方法时持锁

java
public class CallingExternalMethod {

    private final Object lock = new Object();
    private final ExternalService service = new ExternalService();

    // ❌ 调用外部方法时持锁,可能死锁
    public void callWithLock() {
        synchronized (lock) {
            service.call();  // 如果 service 内部也用了 lock,可能死锁
        }
    }

    // ✅ 缩小锁范围
    public void callWithoutLock() {
        Object result;
        synchronized (lock) {
            result = prepareData();
        }
        service.call();  // 在锁外调用外部方法
    }
}

陷阱三:内存可见性

一个线程的修改,另一个线程看不到。

场景:共享可变状态

java
public class VisibilityIssue {

    // ❌ 普通变量:可能不可见
    private static boolean done = false;
    private static int result = 0;

    // ✅ volatile 变量:保证可见性
    private static volatile boolean volDone = false;
    private static volatile int volResult = 0;

    public static void main(String[] args) {
        // reader 线程可能永远看不到 writer 线程的修改
    }
}

场景:i++ 的陷阱

java
public class IncrementTrap {

    // ❌ volatile 不能保证复合操作的原子性
    private static volatile int counter = 0;

    public static void increment() {
        // 看似一个操作,实际三个步骤
        // volatile 只保证每次读写是最新的
        // 不能保证 i++ 本身是原子的
        counter++;
    }

    // ✅ 使用原子类
    private static final AtomicInteger atomicCounter = new AtomicInteger();

    public static void incrementSafe() {
        atomicCounter.incrementAndGet();
    }
}

陷阱四:线程泄漏

资源耗尽,程序崩溃。

场景:线程池未关闭

java
public class ThreadPoolLeak {

    // ❌ 每次调用都创建线程池,不关闭
    public void submit(Runnable task) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(task);
        // executor 没有关闭!
        // 线程池的线程永远不会结束
    }

    // ✅ 正确关闭
    public void submitSafe(Runnable task) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(task);
        executor.shutdown();  // 关闭,不再接受新任务
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();  // 超时强制关闭
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    // ✅ 共享线程池
    private static final ExecutorService SHARED_EXECUTOR =
        Executors.newFixedThreadPool(10);

    public void submitShared(Runnable task) {
        SHARED_EXECUTOR.submit(task);
        // 由应用统一管理生命周期
    }
}

场景:ThreadLocal 未清理

java
public class ThreadLocalLeak {

    private static final ThreadLocal<Object> data = new ThreadLocal<>();

    // ❌ 使用后未清理
    public void process1() {
        data.set(new byte[10 * 1024 * 1024]);  // 10MB
        // 用完没清理,线程复用后内存泄漏
    }

    // ✅ 正确清理
    public void process2() {
        try {
            data.set(new byte[10 * 1024 * 1024]);
            // 业务逻辑
        } finally {
            data.remove();  // 必须清理
        }
    }
}

陷阱五:对象逸出

本不该被其他线程访问的对象,逃出去了。

场景:在构造函数中 this 逸出

java
public class ThisEscape {

    // ❌ 在构造函数中启动线程,this 逸出
    public ThisEscape() {
        new Thread(() -> {
            // 此时对象可能还没构造完成
            System.out.println(this);
        }).start();
    }
}

// ✅ 使用工厂方法延迟构造
public class SafeConstruction {

    private final Thread thread;

    private SafeConstruction() {
        thread = new Thread(() -> {
            // 构造完成后才能访问
        });
    }

    // 工厂方法
    public static SafeConstruction create() {
        SafeConstruction instance = new SafeConstruction();
        instance.thread.start();  // 对象构造完成后才启动线程
        return instance;
    }
}

场景:内部状态逸出

java
public class StateEscape {

    // ❌ 返回可变内部集合的引用
    private final List<String> items = new ArrayList<>();

    public List<String> getItems() {
        return items;  // 调用者可以修改内部状态
    }

    // ✅ 返回不可变副本
    public List<String> getItemsSafe() {
        return Collections.unmodifiableList(new ArrayList<>(items));
    }
}

陷阱速查表

陷阱代码特征后果修复
竞态条件共享变量 + 复合操作结果不确定synchronized / 原子类
死锁多把锁 + 不一致顺序程序卡死固定顺序 / tryLock
可见性普通变量跨线程数据不一致volatile / synchronized
线程泄漏线程池未关闭OOMshutdown() / TTL 清理
对象逸出构造函数逸出 this不可预测工厂方法 / 完成后启动

预防原则

  1. 最小化共享:能用局部变量就不用共享
  2. 不可变优先:能用 final 就加 final
  3. 显式同步:不清楚是否线程安全时,默认加锁
  4. 统一锁顺序:多把锁时固定获取顺序
  5. 资源清理:ThreadLocal/线程池必须清理
  6. 代码审查:并发代码必须 review

要点回顾

  • 竞态条件:共享变量 + 复合操作 → 使用原子类
  • 死锁:多锁顺序不一致 → 固定锁顺序 / tryLock
  • 可见性:普通变量跨线程 → volatile / synchronized
  • 线程泄漏:资源未关闭 → 正确清理
  • 对象逸出:this 在构造时逃出 → 工厂方法

相关链接

基于 VitePress 构建