并发面试题
面试问到并发,逃不开这些问题。
本章节帮你梳理高频面试题,理解背后的原理,而不仅仅是背答案。
线程基础
Q1: 进程和线程的区别?
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 独立的内存空间 | 共享进程资源 |
| 开销 | 大(进程创建、切换) | 小(线程创建、切换) |
| 通信 | IPC(管道、消息队列等) | 直接读写共享内存 |
| 独立性强 | 进程间隔离 | 线程间共享 |
简单说:进程是资源分配的单位,线程是 CPU 调度的单位。
Q2: 创建线程有几种方式?
java
// 方式1:继承 Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread 方式");
}
}
new MyThread().start();
// 方式2:实现 Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable 方式");
}
}
new Thread(new MyRunnable()).start();
// 方式3:实现 Callable + FutureTask
class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
return 42;
}
}
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
new Thread(task).start();
System.out.println(task.get());
// 方式4:线程池
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("线程池方式"));本质上只有一种:创建 Thread 对象。Runnable 和 Callable 只是任务载体。
Q3: sleep() 和 wait() 的区别?
| 对比项 | sleep() | wait() |
|---|---|---|
| 所属 | Thread 类 | Object 类 |
| 锁行为 | 不释放锁 | 释放锁 |
| 唤醒方式 | 超时/中断 | notify() / notifyAll() |
| 使用场景 | 暂停执行 | 线程通信 |
| 调用位置 | 任意位置 | 必须在 synchronized 中 |
java
// sleep 不释放锁
synchronized (lock) {
Thread.sleep(1000); // 锁还在手里
}
// wait 释放锁
synchronized (lock) {
while (conditionNotMet) {
lock.wait(); // 锁放开
}
}synchronized 和 Lock
Q4: synchronized 和 Lock 的区别?
| 对比项 | synchronized | Lock |
|---|---|---|
| 获取方式 | 自动获取/释放 | 手动获取/释放 |
| 灵活性 | 低 | 高(tryLock、读写锁) |
| 超时等待 | 不支持 | 支持 |
| 中断等待 | 不支持 | 支持 |
| 公平锁 | 非公平 | 可配置 |
| 条件变量 | 内置(this) | 多个 Condition |
java
// synchronized:自动释放
synchronized (lock) {
// 操作
} // 自动释放
// Lock:手动释放
lock.lock();
try {
// 操作
} finally {
lock.unlock(); // 必须手动释放
}
// Lock 高级特性
ReentrantLock lock = new ReentrantLock();
// tryLock:带超时
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 操作
} finally {
lock.unlock();
}
}
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
// 读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // 读锁,多线程可并发
rwLock.writeLock().lock(); // 写锁,排他Q5: synchronized 底层原理?
java
synchronized (obj) {
// 代码
}字节码层面:
monitorenter // 获取锁
// 代码
monitorexit // 释放锁JVM 层面:
- 无锁:对象头标志位为无锁状态
- 偏向锁:第一次获取时记录线程 ID,后续无竞争无需同步
- 轻量级锁:自旋CAS获取
- 重量级锁:阻塞等待
volatile
Q6: volatile 的作用?
两个作用,缺一不可:
- 保证可见性:写操作立即刷新到主内存,读操作立即从主内存读取
- 防止指令重排序:通过内存屏障阻止编译器和 CPU 重排序
不保证原子性:i++ 这种复合操作,volatile 保护不了。
java
// ❌ volatile 不能保证原子性
private volatile int counter = 0;
counter++; // 三个步骤,volatile 只保证读写
// ✅ 原子类才能保证
private final AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();线程池
Q7: 线程池参数?
java
ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)Q8: 线程池工作流程?
任务提交
│
├─► 核心线程未满 ──► 创建核心线程执行
│
├─► 核心线程已满 ──► 任务入队
│
├─► 队列已满 ──► 创建临时线程执行
│
└─► 线程数达上限 ──► 执行拒绝策略Q9: Executors 创建的线程池有什么问题?
java
// ❌ FixedThreadPool:队列无界,可能 OOM
Executors.newFixedThreadPool(10);
// 内部:new LinkedBlockingQueue<>() 无界
// ❌ CachedThreadPool:线程数无界,可能 OOM
Executors.newCachedThreadPool();
// 内部:maximumPoolSize = Integer.MAX_VALUE
// ❌ SingleThreadExecutor:队列无界,可能 OOM
Executors.newSingleThreadExecutor();
// ✅ 正确做法:显式指定队列大小
new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 有界队列
new ThreadPoolExecutor.AbortPolicy()
);CAS 和原子类
Q10: CAS 原理?
Compare-And-Swap:
┌─────────────────────────────────┐
│ 1. 读取内存值 V │
│ 2. 比较 V 是否等于期望值 A │
│ 3. 相等则写入新值 B,返回 true │
│ 4. 不等则什么都不做,返回 false │
└─────────────────────────────────┘
ABA 问题:值从 A 变成 B 又变回 A,CAS 会认为没变
解决:AtomicStampedReference(带版本号)Q11: ConcurrentHashMap 1.7 和 1.8 的区别?
| 版本 | 实现 | 并发控制 |
|---|---|---|
| JDK 7 | Segment + 数组 + 链表 | 分段锁(16 个 Segment) |
| JDK 8 | CAS + synchronized + 红黑树 | 锁细化(只锁单个桶) |
JDK 8 去掉了 Segment,性能大幅提升。
线程安全集合
Q12: HashMap 线程安全吗?
不安全。并发问题:
- 数据覆盖
- JDK 7 扩容死循环(JDK 8 已修复)
安全替代方案:
java
// 方案1:ConcurrentHashMap
ConcurrentHashMap<String, Integer> map1 = new ConcurrentHashMap<>();
// 方案2:Collections.synchronizedMap
Map<String, Integer> map2 = Collections.synchronizedMap(new HashMap<>());
// 方案3:Hashtable(不推荐,性能差)
Hashtable<String, Integer> map3 = new Hashtable<>();Q13: CopyOnWriteArrayList 和 synchronizedList 区别?
| 特性 | CopyOnWriteArrayList | synchronizedList |
|---|---|---|
| 读操作 | 无锁,最快 | 有锁 |
| 写操作 | 复制整个数组 | 有锁 |
| 迭代器 | 弱一致,不抛异常 | 快速失败 |
| 适用 | 读多写少 | 写多读少 |
面试回答技巧
技巧一:原理优先
不要只背结论,要理解为什么。
问:synchronized 和 ReentrantLock 哪个快?
答:在低竞争场景下两者性能接近,
但 synchronized 有偏向锁优化,第一次获取几乎无开销。
在高竞争场景下,ReentrantLock 的 tryLock 特性更有优势。技巧二:场景驱动
不要孤立回答,结合使用场景。
问:什么时候用 volatile?
答:当只有一个线程写、多个线程读的状态标志时用 volatile。
比如 shutdown flag、DCL 单例模式中的实例引用。
如果需要复合操作,就得用原子类或锁。技巧三:深度延伸
回答完基本问题,可以主动延伸。
问:线程池的核心参数?
答:7 个参数。重点是 corePoolSize 和 maximumPoolSize 的关系,
以及 workQueue 的选择。
实际上,我更推荐显式创建 ThreadPoolExecutor,
而不是用 Executors 工具类,因为后者容易导致 OOM。必背口诀
线程安全:
单一变量 → Atomic*
复合操作 → synchronized / Lock
线程隔离 → ThreadLocal
高并发 Map → ConcurrentHashMap
读多写少 → CopyOnWrite
JMM 三要素:
原子性 → synchronized / Lock / Atomic*
可见性 → volatile / synchronized
有序性 → volatile / synchronized / happens-before
线程池:
CPU 密集 → 核心数
I/O 密集 → 核心数 × (1 + 等待/计算)
队列必须有界,防止 OOM要点回顾
- 线程基础:进程 vs 线程,sleep vs wait
- 同步机制:synchronized vs Lock,各有适用场景
- volatile:保证可见性和有序性,不保证原子性
- 线程池:核心参数 + 工作流程 + 避免 Executors
- 并发集合:根据读写比选择合适的集合
- CAS:无锁算法,了解 ABA 问题和解决方案
