1. JUC并发编程完全指南:从入门到精通
上周帮团队排查一个线上订单超卖问题,发现根本原因是开发人员对JUC组件理解不透彻。这让我意识到,虽然Java并发编程是面试必问点,但真正能系统掌握的人并不多。今天我就结合自己踩过的坑,带大家彻底吃透JUC这个Java并发编程利器。
JUC(java.util.concurrent)是Java 5引入的标准并发工具库,包含线程池、原子类、锁、阻塞队列等核心组件。不同于传统的synchronized关键字,JUC提供了更高层次的并发抽象,既能简化开发又能提升性能。根据我的经验,掌握JUC能让你:
- 轻松应对高并发场景
- 写出更健壮的多线程代码
- 在面试中脱颖而出
2. JUC核心组件详解与实战
2.1 线程池(ThreadPoolExecutor)
线程池是JUC中使用频率最高的组件。我见过太多人直接new Thread()创建线程,这在实际项目中是绝对要避免的。来看个电商秒杀场景的线程池配置示例:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
20, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000), // 任务队列
new ThreadFactoryBuilder().setNameFormat("order-process-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
关键参数选择经验:
- 核心线程数:通常等于CPU核心数(Runtime.getRuntime().availableProcessors())
- 队列容量:根据业务特点设置,IO密集型可设大些
- 拒绝策略:CallerRunsPolicy能保证不丢任务但会降低吞吐量
踩坑提醒:线程池一定要自定义命名!线上排查问题时"pool-1-thread-2"这种默认名称会让你痛不欲生。
2.2 锁机制(Lock vs Synchronized)
ReentrantLock比synchronized强大在哪?来看对比表:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 可重入 | ✓ | ✓ |
| 公平锁 | × | ✓ |
| 可中断 | × | ✓ |
| 超时获取 | × | ✓ |
| 条件变量 | × | ✓ |
实际项目中,我通常在以下场景选择ReentrantLock:
- 需要尝试获取锁(tryLock)
- 需要公平锁保证顺序
- 需要更细粒度的条件控制
java复制Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者代码示例
lock.lock();
try {
while(queue.isFull()) {
notFull.await();
}
queue.put(item);
notEmpty.signal();
} finally {
lock.unlock();
}
2.3 原子类(Atomic)
AtomicInteger等原子类是无锁编程的基础。它们的底层原理是CAS(Compare And Swap):
java复制// 伪代码展示CAS原理
public boolean compareAndSwap(int expect, int update) {
if(getValue() == expect) {
setValue(update);
return true;
}
return false;
}
实际项目中的应用场景:
- 计数器(如接口调用次数统计)
- 状态标志位(如服务开关)
- 序列号生成
性能对比:在低竞争环境下,AtomicLong比LongAdder快;高竞争时LongAdder性能更好,因为它采用了分段CAS。
3. 并发容器深度解析
3.1 ConcurrentHashMap演进史
| JDK版本 | 实现方式 | 特点 |
|---|---|---|
| 1.5 | 分段锁(16段) | 减小锁粒度 |
| 1.7 | 分段锁优化 | 二次哈希减少冲突 |
| 1.8 | CAS+synchronized | 链表转红黑树 |
1.8版本的putVal方法核心逻辑:
java复制final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 省略链表/红黑树处理逻辑...
}
}
addCount(1L, binCount);
return null;
}
3.2 阻塞队列选型指南
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
| ArrayBlockingQueue | 有界数组实现 | 固定大小线程池 |
| LinkedBlockingQueue | 可选有界链表 | 默认无界但危险 |
| PriorityBlockingQueue | 优先级队列 | 任务优先级调度 |
| SynchronousQueue | 不存储元素 | 直接传递任务 |
| DelayQueue | 延迟队列 | 定时任务调度 |
在消息中间件中,我常用LinkedBlockingQueue作为缓冲队列,但一定要设置合理容量:
java复制// 错误示范:无界队列可能导致OOM
BlockingQueue<Message> queue = new LinkedBlockingQueue<>();
// 正确做法:根据内存设置上限
BlockingQueue<Message> queue = new LinkedBlockingQueue<>(10000);
4. 并发编程实战技巧
4.1 避免死锁的5种方法
- 固定加锁顺序:所有线程按相同顺序获取锁
- 使用tryLock:设置超时时间避免无限等待
- 开放调用:不在持有锁时调用外部方法
- 锁分解:大锁拆分为多个小锁
- 锁粗化:连续的小锁合并为大锁
实际案例:银行转账问题
java复制// 错误写法:可能产生死锁
public void transfer(Account from, Account to, int amount) {
synchronized(from) {
synchronized(to) {
// 转账逻辑
}
}
}
// 正确写法:按hash排序
public void transfer(Account from, Account to, int amount) {
Account first = from.hashCode() < to.hashCode() ? from : to;
Account second = from.hashCode() < to.hashCode() ? to : from;
synchronized(first) {
synchronized(second) {
// 转账逻辑
}
}
}
4.2 线程安全设计模式
-
Immutable模式:
java复制@Immutable public final class Product { private final String id; private final BigDecimal price; // 只有构造方法可设置值 public Product(String id, BigDecimal price) { this.id = id; this.price = price; } // 只有getter方法 } -
ThreadLocal模式:
java复制private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public String formatDate(Date date) { return dateFormat.get().format(date); } -
CopyOnWrite模式:
java复制// 适合读多写少的场景 List<String> list = new CopyOnWriteArrayList<>();
5. 高频面试题深度剖析
5.1 AQS原理详解
AbstractQueuedSynchronizer是JUC的核心基础框架,ReentrantLock、CountDownLatch等都基于它实现。其核心是一个FIFO等待队列和state状态变量。
以ReentrantLock为例的获取锁流程:
- 尝试CAS修改state
- 失败后加入等待队列
- 进入自旋检查状态
- 被前驱节点唤醒
java复制// 简化的AQS获取锁逻辑
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
5.2 ConcurrentHashMap面试三连问
-
为什么1.8放弃分段锁?
- 内存消耗大(每个段都是独立HashEntry数组)
- 并发度固定(创建时确定)
- CAS性能提升使得细粒度锁不再必要
-
size()方法如何实现?
1.8版本采用分段计数baseCount+CounterCell数组,通过LongAdder思想减少竞争。 -
链表转红黑树的阈值为什么是8?
根据泊松分布,哈希冲突达到8的概率极低(小于千万分之一),此时转为树结构更划算。
6. 性能调优实战记录
6.1 线程池参数优化案例
某订单系统压测时出现的问题:
- 平均响应时间:200ms
- 最大TPS:500
- CPU利用率:30%
优化过程:
- 通过jstack发现大量线程处于WAITING状态
- 确认是线程池核心数设置过大(50核CPU设了50个核心线程)
- 调整为CPU核数*2(考虑IO等待)
- 增加队列大小(从100调到1000)
- 改用自定义RejectedExecutionHandler记录拒绝任务
优化后结果:
- TPS提升到1200
- CPU利用率提升到70%
- 99线响应时间降低50%
6.2 锁竞争优化方案
某支付系统出现性能瓶颈,jstack显示:
- 80%线程阻塞在同一个锁上
- 锁持有时间长达100ms
优化步骤:
- 锁分解:将大锁拆分为账户维度的小锁
- 引入分段锁:按账户ID哈希分16段
- 热点账户特殊处理:使用ThreadLocal缓冲
- 最终方案:结合CAS+分段锁
优化效果:
- 吞吐量提升8倍
- 锁等待时间降至5ms以内
7. 常见问题排查手册
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| CPU 100% | 死循环/死锁 | jstack查线程栈 |
| 内存泄漏 | 线程池未关闭/静态集合 | MAT分析堆转储 |
| 响应慢 | 锁竞争激烈 | 用arthas监控锁等待 |
| 数据不一致 | 可见性问题 | 检查volatile使用 |
| 线程数暴涨 | 未用线程池 | 限制最大线程数 |
线上问题排查三板斧:
- jstack:查看线程状态和锁持有情况
bash复制
jstack -l <pid> > thread.log - jmap:分析内存使用情况
bash复制jmap -histo:live <pid> | head -20 - arthas:动态诊断工具
bash复制watch com.example.Service method '{params,returnObj}' -x 3
8. 学习路线与资源推荐
8.1 渐进式学习路径
-
基础阶段(2周):
- 掌握线程生命周期
- 理解synchronized原理
- 学会使用基本JUC组件
-
进阶阶段(3周):
- 阅读AQS源码
- 分析ConcurrentHashMap实现
- 掌握各种锁的应用场景
-
高手阶段(持续):
- JMM内存模型深入
- 无锁编程实践
- 性能调优实战
8.2 推荐书单
- 《Java并发编程实战》(必读)
- 《并发编程的艺术》(原理深入)
- 《JUC源码剖析》(源码级理解)
额外建议:每天抽30分钟阅读JDK源码,从java.util.concurrent包开始,坚持三个月会有质的飞跃。我在学习AQS时,曾手写简化版来加深理解,这个方法非常有效。
