前言
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家: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. 原理解析
线程安全的三大核心问题
- 竞态条件(Race Condition):多个线程对共享资源的非原子操作导致结果不确定性
- 内存可见性:线程本地缓存与主内存不一致(需
volatile
或锁保证可见性) - 指令重排序:编译器/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. 工具与最佳实践
并发调试工具
- jstack:
jstack <pid> > thread_dump.txt # 分析线程状态与锁持有情况
- Arthas:
watch com.example.Service * '{params, throwExp}' -x 3 # 动态监控方法调用
代码规范建议
- 锁范围最小化:只在必要代码块加锁
- 避免嵌套锁:预防死锁(按固定顺序获取锁)
- 优先使用并发工具类:如
CountDownLatch
、CyclicBarrier
、Semaphore
- 禁止在锁内调用外部方法:防止未知阻塞
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
,服务不可用
修复方案:
- 替换为
ConcurrentHashMap
- 使用
computeIfAbsent
原子化初始化操作 - 增加缓存空值(
Null Object
模式)
总结
- 线程安全是设计出来的:而非通过加锁硬编码
- 锁是性能敌人:无锁算法 > 细粒度锁 > 粗粒度锁
- 工具决定效率:JUC并发包 + 监控工具 = 高可用基石
- 池化资源需谨慎:参数调优与拒绝策略缺一不可
下期预告:《浅拷贝与深拷贝:对象复制的隐藏风险》——从内存泄漏到数据篡改,全面解析对象复制的正确姿势。
联系作者
职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集