Java 并发面试题汇总
Java 并发是 Java 后端面试中的高频考点,尤其是
synchronized、volatile、CAS、AQS、ReentrantLock、线程池、ThreadLocal等。本文按照“线程基础 → Java 内存模型 → synchronized → volatile → CAS → AQS → Lock → 线程池 → ThreadLocal → 并发容器”的顺序进行整理,方便面试前快速复习。
一、线程基础
1. 什么是进程?什么是线程?
进程是操作系统进行资源分配的基本单位,一个进程拥有独立的内存空间。
线程是操作系统进行 CPU 调度的基本单位,一个进程可以包含多个线程,这些线程共享进程的内存资源。
例如:
JVM 进程 │ ├── 主线程 ├── GC 线程 ├── JIT 编译线程 └── 其他业务线程面试回答
进程是资源分配的基本单位,线程是 CPU 调度的基本单位。同一个进程中的多个线程共享堆、方法区等资源,但每个线程拥有自己的程序计数器、虚拟机栈和本地方法栈。
2. 为什么要使用多线程?
主要有三个原因:
① 提高 CPU 利用率
如果一个线程因为 IO 操作阻塞,CPU 可以切换去执行其他线程。
② 提高程序响应速度
多个任务可以并发执行。
③ 提高系统吞吐量
例如 Web 服务中,可以使用线程池同时处理多个用户请求。
3. 创建线程有哪些方式?
常见方式:
方式一:继承 Thread
classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("执行任务");}}方式二:实现 Runnable
Runnabletask=()->{System.out.println("执行任务");};newThread(task).start();方式三:实现 Callable
Callable<Integer>task=()->100;FutureTask<Integer>futureTask=newFutureTask<>(task);newThread(futureTask).start();方式四:使用线程池
实际开发中更推荐线程池:
ExecutorServiceexecutor=Executors.newFixedThreadPool(10);executor.submit(()->{System.out.println("执行任务");});二、线程的生命周期
Java 中线程主要有以下状态:
NEW │ │ start() ↓ RUNNABLE │ ├── BLOCKED ├── WAITING └── TIMED_WAITING │ ↓ TERMINATED4. NEW
线程对象创建完成,但是还没有调用start()。
Threadthread=newThread();5. RUNNABLE
调用start()后进入RUNNABLE状态。
需要注意:
Java 中的 RUNNABLE 包含操作系统层面的“就绪”和“运行”两种状态。
6. BLOCKED
线程等待获取synchronized锁时,如果锁已经被其他线程占用,就会进入 BLOCKED。
7. WAITING
线程主动等待其他线程通知。
例如:
Objectlock=newObject();synchronized(lock){lock.wait();}8. TIMED_WAITING
带时间限制的等待。
例如:
Thread.sleep(1000);或者:
lock.wait(1000);三、并发编程存在什么问题?
并发编程最核心的三个问题:
可见性、原子性、有序性
9. 什么是可见性?
一个线程修改共享变量之后,其他线程能够立即看到修改结果。
例如:
booleanflag=false;ThreadA:flag=true;ThreadB:while(!flag){}如果没有正确的内存同步机制,线程 B 可能一直读取到旧值。
10. 什么是原子性?
一个操作要么全部执行,要么全部不执行,中间不能被其他线程打断。
例如:
count++;看起来是一条语句,实际上可以拆成:
读取 count ↓ count + 1 ↓ 写回 count如果两个线程同时执行,就可能发生数据覆盖。
11. 什么是有序性?
程序执行过程中,编译器、JIT 和 CPU 为了优化性能,可能对指令进行重排序。
在单线程环境下,重排序通常不会影响最终结果。
但是在多线程环境下,如果没有正确的同步机制,就可能产生问题。
四、synchronized
12. synchronized 是什么?
synchronized是 Java 提供的内置锁机制,可以保证多个线程对共享资源的互斥访问。
同时可以保证:
- 原子性
- 可见性
- 有序性
例如:
publicsynchronizedvoidadd(){count++;}13. synchronized 锁的是什么?
synchronized本质上是对对象监视器 Monitor进行加锁。
例如:
synchronized(obj){// 临界区}锁住的是obj对象。
14. synchronized 修饰实例方法锁的是谁?
publicsynchronizedvoidmethod(){}等价于:
publicvoidmethod(){synchronized(this){}}所以锁的是:
当前对象
this
15. synchronized 修饰静态方法锁的是谁?
publicstaticsynchronizedvoidmethod(){}锁的是:
当前类的 Class 对象。
例如:
synchronized(MyClass.class){}16. synchronized 的锁升级过程
早期 JVM 中经常介绍:
无锁 ↓ 偏向锁 ↓ 轻量级锁 ↓ 重量级锁不过需要注意:
JDK 15 开始偏向锁默认被禁用,JDK 18 移除了偏向锁相关实现。因此面试时可以了解历史上的锁升级机制,但不要把“偏向锁一定存在”当成当前 JVM 的绝对事实。
传统锁升级主要是为了:
在不同竞争程度下,用不同成本的锁实现提高性能。
五、volatile
17. volatile 是什么?
volatile是 Java 提供的轻量级同步机制,可以保证:
可见性 + 禁止特定类型的指令重排序
例如:
privatevolatilebooleanrunning=true;一个线程修改:
running=false;其他线程能够及时看到最新值。
18. volatile 能保证原子性吗?
不能。
例如:
volatileintcount=0;count++;依然不是原子操作。
因为:
读取 count ↓ count + 1 ↓ 写回 count多个线程依然可能同时读取到相同的值。
19. synchronized 和 volatile 有什么区别?
| 对比 | synchronized | volatile |
|---|---|---|
| 原子性 | ✅ | ❌ |
| 可见性 | ✅ | ✅ |
| 有序性 | ✅ | 部分保证 |
| 阻塞 | 可能阻塞 | 不会因为读写本身产生锁阻塞 |
| 使用场景 | 临界区、复合操作 | 状态标志、简单共享变量 |
一句话:
volatile适合解决共享变量的可见性问题,synchronized更适合解决需要互斥执行的临界区问题。
六、Java 内存模型 JMM
20. 什么是 JMM?
JMM,即 Java Memory Model,Java 内存模型。
它不是简单指 JVM 中的某一块物理内存,而是一套规范,用于解决:
多线程环境下共享变量的可见性、原子性和有序性问题。
线程拥有自己的工作内存:
主内存 / | \ / | \ 线程 A 线程 B 线程 C 工作内存 工作内存 工作内存线程对共享变量的操作,需要通过主内存进行同步。
七、happens-before
21. 什么是 happens-before?
happens-before 是 JMM 中用于描述操作之间可见性关系的规则。
如果:
A happens-before B那么意味着:
A 操作的结果对 B 操作可见。
常见规则:
① 程序次序规则
同一个线程中:
inta=10;intb=a;前面的操作 happens-before 后面的操作。
② 锁规则
解锁操作 happens-before 后续对同一个锁的加锁操作。
③ volatile 规则
对 volatile 变量的写操作 happens-before 后续对该变量的读操作。
④ 线程启动规则
Thread.start()happens-before 新线程中的操作。
⑤ 线程终止规则
线程中的所有操作 happens-before 其他线程检测到该线程已经终止。
例如:
thread.join();八、CAS
22. 什么是 CAS?
CAS:
Compare And Swap,比较并交换。
它是一种乐观锁思想。
CAS 包含三个值:
内存地址 旧值 新值执行逻辑:
当前值 == 预期旧值? │ ┌────┴────┐ │ │ 是 否 │ │ 修改新值 修改失败23. CAS 有什么优点?
CAS 不需要像传统互斥锁一样让线程阻塞等待。
因此在竞争不激烈的情况下性能较好。
Java 中很多原子类底层都会使用 CAS。
例如:
AtomicIntegercount=newAtomicInteger();count.incrementAndGet();24. CAS 有什么问题?
主要有两个:
① ABA 问题
假设:
线程 A:读取 A 线程 B:A → B 线程 B:B → A 线程 A:发现还是 A,于是 CAS 成功线程 A 无法感知中间发生过变化。
解决方式:
使用带版本号的 CAS。
例如:
AtomicStampedReference② 自旋开销
CAS 失败后可能不断重试:
CAS失败 ↓ 重新读取 ↓ CAS ↓ 失败 ↓ 继续重试如果竞争非常激烈,会消耗 CPU。
九、Atomic 原子类
25. AtomicInteger 是什么?
AtomicInteger是 Java 并发包提供的原子操作类。
例如:
AtomicIntegercount=newAtomicInteger();count.incrementAndGet();相比:
count++;可以保证自增操作的原子性。
26. AtomicInteger 为什么线程安全?
底层主要依赖:
CAS + volatile
例如:
读取当前值 ↓ 计算新值 ↓ CAS 更新 ↓ 失败则重试十、AQS
27. 什么是 AQS?
AQS:
AbstractQueuedSynchronizer,抽象队列同步器。
它是 Java 并发包中非常核心的基础组件。
很多并发工具都是基于 AQS 实现的,例如:
- ReentrantLock
- ReentrantReadWriteLock
- Semaphore
- CountDownLatch
28. AQS 的核心思想是什么?
AQS 核心可以理解成:
一个 state + 一个等待队列
例如:
AQS │ ┌─────┴─────┐ │ │ state CLH队列 │ ┌──────┼──────┐ ↓ ↓ ↓ 线程A 线程B 线程Cstate用来表示同步状态。
当线程获取资源失败后,就进入等待队列。
29. AQS 为什么需要队列?
如果线程获取锁失败,不能让所有线程一直 CAS 自旋,否则会浪费大量 CPU。
所以 AQS 会:
获取资源失败 ↓ 进入等待队列 ↓ 阻塞线程 ↓ 资源释放 ↓ 唤醒后继节点这样可以降低 CPU 消耗。
十一、ReentrantLock
30. ReentrantLock 是什么?
ReentrantLock是 Java 提供的可重入锁。
例如:
ReentrantLocklock=newReentrantLock();lock.lock();try{// 临界区}finally{lock.unlock();}31. ReentrantLock 和 synchronized 有什么区别?
| 对比 | synchronized | ReentrantLock |
|---|---|---|
| 使用方式 | JVM 内置 | Java API |
| 可重入 | ✅ | ✅ |
| 公平锁 | 不支持显式设置 | 支持 |
| 尝试获取锁 | 不方便 | tryLock() |
| 可中断 | 支持一定形式 | lockInterruptibly() |
| 条件队列 | wait/notify | Condition |
| 自动释放 | ✅ | ❌,需要手动 unlock |
面试时可以这样回答:
两者都可以实现互斥和可重入,但 ReentrantLock 提供了更丰富的功能,例如公平锁、尝试获取锁、可中断获取锁以及多个 Condition 条件队列。synchronized 使用起来更加简单,正常情况下优先使用 synchronized 即可。
十二、ReentrantLock 为什么可重入?
例如:
lock.lock();lock.lock();try{// ...}finally{lock.unlock();lock.unlock();}同一个线程可以多次获取同一把锁。
内部会记录:
持有线程 = 当前线程 state = 2每获取一次:
state++每释放一次:
state--直到:
state == 0锁才真正释放。
十三、Condition
32. Condition 是什么?
Condition可以理解为:
ReentrantLock 版本的 wait/notify。
例如:
Conditioncondition=lock.newCondition();lock.lock();try{condition.await();}finally{lock.unlock();}其他线程:
lock.lock();try{condition.signal();}finally{lock.unlock();}十四、线程池
33. 为什么要使用线程池?
线程池主要解决三个问题:
① 降低线程创建和销毁的开销
线程可以重复使用。
② 控制线程数量
避免创建大量线程导致 CPU 和内存资源耗尽。
③ 统一管理线程
可以统一进行:
- 任务提交
- 线程管理
- 任务排队
- 拒绝策略
十五、线程池核心参数
JavaThreadPoolExecutor最重要的参数:
ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);分别表示:
| 参数 | 含义 |
|---|---|
| corePoolSize | 核心线程数 |
| maximumPoolSize | 最大线程数 |
| keepAliveTime | 非核心线程空闲存活时间 |
| workQueue | 任务等待队列 |
| threadFactory | 创建线程的工厂 |
| handler | 拒绝策略 |
十六、线程池执行流程
这是线程池面试中非常重要的一道题。
假设:
核心线程数 = 2 最大线程数 = 4 队列容量 = 10提交任务后:
任务提交 ↓ 核心线程是否未满? ↓ 是 → 创建核心线程执行 ↓ 否 ↓ 任务加入阻塞队列 ↓ 队列满了吗? ↓ 否 → 等待 ↓ 是 ↓ 线程数 < 最大线程数? ↓ 是 → 创建非核心线程 ↓ 否 ↓ 执行拒绝策略面试时一定要能把这个流程讲出来。
十七、线程池为什么不推荐 Executors?
例如:
Executors.newFixedThreadPool(10);看起来方便,但是实际开发中通常不推荐直接使用。
原因是:
FixedThreadPool
底层使用无界队列:
LinkedBlockingQueue任务过多时可能导致大量任务堆积,最终造成 OOM。
CachedThreadPool
线程数量理论上可以无限增长。
大量任务提交时可能创建大量线程。
所以实际开发中一般直接使用:
ThreadPoolExecutor明确指定:
- 核心线程数
- 最大线程数
- 队列容量
- 拒绝策略
十八、线程池拒绝策略
线程池无法继续接收任务时,会执行拒绝策略。
JDK 提供四种常见策略:
AbortPolicy
直接抛出异常。
RejectedExecutionExceptionCallerRunsPolicy
由提交任务的线程自己执行任务。
DiscardPolicy
直接丢弃任务。
DiscardOldestPolicy
丢弃队列中最旧的任务,然后重新提交。
十九、ThreadLocal
34. 什么是 ThreadLocal?
ThreadLocal可以为每个线程提供独立的变量副本。
例如:
ThreadLocal<String>threadLocal=newThreadLocal<>();threadLocal.set("hello");Stringvalue=threadLocal.get();不同线程获取到的是不同的数据。
35. ThreadLocal 的底层原理
可以简单理解为:
Thread │ └── ThreadLocalMap │ ├── ThreadLocal → value ├── ThreadLocal → value └── ThreadLocal → value注意:
ThreadLocalMap 是 Thread 对象内部的一部分,每个线程都有自己的 ThreadLocalMap。
二十、ThreadLocal 为什么会内存泄漏?
这是非常高频的问题。
ThreadLocalMap 中:
Entry ├── Key:ThreadLocal 的弱引用 └── Value:强引用如果 ThreadLocal 对象没有其他强引用:
ThreadLocal ↓ 被 GC 回收 ↓ Entry 的 Key = null但是:
Thread ↓ ThreadLocalMap ↓ Entry ↓ ValueValue 仍然可能存在强引用。
如果线程长期存活,例如线程池中的工作线程,就可能导致 Value 长时间无法回收。
因此使用 ThreadLocal 后应该及时:
try{threadLocal.set(value);}finally{threadLocal.remove();}二十一、并发容器
Java 提供了一系列线程安全的并发容器。
常见的有:
ConcurrentHashMap CopyOnWriteArrayList ConcurrentLinkedQueue BlockingQueue二十二、ConcurrentHashMap
36. 为什么需要 ConcurrentHashMap?
普通 HashMap 在多线程环境下可能出现:
- 数据覆盖
- 数据不一致
- 并发修改问题
因此需要线程安全的 Map。
37. ConcurrentHashMap 和 Hashtable 有什么区别?
Hashtable很多方法直接使用 synchronized。
锁粒度比较大。
而ConcurrentHashMap采用更加细粒度的并发控制方式,因此并发性能更好。
38. JDK 1.7 的 ConcurrentHashMap
JDK 1.7 使用:
Segment + HashEntry
整体结构:
ConcurrentHashMap │ ┌──────┼──────┐ Segment Segment Segment │ HashEntry...不同 Segment 可以同时执行操作。
39. JDK 1.8 的 ConcurrentHashMap
JDK 1.8 去掉了 Segment,整体结构更接近:
Node[] │ ├── Node ├── Node └── TreeBin主要通过:
CAS + synchronized
实现并发控制。
面试可以简单总结:
JDK 1.7 的 ConcurrentHashMap 采用 Segment 分段锁;JDK 1.8 去掉 Segment,采用 CAS + synchronized 对桶进行更细粒度的控制,并在链表过长时转成红黑树。
二十三、CopyOnWriteArrayList
40. 什么是 CopyOnWriteArrayList?
CopyOnWriteArrayList 是一种适合:
读多写少
场景的线程安全 List。
写操作时会复制一份数组:
原数组 [ A B C ] ↓ 写操作 新数组 [ A B C D ]然后再替换原来的数组。
41. CopyOnWriteArrayList 有什么缺点?
① 写操作成本高
每次写都需要复制数组。
② 占用额外内存
修改期间会存在新旧两份数组。
③ 不适合大量写操作
所以它比较适合:
读多写少的场景。
二十四、BlockingQueue
42. 什么是 BlockingQueue?
阻塞队列。
当队列满时:
生产者阻塞。
当队列为空时:
消费者阻塞。
典型应用:
生产者-消费者模型。
例如线程池中的任务队列,本质上也是一种生产者消费者思想。
二十五、CountDownLatch
43. CountDownLatch 是什么?
CountDownLatch 可以让一个或多个线程等待其他线程完成任务。
例如:
CountDownLatchlatch=newCountDownLatch(3);三个线程分别执行:
latch.countDown();主线程:
latch.await();直到计数器:
3 → 2 → 1 → 0主线程才继续执行。
二十六、Semaphore
44. Semaphore 是什么?
Semaphore 用于控制同时访问某个资源的线程数量。
例如限制最多 3 个线程访问数据库:
Semaphoresemaphore=newSemaphore(3);线程获取许可:
semaphore.acquire();使用完成:
semaphore.release();可以把它理解成:
一个“许可证池”。
二十七、死锁
45. 什么是死锁?
两个或多个线程互相等待对方持有的资源,导致所有线程都无法继续执行。
例如:
线程 A: 持有锁1 等待锁2 线程 B: 持有锁2 等待锁1形成:
A → 等待 B ↑ ↓ └───────┘二十八、死锁产生的四个必要条件
经典死锁条件:
① 互斥
资源一次只能被一个线程使用。
② 持有并等待
线程已经持有资源,同时继续等待其他资源。
③ 不可剥夺
线程已经获得的资源不能被强制抢走。
④ 循环等待
多个线程形成循环等待关系。
只要破坏其中一个条件,就可以避免死锁。
二十九、如何排查死锁?
可以使用:
jstack例如:
jstack<pid>JVM 会输出线程之间的锁竞争情况。
另外也可以使用:
jconsole或者:
VisualVM辅助分析。
三十、sleep 和 wait 有什么区别?
这是面试中的经典题。
| 对比 | sleep | wait |
|---|---|---|
| 所属类 | Thread | Object |
| 是否释放锁 | ❌ | ✅ |
| 是否需要 synchronized | 不需要 | 需要 |
| 唤醒方式 | 时间到自动恢复 | notify/notifyAll 或超时 |
| 使用场景 | 线程休眠 | 线程间通信 |
例如:
synchronized(lock){lock.wait();}调用wait()后:
当前线程释放锁,并进入等待状态。
三十一、notify 和 notifyAll 有什么区别?
notify()随机唤醒一个等待线程。
notifyAll()唤醒所有等待线程。
实际开发中,如果无法明确判断应该唤醒哪个线程,通常更安全的是:
notifyAll();三十二、并发编程面试高频总结
面试 Java 并发时,可以重点掌握下面这张知识图:
Java 并发 │ ├── 线程基础 │ ├── 线程状态 │ ├── sleep / wait │ └── notify / notifyAll │ ├── JMM │ ├── 可见性 │ ├── 原子性 │ ├── 有序性 │ └── happens-before │ ├── synchronized │ ├── Monitor │ ├── 可重入 │ └── 锁机制 │ ├── volatile │ ├── 可见性 │ └── 禁止特定重排序 │ ├── CAS │ ├── Atomic │ ├── ABA │ └── 自旋 │ ├── AQS │ ├── state │ ├── 等待队列 │ └── ReentrantLock │ ├── 线程池 │ ├── 核心参数 │ ├── 执行流程 │ └── 拒绝策略 │ ├── ThreadLocal │ ├── ThreadLocalMap │ └── 内存泄漏 │ ├── 并发容器 │ ├── ConcurrentHashMap │ ├── CopyOnWriteArrayList │ └── BlockingQueue │ └── 并发工具类 ├── CountDownLatch ├── Semaphore └── CyclicBarrier三十三、面试前重点背诵清单
如果时间比较紧,不需要平均分配时间,优先掌握下面这些。
第一梯队:必须掌握
- synchronized 原理
- synchronized 和 volatile 的区别
- volatile 为什么不能保证原子性
- JMM
- happens-before
- CAS
- ABA 问题
- AQS
- ReentrantLock
- 线程池核心参数
- 线程池执行流程
- 线程池拒绝策略
- ThreadLocal 原理
- ThreadLocal 内存泄漏
- ConcurrentHashMap 1.7 和 1.8 的区别
- 死锁
第二梯队:需要理解
- sleep 和 wait 区别
- notify 和 notifyAll
- CountDownLatch
- Semaphore
- CopyOnWriteArrayList
- BlockingQueue
- Atomic 原子类
第三梯队:了解即可
- CyclicBarrier
- Phaser
- Exchanger
- ForkJoinPool
- CompletableFuture
三十四、最后总结
Java 并发这部分看起来知识点很多,但实际上可以围绕几个核心问题展开:
多个线程同时操作共享数据时,如何保证数据正确?
于是就产生了:
可见性 ↓ volatile / synchronized / JMM 原子性 ↓ synchronized / Lock / CAS / Atomic 有序性 ↓ volatile / happens-before 线程协作 ↓ wait / notify / Condition 线程竞争 ↓ synchronized / AQS / ReentrantLock 任务管理 ↓ ThreadPoolExecutor 线程隔离 ↓ ThreadLocal 并发容器 ↓ ConcurrentHashMap / CopyOnWriteArrayList真正面试的时候,不要只背定义。
例如面试官问:
“为什么 volatile 不能保证 count++ 的线程安全?”
不要只回答:
“因为 volatile 不能保证原子性。”
更好的回答方式是:
count++实际上包含读取、修改、写回三个步骤,即使 count 使用 volatile 保证了可见性,多个线程仍然可能同时读取到相同的旧值,然后分别进行加一并写回,导致其中一次修改被覆盖。因此 volatile 只能保证单次读写的可见性,不能保证这种复合操作的原子性。如果需要保证自增操作的线程安全,可以使用 AtomicInteger 的 CAS,或者使用 synchronized / Lock。
这才是面试中真正有区分度的回答。
Java 并发学习路线
如果按照后端面试的顺序学习,可以记成:
线程基础 ↓ JMM ↓ synchronized ↓ volatile ↓ CAS ↓ Atomic ↓ AQS ↓ ReentrantLock ↓ 线程池 ↓ ThreadLocal ↓ ConcurrentHashMap ↓ 其他并发工具类 ↓ 源码 + 实战其中最重要的是:
JMM → synchronized / volatile → CAS → AQS → 线程池 → ThreadLocal → ConcurrentHashMap
把这一条主线真正理解之后,再去看源码,会比单纯背八股轻松很多。