并发编程初学者的噩梦:线程安全与锁机制误区

本文介绍了如何搭建Apollo自动驾驶系统,从安装Linux到编译Apollo源码,再到验证Dreamview,详细阐述了每个步骤,适合自动驾驶技术爱好者和开发者。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

请添加图片描述

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家https://www.captainbed.cn/z
在这里插入图片描述
请添加图片描述


1. 错误场景复现

场景1:非线程安全的集合操作

// 多线程共享HashMap导致数据错乱
Map<String, Integer> counterMap = new HashMap<>();

// 线程1
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        counterMap.put("key", counterMap.getOrDefault("key", 0) + 1);
    }
}).start();

// 线程2
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        counterMap.put("key", counterMap.getOrDefault("key", 0) + 1);
    }
}).start();

// 预期结果:2000,实际结果可能为 1000~2000 之间的任意值

后果:数据不一致、丢失更新,甚至引发ConcurrentModificationException


场景2:错误使用 synchronized 锁

// 误用锁对象导致同步失效
private Integer lock = 1; // Integer对象不可变,每次赋值创建新对象!

public void unsafeUpdate() {
    synchronized (lock) { // 锁对象变化,同步块失效
        // 业务逻辑
    }
    lock++; // 修改锁对象引用
}

隐患:看似加锁,实际每个线程持有不同的锁对象,完全无同步效果。


场景3:线程池配置灾难

// 创建无界队列线程池
ExecutorService executor = Executors.newCachedThreadPool();

// 提交大量任务
for (int i = 0; i < 1_000_000; i++) {
    executor.submit(() -> {
        // 执行耗时操作
        Thread.sleep(1000);
        return;
    });
}

后果:任务队列无限堆积,最终导致OutOfMemoryError: unable to create new native thread


2. 原理解析

线程安全的三大核心问题

  1. 竞态条件(Race Condition):多个线程对共享资源的非原子操作导致结果不确定性
  2. 内存可见性:线程本地缓存与主内存不一致(需volatile或锁保证可见性)
  3. 指令重排序:编译器/CPU优化打乱执行顺序(happens-before规则约束)

synchronized 的底层实现

// synchronized 字节码示例
monitorenter  // 进入锁(依赖对象头的Mark Word)
// 临界区代码
monitorexit   // 释放锁
  • 锁升级机制:无锁 → 偏向锁 → 轻量级锁(CAS自旋) → 重量级锁(OS互斥量)
  • 锁粗化/消除:JVM优化策略(合并相邻锁、去除不可能竞争的锁)

线程池参数陷阱

参数错误配置正确实践
corePoolSize设为0根据CPU核心数设置(N+1)
workQueue使用无界队列(如LinkedBlockingQueue)使用有界队列 + 拒绝策略
keepAliveTime设置过大(如1小时)根据任务特性设置(通常60秒)
rejectedHandler忽略(默认AbortPolicy)自定义降级策略(如记录日志后丢弃)

3. 正确解决方案

方案1:使用JUC并发集合

// 替换HashMap为ConcurrentHashMap
Map<String, Integer> safeMap = new ConcurrentHashMap<>();

// 原子更新操作
safeMap.compute("key", (k, v) -> (v == null) ? 1 : v + 1);

// 或使用LongAdder(更高性能的计数器)
LongAdder adder = new LongAdder();
adder.increment();
选型指南
  • ConcurrentHashMap:高并发读场景
  • CopyOnWriteArrayList:读多写少场景
  • BlockingQueue:生产者-消费者模式

方案2:显式锁与原子类

// 使用ReentrantLock替代synchronized
private final Lock lock = new ReentrantLock();

public void safeUpdate() {
    lock.lock();
    try {
        // 业务逻辑
    } finally {
        lock.unlock(); // 确保解锁
    }
}

// 使用AtomicInteger实现无锁化
private AtomicInteger counter = new AtomicInteger(0);

public void increment() {
    counter.incrementAndGet(); // CAS操作
}
优势
  • 更灵活的锁机制(可中断、超时、公平锁)
  • 避免锁粗化带来的性能损耗

方案3:线程池标准化创建

// 手动创建线程池(避免Executors陷阱)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4, // corePoolSize
    8, // maxPoolSize
    60, // keepAliveTime(秒)
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000), // 有界队列
    new ThreadFactoryBuilder().setNameFormat("worker-%d").build(),
    new CustomRejectPolicy() // 自定义拒绝策略
);

// 自定义拒绝策略(记录日志后丢弃)
static class CustomRejectPolicy implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        log.warn("任务被拒绝:{}", r.toString());
    }
}

4. 工具与最佳实践

并发调试工具

  1. jstack
    jstack <pid> > thread_dump.txt  # 分析线程状态与锁持有情况
    
  2. Arthas
    watch com.example.Service * '{params, throwExp}' -x 3  # 动态监控方法调用
    

代码规范建议

  • 锁范围最小化:只在必要代码块加锁
  • 避免嵌套锁:预防死锁(按固定顺序获取锁)
  • 优先使用并发工具类:如CountDownLatchCyclicBarrierSemaphore
  • 禁止在锁内调用外部方法:防止未知阻塞

5. Code Review检查清单

检查项正确做法
是否使用线程安全集合?用ConcurrentHashMap代替HashMap
锁对象是否被意外修改?用final修饰锁对象(如private final Object lock = new Object()
线程池是否配置拒绝策略?禁止使用默认的AbortPolicy(引发RejectedExecutionException)
是否误用volatile?volatile仅保证可见性,不保证原子性

6. 真实案例

某交易系统使用HashMap缓存最新价格:

private static Map<String, BigDecimal> priceCache = new HashMap<>();

// 多线程更新缓存
public void updatePrice(String symbol, BigDecimal price) {
    priceCache.put(symbol, price);
}

// 多线程读取缓存
public BigDecimal getPrice(String symbol) {
    return priceCache.get(symbol);
}

后果

  • 部分线程读取到null值,导致交易异常
  • 高峰时段触发ConcurrentModificationException,服务不可用

修复方案

  1. 替换为ConcurrentHashMap
  2. 使用computeIfAbsent原子化初始化操作
  3. 增加缓存空值(Null Object模式)

总结

  • 线程安全是设计出来的:而非通过加锁硬编码
  • 锁是性能敌人:无锁算法 > 细粒度锁 > 粗粒度锁
  • 工具决定效率:JUC并发包 + 监控工具 = 高可用基石
  • 池化资源需谨慎:参数调优与拒绝策略缺一不可

下期预告:《浅拷贝与深拷贝:对象复制的隐藏风险》——从内存泄漏到数据篡改,全面解析对象复制的正确姿势。

联系作者

职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雪碧有白泡泡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值