1. 为什么Java多线程面试总让人头疼?
每次面试前刷Java多线程题目时,是不是总有种"一看就会,一写就废"的感觉?我当年准备面试时,光是synchronized和volatile的区别就背了七八遍,结果被面试官一个实际场景题问得哑口无言。后来做了面试官才发现,90%的候选人都卡在以下几个典型问题上:
- 死记硬背概念但不会解决实际问题(比如让你设计个线程安全的计数器)
- 知道锁但说不清锁升级过程(从偏向锁到重量级锁的完整流程)
- 用过线程池但说不清核心参数的关系(corePoolSize和maxPoolSize如何配合工作)
- 听说过JUC包但连最基础的AQS原理都解释不清
提示:真正懂多线程的开发者,应该能三句话说清楚synchronized和ReentrantLock的本质区别(提示:从实现机制、功能特性和使用场景三个维度)
2. 多线程核心知识体系拆解
2.1 线程基础必须掌握的三个维度
内存模型层面:
- JMM的happens-before原则(写代码时最常违反的就是程序顺序规则)
- volatile的可见性实现原理(通过内存屏障禁止指令重排序)
- 指令重排序的典型case(DCL单例为什么要加volatile)
线程控制层面:
// 错误示范:直接继承Thread的局限性 class MyThread extends Thread { public void run() { // 业务代码 } } // 正确做法:实现Runnable接口 class MyTask implements Runnable { @Override public void run() { // 业务代码 } }状态转换层面:
- NEW -> RUNNABLE:start()方法调用时
- RUNNABLE <-> BLOCKED:synchronized锁竞争时
- RUNNABLE <-> WAITING:wait()/notify()调用时
- RUNNABLE <-> TIMED_WAITING:sleep(ms)调用时
2.2 锁机制的深度解析
synchronized的锁升级全过程:
- 无锁状态:新创建的对象
- 偏向锁:第一个线程访问时(通过CAS设置ThreadID)
- 轻量级锁:有竞争但未膨胀时(自旋尝试获取锁)
- 重量级锁:竞争激烈时(向OS申请mutex)
ReentrantLock的实战技巧:
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); void demo() throws InterruptedException { lock.lock(); // 建议放在try外部 try { while(条件不满足) { condition.await(); // 释放锁并等待 } // 业务处理 condition.signal(); } finally { lock.unlock(); // 必须放在finally } }3. JUC并发工具包实战指南
3.1 AQS的实现精髓
AbstractQueuedSynchronizer的核心设计:
- state变量:通过CAS操作控制同步状态
- CLH队列:用双向链表实现的等待队列
- 模板方法模式:tryAcquire/tryRelease由子类实现
CountDownLatch典型场景:
// 模拟并行任务处理 CountDownLatch latch = new CountDownLatch(3); ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 0; i < 3; i++) { executor.execute(() -> { try { // 模拟任务执行 Thread.sleep(1000); } finally { latch.countDown(); } }); } latch.await(); // 阻塞直到所有任务完成 System.out.println("所有任务执行完毕");3.2 线程池的七个核心参数
- corePoolSize:常驻核心线程数(不会被回收)
- maximumPoolSize:最大线程数(应急创建的临时线程)
- keepAliveTime:临时线程空闲存活时间
- unit:时间单位
- workQueue:任务队列(ArrayBlockingQueue/LinkedBlockingQueue)
- threadFactory:线程创建工厂
- handler:拒绝策略(AbortPolicy/CallerRunsPolicy等)
配置公式:
- CPU密集型:corePoolSize = CPU核数 + 1
- IO密集型:corePoolSize = CPU核数 * 2
4. 高频面试题深度剖析
4.1 CAS的ABA问题解决方案
问题复现步骤:
- 线程1读取变量值为A
- 线程2修改变量A→B→A
- 线程1再次比较时发现仍是A,误认为未被修改过
解决方案对比表:
| 方案 | 实现方式 | 优缺点 |
|---|---|---|
| 版本号 | AtomicStampedReference | 精确但性能开销大 |
| 布尔标记 | AtomicMarkableReference | 轻量但可能冲突 |
4.2 死锁的四个必要条件及破解
必要条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
预防方案:
- 顺序加锁法:统一获取锁的顺序
- 超时放弃:tryLock(timeout)
- 银行家算法:预先计算安全序列
5. 性能优化实战技巧
5.1 线程上下文切换的成本测量
// 测试代码示例 long start = System.nanoTime(); for (int i = 0; i < 1000000; i++) { Thread.yield(); // 主动让出CPU } long duration = System.nanoTime() - start; System.out.println("平均每次切换耗时:" + duration/1000000 + "ns");实测数据:
- 普通上下文切换:1-2微秒
- 跨核上下文切换:3-5微秒
5.2 锁粒度的优化策略
错误案例:
public synchronized void process() { // 方法级锁 // 读操作 // 写操作 }优化方案:
public void process() { // 无锁读操作 synchronized(this) { // 细化锁范围 // 写操作 } }6. 避坑指南与实战经验
ThreadLocal的内存泄漏:
- 必须配合try-finally清理:finally中调用remove()
- 使用static修饰:避免每次创建新实例
Future.get()的阻塞陷阱:
Future<?> future = executor.submit(task); try { future.get(500, TimeUnit.MILLISECONDS); // 必须设置超时 } catch (TimeoutException e) { future.cancel(true); // 中断任务 }ConcurrentHashMap的size()误区:
- 不要用size()做精确控制(因为它是估算值)
- 业务判断应该用mappingCount()方法
我在美团带团队时,曾遇到一个典型case:某核心服务使用FixedThreadPool处理请求,在流量突增时大量请求堆积导致OOM。后来改用自定义的RejectedExecutionHandler,在拒绝时自动降级返回兜底数据,配合动态线程池参数调整,完美解决了问题。这告诉我们:多线程问题往往不是技术难点,而是设计思维和工程经验的体现。