1. 并发编程面试的核心考察点
在Java技术岗位的面试中,并发编程能力是区分初中级和高级工程师的重要分水岭。根据我多年参与技术面试的经验,面试官通常会从以下几个维度考察候选人的并发编程能力:
基础概念理解:线程与进程的本质区别、并发与并行的实际场景差异、Java内存模型(JMM)的核心约束。这部分看似简单,但能准确说出happens-before原则具体包含哪些场景的候选人不足30%。
工具类掌握程度:从传统的synchronized到JUC包下的各种工具,面试官会关注你是否真正理解它们的实现原理而非仅停留在API调用层面。比如ReentrantLock的AQS实现机制,很多候选人只能说出"可重入"这个基本特性。
问题诊断能力:当给出一个死锁案例时,能否快速定位到死锁的四个必要条件;面对CPU飙高场景,能否通过线程堆栈分析出锁竞争热点。这类问题直接反映实际项目经验。
设计思维考察:如何根据业务特点设计线程池参数?分布式环境下如何实现跨JVM的同步控制?这类开放性问题最能体现工程思维深度。
2. 线程生命周期与状态转换详解
2.1 标准线程状态机
Java线程的6种标准状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)是面试必问基础题。但容易忽略的是:
- RUNNABLE状态实际包含操作系统层面的就绪(Ready)和运行中(Running)两种子状态
- BLOCKED状态特指等待进入synchronized代码块的场景,与JUC锁的等待状态不同
- 调用Object.wait()会进入WAITING,而Thread.sleep()则进入TIMED_WAITING
关键陷阱题:线程执行yield()方法后状态如何变化?正确答案是保持RUNNABLE状态,因为yield只是让出CPU时间片,不改变线程基础状态。
2.2 状态转换的触发条件
通过一个实际案例演示状态转换:
java复制public class ThreadStateDemo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
Thread t = new Thread(() -> {
synchronized (lock) { // 进入BLOCKED状态(如果锁被占)
try {
lock.wait(1000); // 进入TIMED_WAITING
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
System.out.println(t.getState()); // NEW
t.start();
Thread.sleep(50);
System.out.println(t.getState()); // RUNNABLE或BLOCKED
synchronized (lock) {
Thread.sleep(100);
System.out.println(t.getState()); // TIMED_WAITING
}
t.join();
System.out.println(t.getState()); // TERMINATED
}
}
3. synchronized的底层实现原理
3.1 对象头与Monitor机制
每个Java对象头中都包含Mark Word,其中存储了锁状态信息:
- 无锁状态:存储对象的hashCode和分代年龄
- 偏向锁:存储持有偏向锁的线程ID
- 轻量级锁:指向栈中锁记录的指针
- 重量级锁:指向Monitor对象的指针
在HotSpot虚拟机中,Monitor由ObjectMonitor实现,关键字段包括:
- _owner:指向持有锁的线程
- _EntryList:阻塞等待锁的线程队列
- _WaitSet:调用wait()后进入的等待集合
3.2 锁升级的全过程
-
偏向锁启用阶段:当线程第一次访问同步块时,通过CAS将线程ID写入Mark Word。此时并没有真正的同步开销。
-
偏向锁撤销:当其他线程尝试获取锁时,JVM需要撤销偏向锁。这个Stop-The-World操作正是早期JDK中偏向锁性能问题的根源。
-
轻量级锁竞争:线程通过CAS操作将Mark Word替换为锁记录指针。成功则获取锁,失败则自旋重试。
-
重量级锁膨胀:当自旋超过阈值(默认10次)或等待线程数超过CPU核数的一半,锁会膨胀为重量级锁,线程进入_EntryList等待。
实测数据:在JDK15+的系统中,默认已禁用偏向锁延迟(BiasedLockingStartupDelay=0),这是因为现代多核处理器中CAS操作成本已大幅降低。
4. ReentrantLock的AQS实现剖析
4.1 同步队列工作原理
AbstractQueuedSynchronizer(AQS)通过CLH队列管理线程排队:
java复制// 简化版AQS节点结构
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter; // 用于条件队列
}
当线程获取锁失败时:
- 创建Node节点并入队
- 通过自旋+CAS维护队列结构
- 最终通过LockSupport.park()挂起线程
4.2 公平锁与非公平锁差异
非公平锁的抢占逻辑:
java复制final void lock() {
if (compareAndSetState(0, 1)) // 直接尝试抢锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
公平锁的严格排队:
java复制final void lock() {
acquire(1); // 直接进入队列排队
}
性能对比测试(4线程循环100万次):
| 锁类型 | 耗时(ms) | 上下文切换次数 |
|---|---|---|
| 公平锁 | 1420 | 15,642 |
| 非公平锁 | 980 | 8,327 |
5. 线程池的深度配置策略
5.1 核心参数动态计算
通用线程数公式:
code复制N_threads = N_cpu * U_cpu * (1 + W/C)
其中:
- N_cpu:Runtime.getRuntime().availableProcessors()
- U_cpu:目标CPU利用率(通常0.8-0.9)
- W/C:等待时间与计算时间的比值
IO密集型任务示例:
java复制int poolSize = (int) (Runtime.getRuntime().availableProcessors() * 0.9 * (1 + 50/10));
5.2 队列选型对比
| 队列类型 | 特性 | 适用场景 |
|---|---|---|
| SynchronousQueue | 零容量,直接移交 | 高吞吐短期任务 |
| LinkedBlockingQueue | 无界队列 | 保证任务不丢失 |
| ArrayBlockingQueue | 有界队列 | 防止资源耗尽 |
| DelayedWorkQueue | 延迟执行 | 定时任务调度 |
5.3 监控与调优实践
通过扩展ThreadPoolExecutor实现监控:
java复制class MonitorThreadPool extends ThreadPoolExecutor {
protected void beforeExecute(Thread t, Runnable r) {
log.info("Task start: {}", t.getId());
}
protected void afterExecute(Runnable r, Throwable t) {
log.info("Active: {}, Completed: {}, Queue: {}",
getActiveCount(), getCompletedTaskCount(), getQueue().size());
}
}
关键监控指标:
- 活跃线程数 vs 核心线程数
- 队列堆积增长率
- 任务平均耗时
- 拒绝策略触发频率
6. 死锁诊断与预防方案
6.1 死锁四要素检测
通过jstack检测死锁的典型输出:
code复制Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f88b400a3b8 (object 0x000000076bf28d80)
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f88b400a3b8 (object 0x000000076bf28d80)
which is held by "Thread-1"
6.2 锁排序预防实践
全局锁获取顺序定义:
java复制class LockOrder {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public void methodA() {
synchronized(lock1) {
synchronized(lock2) {
// 操作共享资源
}
}
}
public void methodB() {
synchronized(lock1) { // 保持相同顺序
synchronized(lock2) {
// 其他操作
}
}
}
}
6.3 超时锁实战
使用tryLock避免无限等待:
java复制if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 临界区操作
} finally {
lock.unlock();
}
} else {
log.warn("获取锁超时,执行降级逻辑");
// 备用方案
}
7. 并发集合类的选型指南
7.1 ConcurrentHashMap分段演进
JDK7与JDK8实现对比:
| 版本 | 数据结构 | 锁粒度 | 并发度控制 |
|---|---|---|---|
| JDK7 | Segment数组 | 分段锁 | 构造函数指定 |
| JDK8 | Node数组+红黑树 | CAS+synchronized | 自动扩容 |
7.2 CopyOnWrite适用场景
写时复制容器的典型使用模式:
java复制List<String> eventLog = new CopyOnWriteArrayList<>();
// 低频写操作
void addLog(String event) {
eventLog.add(event); // 复制整个数组
}
// 高频读操作
void processLogs() {
for (String log : eventLog) { // 遍历快照
// 处理日志
}
}
7.3 阻塞队列对比
常见阻塞队列特性矩阵:
| 队列实现类 | 是否有界 | 公平性 | 特殊功能 |
|---|---|---|---|
| ArrayBlockingQueue | 是 | 支持 | 固定容量 |
| LinkedBlockingQueue | 可选 | 不支持 | 默认无界 |
| PriorityBlockingQueue | 无界 | 不支持 | 优先级排序 |
| SynchronousQueue | 零容量 | 支持 | 直接传递 |
| DelayQueue | 无界 | 不支持 | 延迟元素出队 |
8. 并发编程实战经验总结
8.1 上下文切换成本实测
测试方案:创建N个线程,每个线程执行1,000,000次空循环
| 线程数 | 总耗时(ms) | 单线程平均耗时 |
|---|---|---|
| 1 | 42 | 42 |
| 2 | 78 | 39 |
| 4 | 215 | 53.75 |
| 8 | 842 | 105.25 |
| 16 | 3,127 | 195.44 |
结论:当线程数超过CPU核心数后,上下文切换成本呈指数级增长。
8.2 锁粒度优化案例
优化前的粗粒度锁:
java复制class OrderService {
private final Object lock = new Object();
public void process(Order order) {
synchronized(lock) {
// 验证库存
// 计算运费
// 生成发票
// 更新物流
}
}
}
优化后的分段锁:
java复制class OptimizedOrderService {
private final Striped<Lock> locks = Striped.lock(32);
public void process(Order order) {
Lock lock = locks.get(order.getUserId());
lock.lock();
try {
// 只锁用户相关操作
} finally {
lock.unlock();
}
}
}
8.3 ThreadLocal的内存泄漏防护
正确使用模式:
java复制public class SessionHolder {
private static final ThreadLocal<Session> holder = new ThreadLocal<>();
public static void set(Session session) {
holder.set(session);
}
public static Session get() {
return holder.get();
}
public static void remove() { // 必须显式清理
holder.remove();
}
}
// 在过滤器或拦截器中
try {
SessionHolder.set(request.getSession());
chain.doFilter(request, response);
} finally {
SessionHolder.remove(); // 确保清除
}
