1. JUC并发编程的本质理解
第一次接触java.util.concurrent包时,很多人会被其复杂的API体系所迷惑。实际上,JUC要解决的核心问题可以归结为:在多线程环境下,如何高效安全地管理共享状态。与传统的synchronized关键字相比,JUC提供了一整套更精细化的并发控制工具。
我在处理电商秒杀系统时深有体会:当QPS突破5万时,简单的synchronized会导致大量线程阻塞,而采用JUC的ReentrantLock配合Condition,不仅吞吐量提升3倍,还能实现更灵活的等待通知机制。这种性能差异源于JUC在硬件层面的优化——通过CAS操作、内存屏障等技术减少线程上下文切换。
2. 原子操作类的实战技巧
2.1 AtomicInteger的底层原理
AtomicInteger的incrementAndGet()看似简单,实则包含精妙设计。其内部通过Unsafe类直接操作内存,采用CAS(Compare And Swap)机制:比较工作内存与主存的值,若一致则更新,否则重试。在JDK8的压测中,AtomicInteger的吞吐量是synchronized版本的8倍。
关键细节:CAS存在ABA问题,如果值从A→B→A,CAS会误判无变化。解决方案是使用AtomicStampedReference添加版本号控制。
2.2 LongAdder的性能优化
当我在日志统计系统中用AtomicLong记录请求数时,发现高并发下性能急剧下降。LongAdder通过分段锁机制解决了这个问题:它将值分解到多个cell,线程竞争时只在对应cell上CAS,最终汇总结果。实测在100线程并发时,LongAdder的吞吐量是AtomicLong的6倍。
java复制// 典型使用场景
LongAdder counter = new LongAdder();
parallelStream().forEach(e -> {
counter.increment(); // 无竞争递增
});
3. 并发容器的选型策略
3.1 ConcurrentHashMap的分段进化
JDK7的ConcurrentHashMap采用分段锁,而JDK8改为synchronized+CAS+红黑树。这种变化带来两个实际影响:
- 当我的缓存系统迁移到JDK8后,查询性能提升40%
- resize时从全局锁变为协助扩容,写操作不再完全阻塞
避坑指南:size()方法在并发环境下不精确,如需精确计数应使用mappingCount()
3.2 CopyOnWriteArrayList的适用场景
在配置中心监听器实现中,我对比了三种方案:
- Vector:全表锁导致1000并发时RT高达200ms
- Collections.synchronizedList:同Vector
- CopyOnWriteArrayList:写时复制使RT稳定在15ms
代价是内存占用增加,适合读多写少(如监听器注册)的场景。写入时会有短暂延迟,因为要复制整个数组。
4. 线程池的深度调优
4.1 参数配置黄金法则
根据线上事故总结的公式:
- 计算密集型:corePoolSize = CPU核数 + 1
- IO密集型:corePoolSize = CPU核数 * (1 + 平均等待时间/平均计算时间)
java复制// 电商订单处理线程池配置案例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8, // 8核服务器IO密集型
16,
60s,
new LinkedBlockingQueue(1000), // 根据内存调整
new NamedThreadFactory("order-process"),
new CallerRunsPolicy() // 保证不丢订单
);
4.2 工作队列选型对比
在消息推送系统中测试三种队列:
- SynchronousQueue:高吞吐但易拒绝任务
- LinkedBlockingQueue:内存可控但响应慢
- ArrayBlockingQueue:平衡之选,建议设置合理容量
实测数据:当QPS=3000时,ArrayBlockingQueue(2000)的99线比LinkedBlockingQueue低30ms
5. AQS的实战应用
5.1 ReentrantLock的非公平锁优势
在分布式锁本地缓存方案中,对比发现:
- 公平锁:线程按序获取,吞吐量1200/s
- 非公平锁:允许插队,吞吐量2100/s
这是因为非公平锁减少了线程切换,但可能导致饥饿。建议:追求吞吐量用非公平锁,需要公平性则用公平锁。
5.2 CountDownLatch与CyclicBarrier的区别
在压测平台中的典型用法:
java复制// 模拟并发请求
CountDownLatch start = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(100);
for(int i=0; i<100; i++){
new Thread(() -> {
start.await(); // 等待发令枪
doRequest();
end.countDown();
}).start();
}
start.countDown(); // 同时启动
end.await(); // 等待全部完成
CyclicBarrier更适合分阶段任务,比如对账系统中的多源数据采集阶段。
6. 锁优化实战记录
6.1 避免死锁的编码规范
- 按固定顺序获取多把锁(如按hash排序)
- 使用tryLock设置超时时间
- 通过jstack检测死锁时,重点关注BLOCKED状态的线程
6.2 偏向锁的失效场景
在JDK15的测试环境中发现,当:
- 调用hashCode()会撤销偏向锁
- 超过20个线程竞争时自动升级为轻量级锁
- 使用wait()会直接升级为重量级锁
可以通过-XX:BiasedLockingStartupDelay=0取消延迟启用偏向锁。
7. 并发编程的监控手段
7.1 JStack诊断线程阻塞
分析线程dump时的关键步骤:
- grep "BLOCKED"查看阻塞线程
- 查找"waiting to lock <0x0000000713f0b3d8>"定位锁地址
- 对照"holding lock <0x0000000713f0b3d8>"找到持有者
7.2 Arthas实时监控
常用命令示例:
bash复制watch java.util.concurrent.locks.ReentrantLock getQueueLength
trace com.example.OrderService process
8. 并发设计模式实践
8.1 生产者消费者模式
使用BlockingQueue的标准实现:
java复制BlockingQueue<Order> queue = new ArrayBlockingQueue<>(100);
// 生产者
executor.submit(() -> {
while(true){
Order order = createOrder();
queue.put(order); // 自动阻塞
}
});
// 消费者
executor.submit(() -> {
while(true){
Order order = queue.take();
process(order);
}
});
8.2 ThreadLocal的内存泄漏防护
正确使用模板:
java复制try {
threadLocal.set(user);
// 业务逻辑
} finally {
threadLocal.remove(); // 必须清理
}
建议使用FastThreadLocal(Netty实现)避免弱引用导致的性能问题。