并发编程面试题
2026/1/15大约 6 分钟
并发编程面试题
基础概念
1. 进程和线程的区别?
- 进程:操作系统资源分配的基本单位,拥有独立的内存空间
- 线程:CPU 调度的基本单位,共享进程的内存空间
- 区别:
- 进程间相互独立,线程间共享资源
- 进程切换开销大,线程切换开销小
- 进程通信需要 IPC,线程可直接读写共享变量
2. 并发和并行的区别?
- 并发:多个任务交替执行,宏观上同时进行(单核 CPU)
- 并行:多个任务真正同时执行(多核 CPU)
3. 创建线程有几种方式?
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口(有返回值)
- 使用线程池
4. Runnable 和 Callable 的区别?
| 特性 | Runnable | Callable |
|---|---|---|
| 返回值 | 无 | 有 |
| 异常 | 不能抛出检查异常 | 可以抛出检查异常 |
| 方法 | run() | call() |
5. 线程有哪些状态?
- NEW:新建
- RUNNABLE:可运行
- BLOCKED:阻塞(等待锁)
- WAITING:等待(wait/join)
- TIMED_WAITING:超时等待(sleep/wait(timeout))
- TERMINATED:终止
synchronized 相关
6. synchronized 的作用?
- 保证原子性:同一时刻只有一个线程执行同步代码
- 保证可见性:解锁前将变量刷新到主内存
- 保证有序性:禁止指令重排
7. synchronized 的使用方式?
// 1. 同步实例方法,锁是 this
public synchronized void method() { }
// 2. 同步静态方法,锁是 Class 对象
public static synchronized void staticMethod() { }
// 3. 同步代码块,锁是指定对象
synchronized (lock) { }8. synchronized 的锁升级过程?
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 偏向锁:只有一个线程访问,记录线程 ID
- 轻量级锁:多个线程交替访问,CAS 自旋
- 重量级锁:多个线程竞争,阻塞等待
9. synchronized 和 Lock 的区别?
| 特性 | synchronized | Lock |
|---|---|---|
| 获取/释放锁 | 自动 | 手动 |
| 可中断 | 不可 | lockInterruptibly() |
| 超时获取 | 不可 | tryLock(timeout) |
| 公平锁 | 不可 | 可配置 |
| 条件变量 | 单个 | 多个 Condition |
volatile 相关
10. volatile 的作用?
- 可见性:修改后立即刷新到主内存,其他线程立即可见
- 有序性:禁止指令重排
注意
volatile 不保证原子性!
11. volatile 和 synchronized 的区别?
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 阻塞 | 不会 | 会 |
12. 双重检查锁定为什么要用 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;
}
}instance = new Singleton() 分三步:
- 分配内存
- 初始化对象
- 将引用指向内存
没有 volatile,可能发生指令重排(1→3→2),导致其他线程获取到未初始化的对象。
线程池相关
13. 线程池的核心参数?
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- workQueue:工作队列
- threadFactory:线程工厂
- handler:拒绝策略
14. 线程池的工作流程?
- 提交任务
- 核心线程数未满 → 创建核心线程执行
- 核心线程数已满 → 加入工作队列
- 工作队列已满 → 创建非核心线程执行
- 线程数达到最大 → 执行拒绝策略
15. 线程池有哪些拒绝策略?
- AbortPolicy:抛出异常(默认)
- CallerRunsPolicy:由调用线程执行
- DiscardPolicy:直接丢弃
- DiscardOldestPolicy:丢弃最老的任务
16. 为什么不建议使用 Executors 创建线程池?
- newFixedThreadPool/newSingleThreadExecutor:使用无界队列,可能导致 OOM
- newCachedThreadPool:最大线程数为 Integer.MAX_VALUE,可能创建大量线程
17. 如何合理配置线程池大小?
- CPU 密集型:线程数 = CPU 核心数 + 1
- IO 密集型:线程数 = CPU 核心数 * 2
并发工具类
18. CountDownLatch 和 CyclicBarrier 的区别?
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 计数方式 | 递减 | 递减后重置 |
| 可重用 | 不可 | 可以 |
| 等待方 | 一个线程等待多个线程 | 多个线程互相等待 |
19. Semaphore 的作用?
控制同时访问特定资源的线程数量,常用于限流和资源池。
Semaphore semaphore = new Semaphore(3); // 最多 3 个线程同时访问
semaphore.acquire(); // 获取许可
semaphore.release(); // 释放许可20. ConcurrentHashMap 的实现原理?
JDK 7:分段锁(Segment),每个 Segment 是一个 ReentrantLock
JDK 8:
- 数组 + 链表 + 红黑树
- CAS + synchronized
- 锁粒度更细,只锁单个桶
原子类
21. CAS 是什么?有什么问题?
CAS(Compare And Swap):比较并交换,包含三个操作数:
- 内存位置 V
- 预期值 A
- 新值 B
如果 V == A,则 V = B,否则不做操作。
问题:
- ABA 问题:值从 A 变成 B 又变回 A,CAS 检测不到变化
- 自旋开销:长时间自旋消耗 CPU
- 只能保证单个变量的原子性
22. 如何解决 ABA 问题?
使用 AtomicStampedReference,增加版本号:
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);
int stamp = ref.getStamp();
ref.compareAndSet(1, 2, stamp, stamp + 1);23. LongAdder 和 AtomicLong 的区别?
- AtomicLong:所有线程竞争同一个变量
- LongAdder:分散热点,多个 Cell 分担竞争,最后汇总
高并发写入场景下,LongAdder 性能更好。
死锁
24. 什么是死锁?产生条件?
多个线程互相等待对方持有的资源,导致所有线程都无法继续执行。
四个必要条件:
- 互斥条件:资源只能被一个线程持有
- 请求与保持:持有资源的同时请求其他资源
- 不可剥夺:资源只能由持有者释放
- 循环等待:多个线程形成环形等待链
25. 如何避免死锁?
- 破坏请求与保持:一次性申请所有资源
- 破坏不可剥夺:申请不到资源时释放已持有的资源
- 破坏循环等待:按固定顺序申请资源
- 使用 tryLock:设置超时时间
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
try {
// 执行操作
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}26. 如何排查死锁?
# 使用 jstack 查看线程堆栈
jstack <pid>
# 使用 jconsole 或 VisualVM 图形化工具ThreadLocal
27. ThreadLocal 的作用?
为每个线程提供独立的变量副本,实现线程隔离。
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("value");
String value = threadLocal.get();
threadLocal.remove(); // 使用完必须移除,防止内存泄漏28. ThreadLocal 内存泄漏问题?
ThreadLocalMap 的 key 是弱引用,value 是强引用。当 ThreadLocal 被回收后,key 变为 null,但 value 仍然存在,导致内存泄漏。
解决方案:使用完后调用 remove() 方法。
其他
29. sleep() 和 wait() 的区别?
| 特性 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 释放锁 | 不释放 | 释放 |
| 唤醒方式 | 超时自动唤醒 | notify()/notifyAll() |
| 使用位置 | 任意位置 | 同步块中 |
30. notify() 和 notifyAll() 的区别?
- notify():随机唤醒一个等待线程
- notifyAll():唤醒所有等待线程
建议使用 notifyAll(),避免信号丢失。
31. 为什么 wait() 必须在同步块中调用?
wait() 会释放锁,如果不在同步块中,就没有锁可以释放,会抛出 IllegalMonitorStateException。
32. Java 内存模型(JMM)是什么?
JMM 定义了线程和主内存之间的抽象关系:
- 所有变量存储在主内存
- 每个线程有自己的工作内存
- 线程对变量的操作必须在工作内存中进行
三大特性:
- 原子性
- 可见性
- 有序性