并发陷阱
并发编程的坑,比普通编程多得多。
有些错误,一眼就能看出来;有些错误,藏在深处,测试环境好好的,生产环境就崩了。本文总结最常见的并发陷阱,帮你避开。
五大陷阱
| 陷阱 | 后果 | 常见程度 |
|---|---|---|
| 竞态条件 | 结果不确定 | 非常高 |
| 死锁 | 程序卡死 | 高 |
| 内存可见性 | 数据不一致 | 高 |
| 线程泄漏 | 资源耗尽 | 中 |
| 对象逸出 | 线程不安全 | 中 |
陷阱一:竞态条件
最常见的并发 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 |
| 线程泄漏 | 线程池未关闭 | OOM | shutdown() / TTL 清理 |
| 对象逸出 | 构造函数逸出 this | 不可预测 | 工厂方法 / 完成后启动 |
预防原则
- 最小化共享:能用局部变量就不用共享
- 不可变优先:能用 final 就加 final
- 显式同步:不清楚是否线程安全时,默认加锁
- 统一锁顺序:多把锁时固定获取顺序
- 资源清理:ThreadLocal/线程池必须清理
- 代码审查:并发代码必须 review
要点回顾
- 竞态条件:共享变量 + 复合操作 → 使用原子类
- 死锁:多锁顺序不一致 → 固定锁顺序 / tryLock
- 可见性:普通变量跨线程 → volatile / synchronized
- 线程泄漏:资源未关闭 → 正确清理
- 对象逸出:this 在构造时逃出 → 工厂方法
相关链接
- 并发问题排查 - 如何排查这些问题
- 原子性 - 竞态条件的根源
- ThreadLocal 内存泄漏 - ThreadLocal 的清理
