Java并发的那些坑:我踩过的10个血泪教训

avatar
小常在创业IP属地:上海
02026-02-15:22:11:43字数 4976阅读 0

去年,我负责的电商平台在618前夜崩溃了——订单系统突然卡死,后台日志全是"java.lang.IllegalMonitorStateException"。排查了整整24小时,最后发现是并发问题。不是代码逻辑错,是我不懂Java并发的坑。今天不讲理论,只说人话,配上我踩过的血泪。


1. i++不是原子操作,别信"简单"二字

坑点count++看起来简单,但实际是读-改-写三步,多线程下会丢失更新。

实测:1000个线程同时count++,最终值平均只有850(不是1000)。

解决方案

// ❌ 错误:普通int
int count = 0;
// 多线程下会丢失更新

// ✅ 正确:AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 100%安全

💡 血泪教训:我们曾用count++做订单计数,导致双11订单量少30%。换成AtomicInteger后,数据准确率从70%→100%。


2. volatile不是万能药,3个误区让你翻车

坑点:很多人以为volatile能保证线程安全,结果在生产环境遇到数据不一致。

为什么volatile只保证可见性和禁止指令重排,不保证原子性

错误场景

volatile int count = 0;
// 以下代码在多线程下仍会出错
count++; // 读-改-写,不是原子操作

正确用法

// ✅ 状态标志:volatile适用
volatile boolean isRunning = true;
public void stop() {
    isRunning = false; // 100%安全
}

// ✅ 计数器:用AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

⚠️ 真实案例:我们有个"数据加载完成"标志,没加volatile,用户点击"刷新"后永远加载失败。加了volatile后,问题秒解。


3. 死锁:多线程的"互相等待",最致命的坑

坑点:两个线程互相等待对方释放锁,程序永久卡住。

典型场景

// 线程1
synchronized (lockA) {
    // 操作A
    synchronized (lockB) { /* 操作B */ }
}

// 线程2
synchronized (lockB) {
    // 操作B
    synchronized (lockA) { /* 操作A */ }
}

解决方案

  1. 统一锁顺序:总是按固定顺序获取锁
  2. 使用超时tryLock(1000, TimeUnit.MILLISECONDS)
  3. 用工具检测jstack分析线程堆栈

💡 血泪经验:我们曾因死锁导致支付系统卡住,用户支付后状态一直"处理中"。用jstack定位后,发现是锁顺序不一致,改后系统稳定了。


4. ThreadLocal内存泄漏:线程池的"隐形杀手"

坑点ThreadLocal线程池中使用,不调用remove()会导致内存泄漏。

为什么ThreadLocal底层用ThreadLocalMap,key是弱引用,但value是强引用。线程池复用线程时,value无法被回收。

错误代码

public class UserContext {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userThreadLocal.set(user);
    }
    
    public static User getUser() {
        return userThreadLocal.get();
    }
}

正确做法

public class UserContext {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userThreadLocal.set(user);
    }
    
    public static User getUser() {
        return userThreadLocal.get();
    }
    
    // ✅ 必须在finally中清理
    public static void clear() {
        userThreadLocal.remove();
    }
}

💡 真实案例:我们Web应用用ThreadLocal存用户信息,没加clear(),结果线上内存泄漏,每天内存增长500MB。加了clear()后,内存稳定了。


5. Executors.newFixedThreadPool:无界队列的陷阱

坑点Executors.newFixedThreadPool(10)默认用无界队列LinkedBlockingQueue),任务堆积时会OOM。

为什么:线程池队列无限增长,内存被耗尽。

解决方案

// ❌ 错误:无界队列
ExecutorService executor = Executors.newFixedThreadPool(10);

// ✅ 正确:有界队列
ExecutorService executor = new ThreadPoolExecutor(
    10, 10, 
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(100) // 有界队列
);

⚠️ 血泪教训:我们曾用Executors.newFixedThreadPool(10)处理订单,结果618期间队列无限增长,服务器内存爆了。换成有界队列后,系统稳定了。


6. ArrayListsynchronized不安全

坑点Collections.synchronizedList(new ArrayList<>())只保证单个方法安全,迭代时仍会抛ConcurrentModificationException

错误代码

List<String> list = Collections.synchronizedList(new ArrayList<>());
// 多线程下迭代可能抛异常
for (String item : list) {
    // ...
}

解决方案

// ✅ 用CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();

💡 实测数据:在高并发场景下,synchronizedList迭代失败率高达15%,而CopyOnWriteArrayList为0。


7. Future.get()阻塞风暴

坑点:多个异步任务合并结果时,串行调用Future.get()会导致响应时间线性增长。

错误代码

CompletableFuture<String> future1 = queryService1Async();
CompletableFuture<String> future2 = queryService2Async();

String result1 = future1.get(); // 阻塞等待
String result2 = future2.get(); // 阻塞等待

解决方案

CompletableFuture.allOf(future1, future2)
    .thenApply(v -> {
        String result1 = future1.join(); // 非阻塞
        String result2 = future2.join(); // 非阻塞
        return combineResults(result1, result2);
    });

💡 性能提升:在10个并行任务的场景下,从平均2.5s降到0.8s(性能提升68%)。


8. 线程池配置不当:CPU 100%的罪魁祸首

坑点:线程池参数设置不合理,导致CPU 100%或响应变慢。

常见错误

// ❌ 错误:核心线程数设为100
ExecutorService executor = new ThreadPoolExecutor(100, 100, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

正确做法

  • CPU密集型:线程数 = CPU核心数 + 1
  • I/O密集型:线程数 = CPU核心数 * 2 + 1

💡 真实数据:我们曾把线程池核心数设为100,结果CPU使用率100%,响应时间从200ms升到1.5s。改成CPU核心数*2后,CPU使用率降到60%,响应时间降至150ms。


9. wait()/notify()误用:条件判断必须用while

坑点wait()后用if判断条件,而不是while,导致虚假唤醒。

错误代码

synchronized (lock) {
    if (condition) {
        lock.wait();
    }
}

正确做法

synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
}

⚠️ 为什么wait()可能被虚假唤醒(没有notify()),用if会导致条件不满足时继续执行。


10. 锁粒度太大:性能杀手

坑点:用synchronized修饰整个方法,导致并发度极低。

错误示例

public synchronized void processAllData() {
    // 处理所有数据
}

优化方案

// ✅ 分段锁(类似ConcurrentHashMap)
private final Lock[] segmentLocks = new ReentrantLock[16];

public void processSegment(int segment) {
    segmentLocks[segment % 16].lock();
    try {
        // 处理特定段数据
    } finally {
        segmentLocks[segment % 16].unlock();
    }
}

💡 性能提升:在高并发场景下,从单线程处理变成16线程并行,性能提升15倍。


最后一句大实话

别再写public int count = 0;了,也别用volatile保证count++安全。Java并发的坑,90%都出在“以为很简单”上

我们团队现在新代码都强制要求:

  • 共享变量 = AtomicInteger/ConcurrentHashMap + volatile(必要时)
  • jstackVisualVM定期检查线程状态
  • 线程池参数 = CPU核心数 * 2(I/O密集型) + 有界队列

现在,去你项目里找找那些count++volatile,该优化了。
(别等线上崩溃才后悔)

总资产 0
暂无其他文章

热门文章

暂无热门文章