Java 并发高频面试题(含口述版回答)
1. 创建线程的方式有哪些?
口述版:
主要有四种:
- 继承 Thread 类,重写
run()方法。 - 实现 Runnable 接口,把任务传给 Thread。
- 实现 Callable 接口,配合 FutureTask,可以拿到返回值,也能抛异常。
- 使用线程池(最推荐),通过
ExecutorService提交任务。
实际项目中几乎都用线程池,而不是自己 new Thread。自己创建线程成本高、不可控,容易造成资源浪费。
2. 线程有哪些状态?生命周期是怎样的?
口述版:
Java 线程有 6 种状态(Thread.State):
| 状态 | 说明 |
|---|---|
| NEW | 新建,还没调用 start() |
| RUNNABLE | 可运行(包括正在运行和就绪) |
| BLOCKED | 阻塞,等待获取 synchronized 锁 |
| WAITING | 无限等待(wait、join、park) |
| TIMED_WAITING | 有时限的等待(sleep、wait(timeout)) |
| TERMINATED | 终止,线程执行完毕 |
注意:Java 里没有把「运行中」和「就绪」严格区分,都叫 RUNNABLE。阻塞主要指等锁(BLOCKED)和主动等待(WAITING)。
3. synchronized 和 ReentrantLock 有什么区别?
口述版:
两者都能实现可重入的互斥锁,但有明显差异:
| 对比点 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM 内置关键字 | JDK 实现的类(基于 AQS) |
| 释放锁 | 自动释放 | 必须手动 unlock(建议 try-finally) |
| 可中断 | 不可中断 | 支持 lockInterruptibly |
| 公平锁 | 非公平 | 可选择公平/非公平 |
| 条件队列 | 一个(wait/notify) | 多个 Condition |
| 性能 | JDK6 后优化很好 | 功能更丰富,性能接近 |
使用建议:
- 简单同步用
synchronized就够了,代码更简洁安全。 - 需要尝试加锁、可中断、公平锁、多个条件队列时,用
ReentrantLock。
4. volatile 关键字的作用是什么?
口述版:
volatile 主要有两个作用:
- 保证可见性:一个线程修改了 volatile 变量,其他线程能立刻看到最新值(通过内存屏障强制刷新工作内存)。
- 禁止指令重排序:通过内存屏障防止编译器和 CPU 重排指令,保证有序性。
它不保证原子性。比如 i++ 即使加了 volatile,多线程下仍然不安全,因为 i++ 是读-改-写三步操作。
典型使用场景:
- 状态标志位(比如
volatile boolean flag) - 双重检查锁定的单例(DCL)中的实例变量
- 读写锁中的可见性保证
5. 讲讲 Java 内存模型(JMM)和 happens-before
口述版:
JMM(Java Memory Model)定义了线程如何与主内存交互,解决可见性、有序性、原子性问题。
核心概念:
- 主内存:所有线程共享
- 工作内存:每个线程私有,保存变量副本
happens-before 是判断两个操作是否存在可见性顺序的规则。常见的有:
- 程序顺序规则:同一线程内前面的操作 happens-before 后面的操作
- 监视器锁规则:unlock 操作 happens-before 后续的 lock
- volatile 规则:对 volatile 的写 happens-before 后续的读
- 传递性:A hb B 且 B hb C,则 A hb C
- 线程启动、终止、中断等规则
理解 happens-before 比死记内存屏障更重要,面试时能讲清楚可见性是怎么保证的。
6. 什么是 CAS?有什么问题?
口述版:
CAS(Compare-And-Swap)是一种乐观锁实现,包含三个操作数:内存值 V、期望值 A、要更新的值 B。只有当 V == A 时,才把 V 更新为 B,否则失败重试。
Java 中通过 Unsafe 类和原子类(AtomicInteger 等)使用 CAS。
优点:无锁,并发性能好。
三大问题:
- ABA 问题:值从 A 变成 B 再变回 A,CAS 以为没变。解决:加版本号(AtomicStampedReference)。
- 自旋开销大:长时间竞争会导致 CPU 空转。
- 只能保证一个变量的原子性:多个变量要用 AtomicReference 或加锁。
7. ThreadLocal 是什么?原理和内存泄漏问题?
口述版:
ThreadLocal 让每个线程都有自己独立的变量副本,互不干扰,常用于保存用户上下文、连接、Session 等。
原理:
每个 Thread 对象内部有一个 ThreadLocalMap,key 是 ThreadLocal 对象的弱引用,value 是具体值。
内存泄漏问题:
- key 是弱引用,value 是强引用。
- 如果 ThreadLocal 对象被回收,key 变 null,但 value 还在,造成泄漏。
- 线程池场景更严重,线程被复用,value 一直不释放。
正确用法:用完一定要调用 remove(),尤其在线程池环境。
8. 线程池核心参数有哪些?拒绝策略是什么?
口述版:
ThreadPoolExecutor 七大参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:非核心线程空闲存活时间
- unit:时间单位
- workQueue:任务队列
- threadFactory:线程工厂
- handler:拒绝策略
执行流程:
- 线程数 < corePoolSize → 创建核心线程
- 核心线程满了 → 任务进队列
- 队列满了 → 创建非核心线程(最多到 maximumPoolSize)
- 还满 → 执行拒绝策略
四种拒绝策略:
- AbortPolicy(默认):直接抛 RejectedExecutionException
- CallerRunsPolicy:用调用线程执行任务
- DiscardPolicy:直接丢弃
- DiscardOldestPolicy:丢弃队列里最老的任务,再尝试提交
生产环境一般自己定义线程池,不推荐用 Executors 工具类(容易 OOM)。
9. ConcurrentHashMap 怎么实现线程安全的?和 Hashtable、Collections.synchronizedMap 有什么区别?
口述版:
JDK 1.7:分段锁(Segment),默认 16 个段,不同段可以并发写。
JDK 1.8 及以后:取消分段锁,改用 CAS + synchronized。
- 数组节点用 CAS 操作
- 链表/红黑树头节点用 synchronized 锁定
- 锁粒度更细,并发度更高
对比:
- Hashtable:整个表用 synchronized,并发度低
- Collections.synchronizedMap:也是整张 map 加锁
- ConcurrentHashMap:锁粒度细,读操作大部分无锁,性能最好
注意:ConcurrentHashMap 的 size、isEmpty 等在并发下是弱一致性的,不保证绝对准确。
10. CountDownLatch、CyclicBarrier、Semaphore 的区别?
口述版:
三个常用同步工具:
| 工具 | 作用 | 能否重用 | 典型场景 |
|---|---|---|---|
| CountDownLatch | 一个或多个线程等待其他线程完成 | 不能 | 主线程等所有子任务结束 |
| CyclicBarrier | 一组线程互相等待,到齐后再继续 | 能(可重置) | 多线程分阶段计算 |
| Semaphore | 控制同时访问资源的线程数量 | 能 | 限流、资源池 |
简单记:
- CountDownLatch:等人(倒计时结束就放行)
- CyclicBarrier:互相等(人齐了一起走)
- Semaphore:限流(只有 N 个许可证)
11. 讲讲 AQS 原理
口述版:
AQS(AbstractQueuedSynchronizer)是 Java 并发包的基础框架,ReentrantLock、CountDownLatch、Semaphore 等都基于它实现。
核心思想:
- 用一个 volatile int state 表示同步状态
- 用 CLH 队列(双向链表)管理等待的线程
- 支持独占模式和共享模式
获取锁失败的线程会进入队列,通过 LockSupport.park() 挂起,释放锁时唤醒后继节点。
面试能讲到:state 的含义、独占/共享、FIFO 队列、park/unpark,就已经很好了。再深入可以提模板方法模式(tryAcquire、tryRelease 由子类实现)。
12. 什么是死锁?怎么排查和避免?
口述版:
死锁是两个或多个线程互相持有对方需要的锁,形成环形等待,永远阻塞。
产生死锁的四个必要条件:
- 互斥
- 占有且等待
- 不可剥夺
- 循环等待
排查:
jstack看线程堆栈,找 waiting to lock 和 locked- 或用
jconsole/ VisualVM 检测死锁
避免方法:
- 按固定顺序获取锁
- 使用定时锁(tryLock(timeout))
- 减少锁的嵌套
- 使用更高级的并发工具代替自己加锁
13. synchronized 的锁升级过程是怎样的?
口述版:
JDK 6 之后对 synchronized 做了大量优化,锁有四个状态:
- 无锁
- 偏向锁:只有一个线程进入,直接偏向该线程,几乎无开销
- 轻量级锁:有竞争但程度低,用 CAS 自旋
- 重量级锁:竞争激烈,升级为操作系统互斥量,线程挂起
锁可以升级,但不能降级(偏向锁在一定条件下可以批量撤销)。
理解锁升级是为了说明:synchronized 在低竞争下性能已经很好,不一定非要用 ReentrantLock。
14. Future 和 CompletableFuture 有什么区别?
口述版:
Future 是 Java 5 引入的,只能通过 get() 阻塞获取结果,功能很弱:
- 不能方便地做链式调用
- 不能组合多个异步任务
- 异常处理麻烦
CompletableFuture 是 Java 8 引入的,实现了 Future 和 CompletionStage,功能强大:
- 支持非阻塞回调(
thenApply、thenAccept等) - 可以组合多个任务(
allOf、anyOf、thenCombine) - 支持异常处理(
exceptionally、handle) - 可以手动完成(
complete)
在异步编程、并行任务编排场景,强烈推荐用 CompletableFuture,而不是原始 Future。
(详细对比和阻塞问题可结合之前讨论的「Future.get() 占用 HTTP 线程」来答,很加分。)
15. 线程池为什么不推荐用 Executors 创建?
口述版:
《阿里巴巴 Java 开发手册》明确不推荐,原因是:
- FixedThreadPool 和 SingleThreadPool:队列是无界的 LinkedBlockingQueue,任务堆积会导致 OOM。
- CachedThreadPool:允许创建的线程数是 Integer.MAX_VALUE,可能创建大量线程导致 OOM。
- ScheduledThreadPool:同样存在线程数失控风险。
正确做法是自己通过 ThreadPoolExecutor 构造方法,明确指定核心参数和拒绝策略,让资源可控。
面试加分点
- 能结合项目讲线程池参数是怎么评估的、拒绝策略选了什么。
- 清楚 ConcurrentHashMap 在 JDK7 和 JDK8 的实现差异。
- 能讲清楚 volatile 和 synchronized 在可见性、原子性上的区别。
- 对 AQS、CAS、happens-before 有基本认知。
- 实际用过 CompletableFuture 做异步编排(比如之前 PDF 异步生成),能说出优势。
- 知道 ThreadLocal 内存泄漏的原因和正确用法。
快速记忆框架:
Java 并发面试核心围绕:
线程基础 → synchronized / Lock → volatile / JMM → CAS / 原子类 → 线程池 → 并发容器 → AQS 与同步工具 → CompletableFuture
把原理说清楚,再结合自己项目中的实际使用场景,竞争力会明显提升。