1. Java并发面试核心问题解析
作为Java开发者面试中的必考领域,并发编程问题几乎出现在90%的中高级岗位面试中。我经历过上百场技术面试,发现面试官最常考察的并发问题主要集中在以下几个维度:
- 基础概念:线程与进程区别、并发与并行差异
- 核心机制:synchronized实现原理、volatile语义
- 并发工具:AQS体系、并发容器使用场景
- 线程管理:线程池参数配置、拒绝策略选择
- 实战问题:死锁排查、高并发场景设计
1.1 synchronized的锁升级过程
当面试官问"说说synchronized实现原理"时,他们期待听到的是完整的锁升级过程:
- 无锁状态:新创建的对象尚未被任何线程访问
- 偏向锁(Mark Word中记录线程ID):适用于单线程重复访问场景,通过CAS设置Owner线程
- 轻量级锁(栈帧中创建Lock Record):当多线程交替执行时,通过自旋尝试获取锁
- 重量级锁(指向Monitor对象):当竞争激烈时,线程进入阻塞队列等待唤醒
关键点:锁升级是不可逆过程,且不同阶段性能差异显著。在基准测试中,偏向锁的获取速度比重量级锁快100倍以上。
1.2 volatile的内存语义
这个关键字常被误解为"轻量级锁",其实它的核心作用有两个:
- 可见性保证:写操作会立即刷新到主内存,读操作会从主内存读取最新值
- 禁止指令重排序:通过内存屏障(Memory Barrier)实现
典型应用场景包括:
- 状态标志位(如shutdown标志)
- 单例模式的双重检查锁定
- 线程间简单状态通信
// 典型错误示例:误用volatile实现原子操作 private volatile int count = 0; public void increment() { count++; // 这仍然不是原子操作! }2. 并发工具类深度剖析
2.1 AQS实现原理
AbstractQueuedSynchronizer是Java并发包的基石,其核心数据结构包括:
- state变量:表示资源状态的int值
- CLH队列:双向链表实现的等待队列
- ConditionObject:条件变量实现
以ReentrantLock为例,其获取锁的流程为:
- 尝试通过CAS修改state
- 失败后创建Node加入队列尾部
- 进入自旋检查前驱节点状态
- 被前驱节点唤醒后尝试获取锁
2.2 ConcurrentHashMap优化演进
| JDK版本 | 实现方式 | 并发度 | 关键改进 |
|---|---|---|---|
| 1.7 | 分段锁(Segment) | 默认16 | 减少锁竞争 |
| 1.8 | CAS+synchronized | 桶数量 | 链表转红黑树 |
| 17 | 优化扩容机制 | 动态调整 | 减少内存消耗 |
实际面试中常问的问题包括:
- size()方法的实现原理
- 为什么用synchronized替代ReentrantLock
- 扩容期间读操作如何处理
3. 线程池的实战配置
3.1 参数配置黄金法则
面对"如何配置线程池参数"的问题,建议从以下维度回答:
- 核心线程数:CPU密集型任务设为CPU核数+1,IO密集型可设为2*CPU核数
- 队列选择:
- SynchronousQueue:直接传递,适合短任务
- LinkedBlockingQueue:无界队列,可能OOM
- ArrayBlockingQueue:有界队列,需要合理设置大小
- 拒绝策略:
- AbortPolicy:默认策略,抛出异常
- CallerRunsPolicy:由调用线程执行
- DiscardOldestPolicy:丢弃最老任务
3.2 线上问题排查案例
某电商平台大促期间出现的线程池问题:
- 现象:接口响应变慢,最终超时
- 排查:
jstack <pid> | grep "pool" -A 30 # 查看线程状态 - 发现:200个线程全部阻塞在数据库查询
- 解决:改用带超时的连接池,设置合理的maxWait
4. 高频面试题精讲
4.1 死锁产生与排查
死锁的四个必要条件:
- 互斥条件
- 请求与保持
- 不可剥夺
- 循环等待
排查工具链:
jstack <pid> > thread_dump.txt # 获取线程快照 # 查找"deadlock"关键词或相互等待的线程预防方案:
- 使用tryLock设置超时
- 统一资源获取顺序
- 使用jconsole可视化监控
4.2 CAS的ABA问题
经典ABA问题场景:
- 线程1读取值A
- 线程2修改A→B→A
- 线程1比较发现仍是A,误认为未被修改过
解决方案:
- 使用AtomicStampedReference添加版本号
- 对于引用类型,可以利用地址不变的特性
5. 高并发场景设计
5.1 秒杀系统核心要点
三级缓冲架构:
- 前端层:
- 按钮置灰
- 随机放量
- 验证码过滤
- 中间层:
- 库存预扣减(Redis DECR)
- 消息队列削峰
- 数据层:
- 乐观锁更新
- 分库分表
5.2 分布式锁实现方案
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis SETNX | 性能高 | 锁续期复杂 | 短时任务 |
| Zookeeper | 可靠性高 | 性能较低 | 长时任务 |
| 数据库 | 实现简单 | 性能差 | 低频场景 |
RedLock算法要点:
- 获取当前时间
- 顺序向N个节点获取锁
- 计算获取锁耗时
- 当且仅当多数节点成功且耗时小于锁有效期时视为成功
6. JMM与happens-before
Java内存模型的核心规则:
| 关系 | 保证内容 |
|---|---|
| 程序顺序规则 | 线程内顺序执行 |
| 监视器锁规则 | unlock先于后续lock |
| volatile规则 | 写先于后续读 |
| 传递性 | A先于B,B先于C → A先于C |
常见误区纠正:
- synchronized不仅保证原子性,也保证可见性
- final字段的安全发布需要正确构造
- 指令重排序只影响无依赖关系的操作
7. 并发编程避坑指南
7.1 性能陷阱
锁粗化误区:
// 错误做法:过度合并同步块 synchronized(lock) { operation1(); operation2(); // 实际不需要同步的操作 }上下文切换成本:
- 测试表明:当线程数超过CPU核心数2倍时,吞吐量开始下降
- 解决方案:使用协程(如Quasar)或异步编程
7.2 调试技巧
线程转储分析:
jstack <pid> | grep -A 1 "BLOCKED"JFR监控:
jcmd <pid> JFR.start duration=60s filename=recording.jfr可视化工具:
- JConsole观察线程状态
- VisualVM分析锁竞争
8. 现代并发发展趋势
8.1 Project Loom展望
虚拟线程特性:
- 轻量级:百万级线程创建
- 兼容性:与现有Thread API兼容
- 调度器:由JVM管理
Thread.startVirtualThread(() -> { // 并发任务 });8.2 响应式编程实践
Spring WebFlux示例:
public Mono<User> getUser(String id) { return Mono.fromCallable(() -> repository.findById(id)) .subscribeOn(Schedulers.boundedElastic()); }性能对比:
- 传统Servlet:1请求=1线程
- WebFlux:常量数量工作线程
在真实项目中选择时,需要考虑团队技能栈和业务特点,不要盲目追求新技术。对于已有Spring MVC项目,可以逐步引入WebClient作为HTTP客户端来体验响应式编程的优势。