⚡ 为什么并发编程是Java面试的“必考题”?
在Java后端开发岗位(尤其是中高级/架构师方向)的面试中,并发编程几乎100%会被深度考察。原因有三:
- 业务复杂度提升:高并发系统(如秒杀、订单、支付)要求开发者必须掌握线程调度、资源竞争与性能调优;
- 技术栈底层依赖并发模型:Spring、Netty、Dubbo、Redis等主流框架大量使用并发工具类与线程池;
- 性能瓶颈常出现在并发层:数据库慢查询只是表象,真正瓶颈常是线程阻塞、锁竞争或内存溢出。
本页面基于2024年主流互联网企业(阿里、腾讯、字节、美团、拼多多)最新面试题库整理,覆盖:Thread/Runnable实现差异、synchronized底层原理、ReentrantLock与AQS机制、线程池参数调优、JMM三大特性等10+大类核心问题,每题均配真实代码示例、常见误区与优化建议。
⚙️ 一、并发编程核心概念精解(附对比表格)
〈1〉线程 vs 进程:面试高频区分点
面试常问:“为什么Java中创建线程比创建进程更轻量?”
- 资源开销:进程拥有独立内存空间(代码段、数据段、堆、栈),线程共享进程资源,仅私有栈与寄存器;
- 切换成本:进程切换需保存/恢复页表、MMU状态;线程切换仅保存寄存器与栈指针;
- 通信方式:进程间通信需IPC(管道、消息队列、共享内存);线程间可直接读写共享变量。
// 创建线程的三种方式对比
public class ThreadCreationDemo {
// 方式1:继承Thread类(不推荐,单继承限制)
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread: " + Thread.currentThread().getName());
}
}
// 方式2:实现Runnable接口(推荐,解耦任务与线程)
static class MyTask implements Runnable {
@Override
public void run() {
System.out.println("MyTask: " + Thread.currentThread().getName());
}
}
// 方式3:Lambda + Thread(现代写法)
public static void main(String[] args) {
new MyThread().start();
new Thread(new MyTask()).start();
new Thread(() -> System.out.println("Lambda: " + Thread.currentThread().getName())).start();
}
}
避坑指南:避免继承Thread,因Java单继承限制;Runnable更灵活,可配合线程池使用。
〔2〕并发 vs 并行:概念混淆重灾区
面试官最爱陷阱题:“多核CPU下,多线程一定比单线程快吗?”
| 维度 | 并发(Concurrency) | 并行(Parallelism) |
|---|---|---|
| 定义 | 宏观上多个任务交替执行(单核时代) | 微观上多个任务同时执行(多核时代) |
| 核心目标 | 提高资源利用率(如I/O等待时切换线程) | 提升吞吐量(真正同时计算) |
| 是否需要多核 | 否(单核即可) | 是 |
| Java实现 | Thread.yield()、线程调度 | ForkJoinPool、并行流(parallelStream) |
典型案例:Web服务器接收请求时,若每个请求耗时10ms(含9ms I/O),单线程每秒处理100请求;多线程(假设无锁竞争)可提升至每秒1000+,因I/O等待期间切换线程——这是并发;若每个请求需10ms纯CPU计算,多核并行才有效——这是并行。
⚡ 二、线程安全:从现象到本质
〈1〉原子性问题:为什么i++不是原子操作?
经典面试题:“以下代码是否线程安全?为什么?”
public class Counter {
private int count = 0;
public void increment() {
count++; // 问题!
}
public int getCount() {
return count;
}
}
原理剖析:i++在JVM字节码中分解为:iload_0(读取)→ iinc 0,1(自增)→ istore_0(写回)。三步非原子,多线程下可能丢失更新。
修复方案:
// 方案1:synchronized块(推荐入门)
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
优势:简单直观;代价:阻塞所有其他线程,高并发下性能差。
// 方案2:AtomicInteger(高并发首选)
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // CAS无锁实现
}
public int getCount() {
return count.get();
}
}
原理:基于CAS(Compare-And-Swap)硬件指令,无锁竞争;注意:频繁自旋会导致CPU飙升。
// 方案3:ReentrantLock(需手动释放)
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须在finally释放!
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
优势:支持公平锁、可中断、超时获取;代价:代码复杂,易出错。
〔2〕可见性问题:volatile的真相
面试陷阱:“volatile能保证原子性吗?”
答案:不能! volatile仅保证可见性与禁止指令重排序,不保证原子性。
// 错误示例:volatile无法解决i++的原子性
public class VolatileCounter {
private volatile int count = 0;
public void increment() {
count++; // 仍可能丢失更新!
}
}
正确用法:
- 状态标记:如“是否停止线程”
- 一次性安全发布:如单例双重检查锁定中的instance字段
// 正确示例:volatile用于状态标记
public class Worker implements Runnable {
private volatile boolean running = true;
@Override
public void run() {
while (running) {
// 执行任务...
}
}
public void stop() {
running = false; // 确保其他线程可见
}
}
〔3〕有序性问题:指令重排序的隐形陷阱
经典案例:双重检查锁定单例的缺陷
// 错误写法:可能返回未完全初始化的对象
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题:指令重排序!
}
}
}
return instance;
}
}
问题根源:new Singleton()分解为:
① 分配内存
② 调用构造函数
③ 将引用指向内存地址
重排序后可能为:①→③→②,导致其他线程拿到未初始化对象。
修复方案:对instance加volatile禁止重排序
public class Singleton {
private static volatile Singleton instance; // 关键:volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
🔒 三、锁机制深度解析:synchronized vs ReentrantLock
〈1〉synchronized底层原理
面试高频:“synchronized锁升级过程?”
对象头Mark Word中存储对象自身的哈希码、GC年龄等信息。
首次竞争时,JVM将线程ID写入Mark Word,后续同一对象的锁由该线程“持有”,无需CAS操作。适用于单线程重复锁场景。
多线程交替进入同步块,通过自旋尝试获取锁(避免线程挂起),适用于锁竞争不激烈场景。
自旋失败后,升级为重量级锁,线程挂起等待,由操作系统调度。适用于高竞争场景。
注意:JDK6后默认开启偏向锁,可通过-XX:-UseBiasedLocking关闭。
〔2〕ReentrantLock核心机制
AQS(AbstractQueuedSynchronizer)原理:
- 维护一个FIFO双向同步队列(CLH队列),Node节点存储线程引用;
- 通过state变量表示锁状态(0=未锁,>0=重入次数);
- 非公平锁:尝试直接获取state,失败则入队;公平锁:入队前先检查队列是否为空。
// ReentrantLock非公平锁tryAcquire源码简化版
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 无锁状态
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 重入
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
避免了线程挂起/唤醒的开销,新线程可能“插队”获取刚释放的锁,减少上下文切换。
⚡ 四、线程池:企业级并发控制核心
〈1〉线程池参数详解(ThreadPoolExecutor)
| 参数 | 作用 | 面试陷阱 |
|---|---|---|
| corePoolSize | 核心线程数(常驻线程) | 设为CPU核数+1?仅适用于CPU密集型! |
| maximumPoolSize | 最大线程数 | 设为Integer.MAX_VALUE?可能导致OOM! |
| keepAliveTime | 非核心线程存活时间 | 需配合unit使用,否则单位错误引发崩溃 |
| workQueue | 任务队列 | LinkedBlockingQueue无界?可能内存溢出! |
| threadFactory | 线程工厂 | 不自定义?线程名无意义,排查困难 |
| handler | 拒绝策略 | 默认AbortPolicy?可能导致任务丢失! |
企业级推荐配置:
// CPU密集型:core=CPU核数,max=CPU核数+1
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService cpuPool = new ThreadPoolExecutor(
cpuCores, cpuCores + 1,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("cpu-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// I/O密集型:core=2*CPU核数,max=可调参数
ExecutorService ioPool = new ThreadPoolExecutor(
cpuCores * 2, cpuCores * 3,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10000),
new ThreadFactoryBuilder().setNameFormat("io-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
拒绝策略选择:
- CallerRunsPolicy:调用者线程执行任务(最安全,避免丢失)
- DiscardOldestPolicy:丢弃队列头任务(适合日志等可丢弃场景)
〔2〕线程池常见坑点
- 线程池复用问题:多个业务共用一个线程池,一个任务阻塞导致其他任务延迟;
→ 解决方案:按业务拆分线程池(如支付池、订单池)。 - 未捕获异常:线程池中任务抛出未捕获异常,线程退出但不重启;
→ 解决方案:重写afterExecute方法记录日志。 - submit vs execute:submit返回Future,可获取异常;execute无返回,异常直接抛出;
→ 建议:关键任务用submit + Future.get()。
// 安全的异常处理示例
ExecutorService pool = Executors.newFixedThreadPool(10);
Future> future = pool.submit(() -> {
// 业务逻辑...
throw new RuntimeException("模拟异常");
});
try {
future.get(); // 捕获异常
} catch (ExecutionException e) {
System.err.println("任务执行失败:" + e.getCause().getMessage());
}
📦 五、并发容器:替代Vector/Hashtable的正确姿势
〈1〉ConcurrentHashMap原理
JDK7 vs JDK8重大差异:
| 特性 | JDK7 | JDK8 |
|---|---|---|
| 数据结构 | Segment分段 + HashEntry数组 | Node数组 + 链表/红黑树 |
| 锁粒度 | Segment级锁(16把锁) | Node级锁(更细粒度) |
| 扩容 | 单线程扩容 | 多线程协助扩容(迁移任务分片) |
| size() | 遍历Segment累加(O(n)) | CounterCell计数器(O(1)) |
〔2〕CopyOnWriteArrayList适用场景
核心原理:写时复制(写操作加锁,复制新数组;读操作无锁)
适用场景:
- 读多写少(如配置列表、监听器注册表)
- 允许读到旧数据(最终一致性)
不适用场景:
- 写操作频繁(每次复制开销大)
- 内存敏感(数组副本占用双倍内存)
// 典型使用:注册监听器
private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>();
public void addListener(Runnable listener) {
listeners.add(listener);
}
public void notifyListeners() {
for (Runnable listener : listeners) {
listener.run(); // 读操作无锁,安全
}
}
🧠 六、JMM内存模型:volatile与happens-before规则
〈1〉三大特性详解
| 特性 | 含义 | 保证方式 |
|---|---|---|
| 原子性 | 操作不可中断(如long/double读写在32位JVM非原子) | synchronized、Lock、Atomic类 |
| 可见性 | 修改对其他线程立即可见 | volatile、synchronized、final |
| 有序性 | 禁止指令重排序 | volatile(插入内存屏障)、synchronized(获取/释放锁) |
happens-before规则(面试必考):
- 程序顺序规则:线程内代码按顺序执行
- 监视器锁规则:unlock() happens-before 后续的lock()
- volatile变量规则:写happens-before后续读
- 线程启动规则:start() happens-before 子线程run()
- 线程终止规则:线程结束happens-before join()返回
- 中断规则:interrupt() happens-before 检测中断
- 对象终结规则:构造完成happens-before finalize()开始
// 示例:volatile保证有序性(双重检查单例)
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 重排序被禁止!
}
}
}
return instance;
}
}
❓ 七、高频问题QA:10+个真实面试题深度解析
A:悲观锁假设冲突必然发生(如synchronized),全程加锁;乐观锁假设冲突 rare(如CAS),仅在冲突时重试。volatile不加锁,仅保证可见性,故称“轻量级同步”。
A:
- CountDownLatch:一次性的“倒计时门栓”,线程等待其他线程完成(如主线程等待子线程初始化);
- CyclicBarrier:可重用的“同步屏障”,多线程互相等待到某点再一起继续(如多线程计算后汇总)。
A:
- 原理:每个Thread维护ThreadLocalMap,key为ThreadLocal对象,value为存储值;
- 泄漏风险:ThreadLocalMap的key是弱引用,GC时被清除,但value未清理导致内存泄漏;
- 解决方案:每次使用后调用remove()显式清理。
A:重写ThreadPoolExecutor的afterExecute方法,记录异常并补充执行:[code]protected void afterExecute(Runnable r, Throwable t) { if (t != null) { // 记录日志并重新提交任务 } }[/code]
📌 总结:并发编程学习路线图
1. 基础夯实:掌握线程创建、线程状态、synchronized原理;
2. 进阶突破:深入AQS、JMM、volatile内存屏障;
3. 实战应用:线程池调优、并发容器选型、死锁分析;
4. 高阶思维:性能瓶颈定位、高并发架构设计(如Redis+Lua防超卖)。
jstack看线程栈、用VisualVM看锁竞争、写测试代码验证原理,才能真正掌握并发编程。
本文覆盖Java面试并发问题解析全部核心考点,结合企业真实案例,助你从“能用”进阶到“懂原理、能调优、敢设计”。更多技术干货,请关注www.kaoacca.cn。