目录
- 前言
- 一、各类锁相关面试题汇总
- 二、CAS 机制相关面试题
- 三、Synchronized 面试问题
- 四、JUC 工具类(Callable、ReentrantLock、线程池、Semaphore、CountDownLatch)面试题
- 五、并发安全集合面试题
- 六、死锁相关面试题
- 七、Java 多线程综合杂项面试题
- 文末总结
前言
多线程是 Java 后端面试必考重难点,锁原理、CAS、Synchronized 底层、JUC 工具包、ConcurrentHashMap、死锁等问题频繁出现在校招与社招面试中。本文基于多线程进阶知识点,整理全量面试真题 + 标准答案,方便面试复习与日常查阅。
一、各类锁相关面试题汇总
1. 怎么理解乐观锁和悲观锁?如何实现?
答案: 悲观锁默认并发冲突概率很高,访问资源前必须加锁,其他线程阻塞等待,依托操作系统 mutex 实现。 乐观锁默认冲突概率低,不加锁直接访问数据,更新时校验冲突;冲突则报错交由业务处理,主流实现为版本号 + CAS。 Synchronized 初始采用乐观锁,竞争激烈自动转为悲观锁。
- 悲观锁:synchronized、ReentrantLock;
- 乐观锁:CAS + 版本号、数据库 version 字段。
2. 介绍读写锁原理与适用场景
答案: 读写锁区分读、写两种锁:
- 读锁与读锁:不互斥,多线程可并发读取;
- 写锁与写锁:互斥,同一时间只能一个线程写入;
- 读锁与写锁:互斥,读写不能同时进行。 Java 实现:
ReentrantReadWriteLock,适合读多写少场景(如配置查询、教务系统查看数据),synchronized 不属于读写锁。
3. 什么是自旋锁?优缺点?
答案: 抢锁失败不放弃 CPU、不阻塞挂起,循环重试抢锁即为自旋锁,是轻量级锁常用实现。
- 优点:无线程阻塞、无用户态内核态切换,锁短时释放可立刻抢到锁;
- 缺点:持有锁时间过长,持续空转浪费 CPU 资源。
4. synchronized 是可重入锁吗?
答案: 是可重入锁(递归锁),同一线程可重复获取同一把锁,不会自身死锁。 底层:锁对象记录持有线程 ID + 计数器,同一线程重复加锁计数器 + 1,解锁计数器 - 1,计数归零才算释放锁;Linux 原生 mutex 为不可重入锁。
5. synchronized的底层原理
5.1. 锁的本质
锁的是对象,不是代码;锁信息存在 对象头 Mark Word 里。
5.2. 锁状态(JDK1.6+,只能升级、不能降级)
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
5.3. 偏向锁(无竞争)
- 只有一个线程访问
- Mark Word 记录线程 ID
- 同一线程再来:直接放行、不加锁、不 CAS
- 有竞争:撤销偏向锁 → 升级轻量级锁
5.4. 轻量级锁(少量竞争)
- 底层:CAS + 自旋(用户态)
- 线程栈创建 Lock Record
- CAS 替换 Mark Word
- 失败:自旋重试、不阻塞、不进内核
- 自旋次数多 / 竞争激烈 → 升级重量级锁
5.5. 重量级锁(大量竞争)
- 底层:依赖 OS 的 mutex(内核态)
- 抢锁失败:线程阻塞、挂起
- 锁释放:OS 唤醒线程
- 开销大(用户态 / 内核态切换、线程调度)
5.6. 字节码层面
编译生成:monitorenter / monitorexit 每个对象对应 monitor(管程):owner、entrySet、waitSet
5.7. 四大优化(JDK1.6+)
- 偏向锁:无竞争几乎零开销
- 轻量级 + 自旋:少竞争不阻塞、不切内核
- 锁消除:单线程无竞争锁直接删掉
- 锁粗化:连续多次加锁合并成一次
5.8. 五大特性
- 可重入锁:同一线程可重复加锁(计数器)
- 非公平锁:新线程可插队
- 悲观锁(竞争时)
- 自适应锁:竞争强→重,弱→轻
- 自动释放:出代码块自动 unlock
5.9. 一句话总结
Synchronized 基于对象头 Mark Word,实现无锁→偏向→轻量(CAS 自旋)→重量(OS mutex)逐级升级;弱竞争用户态自旋,强竞争内核阻塞,全程自适应优化。
6. 公平锁与非公平锁区别?synchronized 属于哪种?
答案: 公平锁遵循先来后到,按线程排队顺序获取锁;非公平锁随机分配锁,新线程可直接插队。 操作系统原生调度随机,默认非公平;synchronized 是非公平锁,ReentrantLock 可通过构造参数手动开启公平锁。
二、CAS 机制相关面试题
1. 简述 CAS 机制
答案: CAS 全称比较并交换 (Compare And Swap),硬件原子指令,包含三个参数:内存原值 V、预期旧值 A、新值 B。
- 比较 V 和 A 是否一致;
- 一致则将 B 赋值给 V,返回 true;不一致修改失败返回 false。 是乐观锁底层实现,依托 CPU 硬件 lock 指令保证原子性,Unsafe 类提供底层 CAS 方法。
2. 什么是 ABA 问题?如何解决?
答案: 线程读取数据为 A,期间数据被其他线程 A→B→A,原线程 CAS 校验数值一致,误以为数据未改动,引发业务异常(如重复扣款)。 解决方案:数据 + 版本号,每次修改版本号自增,CAS 同时校验数值与版本,只要版本号增加了,就说明该数据已经被改了;Java 对应AtomicStampedReference。
3.AtomicInteger实现原理
AtomicInteger 是 Java 提供的原子整型类,基于 CAS + 自旋 实现无锁线程安全,底层依赖 Unsafe 类提供的硬件级原子指令。
-
3.1value 使用 volatile 修饰
- 保证多线程之间的内存可见性
- 一个线程修改,其他线程立刻能看到最新值
-
3.2CAS(Compare And Swap)比较并交换
- 包含三个参数:内存值 V、预期值 A、更新值 B
- 逻辑:
- 如果
V == A,说明没被修改,赋值V = B - 如果
V != A,说明被修改过,更新失败
- 如果
-
3.3自旋(循环重试)
- CAS 更新失败时,不阻塞、不挂起
- 重新获取最新值,再次 CAS,直到成功
public class AtomicInteger {
// volatile 保证可见性
private volatile int value;
// 自增 1 方法
public final int getAndIncrement() {
// 自旋循环
for (;;) {
// 1. 获取当前最新值
int oldValue = value;
// 2. 计算新值
int newValue = oldValue + 1;
// 3. CAS 尝试更新
if (compareAndSwap(oldValue, newValue)) {
// 成功就退出
return oldValue;
}
// 失败 → 继续循环重试
}
}
// 本地方法:硬件原子指令
private native boolean compareAndSwap(int expect, int update);
}
3.3. 特点(面试加分)
- 无锁设计,不使用 synchronized,性能极高
- 线程安全,适合高并发计数、序号生成
- CAS 是硬件级原子操作,由 CPU 指令保证原子性
- 自旋避免线程阻塞,高并发下比锁快很多
3.4. 一句话总结(超精简背诵版)
AtomicInteger 基于 volatile 保证可见性,CAS 原子指令保证更新安全,自旋循环保证修改成功,实现无锁、高性能的线程安全自增
三、Synchronized 面试问题
1. 什么是偏向锁?
答案: 无锁竞争场景优化,第一个线程获取锁后,对象头 Mark Word 记录线程 ID,后续同一线程再次加锁直接放行,不执行 CAS、不加真实锁;出现其他线程竞争时撤销偏向锁,升级为轻量级锁,实现延迟加锁、降低开销。
2. 简述 synchronized 整体实现原理
答案: JDK1.6 之后锁分级升级:无锁→偏向锁→轻量级锁→重量级锁(锁只能升级不能降级)。
- 偏向锁:单线程无竞争,标记线程 ID;
- 轻量级锁:少量竞争,CAS + 自适应自旋(用户态);
- 重量级锁:高并发竞争,依赖 OS 的 mutex,线程阻塞挂起(内核态切换,开销大)。 额外优化:锁消除(JVM 剔除无效锁)、锁粗化(合并频繁加解锁);特性:可重入、非公平、自适应升降级、自动释放锁。 字节码层面依靠 monitorenter、monitorexit 指令,对象头 Mark Word 存储所有锁标记。
四、JUC 工具类面试题(观看上一篇博客,比较详细)
1. synchronized 和 ReentrantLock 区别?为什么 JDK 还要新增 Lock 锁?
答案:
- 实现:synchronized 是 JVM 关键字 (C++ 底层),自动释放锁;ReentrantLock 是 Java API,需要手动 finally 解锁;
- 抢锁:synchronized 阻塞死等;Lock 支持 tryLock 超时放弃获取;
- 公平:synchronized 固定非公平;Lock 可自定义公平 / 非公平;
- 等待唤醒:synchronized 依靠 wait/notify 随机唤醒;Lock 配合 Condition 精准唤醒指定线程。 竞争激烈、需要公平锁、限时等待场景优先使用 ReentrantLock。
2. Callable 和 Runnable 区别,FutureTask 作用?
答案: Runnable 无返回值、不能抛出受检异常;Callable 带泛型返回值、可抛出异常。 FutureTask 包装 Callable,用于保存异步任务结果,get () 阻塞等待线程执行结束获取返回值。
3. ThreadPoolExecutor 七大参数含义?
答案:
- corePoolSize:核心线程数(常驻线程,不会被回收);
- maximumPoolSize:最大线程数 = 核心 + 临时线程;
- keepAliveTime:临时线程空闲超时时间;
- unit:时间单位;
- workQueue:阻塞任务队列,存等待执行的任务;
- threadFactory:线程工厂,创建线程;
- handler:拒绝策略,任务满载后的处理规则。 拒绝策略四种:Abort 抛异常、CallerRuns 调用线程执行、DiscardOldest 丢弃队首任务、Discard 丢弃新任务。
4. Executors 创建线程池四种方式?
答案:
- newFixedThreadPool:固定线程数量;
- newCachedThreadPool:线程动态扩容,空闲自动回收;
- newSingleThreadExecutor:单线程池;
- newScheduledThreadPool:定时延迟执行线程池。底层全部基于 ThreadPoolExecutor 封装。
5. Semaphore 信号量作用与使用场景?
答案: 本质计数器,控制同一时间可访问资源的并发线程数。acquire () 申请资源计数器 - 1,release () 释放 + 1;计数器为 0 时线程阻塞。常用做接口限流、连接池资源管控、共享锁实现。
6. CountDownLatch 作用?
答案: 等待多个子线程全部执行完毕后主线程再继续执行。初始化指定任务数,子线程执行完 countDown () 计数器递减,主线程 await () 阻塞直到计数器归零。
五、并发安全集合面试题
1. ConcurrentHashMap 读操作需要加锁吗?为什么?
答案: 读不加锁,使用 volatile 保证读取主内存最新数据,提升并发读性能;仅写操作加锁。
2. JDK1.7 与 1.8 ConcurrentHashMap 区别?
答案: JDK1.7:分段锁 Segment,将数组分成多段,每段独立加锁; JDK1.8:取消分段锁,改用数组 + 链表 / 红黑树,锁每个哈希桶头结点;CAS+Synchronized 实现,扩容拆分搬运、多线程协助迁移数据。
3. HashMap、Hashtable、ConcurrentHashMap 三者区别?
| 特性 | HashMap | Hashtable | ConcurrentHashMap(JDK1.8) |
|---|---|---|---|
| 线程安全 | 不安全 | 安全 | 安全 |
| 加锁位置 | 无锁 | 锁整个对象(数组 + 全集合一把锁) | 锁单个链表头(桶锁),分段淘汰 |
| null 键 / 值 | key、value 都允许 null | key、value 都不允许 null | key 不能 null,value 不能 null |
| 底层结构 | 数组 + 链表;链表 > 8 转红黑树 | 数组 + 链表(无红黑树) | 数组 + 链表 / 红黑树 |
| 扩容 | 单线程扩容 | 单线程全量拷贝扩容 | 多线程协助分段迁移扩容 |
| 效率 | 单线程极高,并发死链 | 全表独占锁,并发极低 | 分段桶锁 + CAS,并发高性能 |
3.1. Hashtable 锁模型
【一把大锁锁住整个HashTable对象】
┌──────────────────────────────┐
│ synchronized(this)全局对象锁 │
│ 数组[0] 数组[1] 数组[2]... │
│ 链表1 链表2 链表3... │
└──────────────────────────────┘
规则:任意线程get/put,全部争抢同一把锁,全部串行排队
缺点:哪怕操作不同下标数据,也要等锁,并发全阻塞。
3.2. HashMap(无锁)
无任何同步锁
数组[0]链表 数组[1]链表 数组[2]链表
多线程同时put,链表成环、数据丢失(线程不安全)
3.3. ConcurrentHashMap JDK1.8 桶锁模型
数组下标独立加锁(链表头作为锁对象)
数组[0]🔒 数组[1] 数组[2]🔒 数组[3]
链表 红黑树 链表 链表
规则:只有操作同一个数组下标才竞争锁,不同下标并发读写互不阻塞;读不加锁,volatile 保证可见性,写 synchronized+CAS。
3.4. 线程安全与锁机制
-
HashMap 无同步锁,多线程并发 put 会出现数据丢失、链表循环死循环,禁止多线程直接使用。
-
Hashtable 所有
put()、get()、remove()全部加synchronized修饰,锁是当前 Hashtable 实例对象,整张哈希表只有一把锁。 任意线程操作任意位置数据,独占整张表,其他线程全部阻塞,并发性能极差。 -
ConcurrentHashMap (JDK1.8 重点)
- 取消 JDK1.7 的 Segment 分段锁;
- 数组每个桶位(链表首节点)作为锁,只锁当前下标链表;
- 查询:读无锁,
volatile修饰数组节点,保证读取最新数据; - 写入:CAS 占位 + 首节点 synchronized 加锁;
- 不同下标元素并发读写互不阻塞,大幅降低锁竞争。
3.5. Key、Value 空值规则
- HashMap:key 允许 1 个 null,value 可以多个 null;
- Hashtable:key=null/value=null 直接空指针报错;
- ConcurrentHashMap:key、value 都不允许 null。
3.6. 底层存储结构
- HashMap: 数组 + 单向链表,链表长度≥8 转为红黑树,≤6 退回链表。
- Hashtable: 纯数组 + 单向链表,无红黑树优化,哈希冲突多时查询效率 O (n)。
- ConcurrentHashMap: 和 HashMap 结构一致:数组 + 链表 / 红黑树,冲突过长树化。
3.7. 扩容机制
- HashMap:单线程一次性扩容,原数组全部拷贝到新数组;并发扩容容易死链。
- Hashtable:单线程独占锁全量迁移,扩容全程独占锁,其他线程阻塞等待,性能拉胯。
- ConcurrentHashMap:分段式多线程协同扩容
- 扩容时新、老数组共存;
- 访问元素的线程顺便帮忙迁移一小段数据;
- 插入只往新数组,查询同时查新 + 老数组;
- 多线程分担迁移任务,扩容效率极高。
3.8 应用场景
- HashMap:单线程场景(普通业务缓存、局部集合)
- Hashtable:基本淘汰,老项目遗留代码,新项目禁止使用
- ConcurrentHashMap:多线程高并发存储(接口缓存、全局 Map、线程安全缓存)
3.9.补充:JDK1.7 ConcurrentHashMap 简单拓展(面试偶尔问到)
JDK1.7 采用Segment 分段锁,把整个数组拆分成多个分段,一段一把锁,同段竞争、跨段并发,JDK1.8 废弃该设计改用桶锁。
Segment0🔒 Segment1🔒 Segment2🔒
子数组 子数组 子数组
总结
- 单线程选 HashMap,速度最快;
- 多线程高并发选 ConcurrentHashMap,分段桶锁兼顾安全与性能;
- Hashtable 全表上锁效率过低,已淘汰。
4. CopyOnWriteArrayList 原理与优缺点?
答案: 写时复制:新增 / 修改集合先复制新数组,修改新数组后替换原引用;读原数组不加锁。 优点:读多写少并发性能高;缺点:写入开销大、占用内存,新数据不能实时读取。
六、死锁相关面试题
1. 死锁产生四个必要条件,如何避免死锁?
答案: 四个必要条件必须同时满足才会死锁:
- 互斥:资源同一时间只能一个线程占用;
- 不可抢占:资源只能持有者主动释放,无法强行抢夺;
- 请求保持:持有旧锁同时申请新锁;
- 循环等待:多个线程循环互相持有对方需要的锁。 最常用方案:破坏循环等待,统一锁的获取顺序(从小到大编号依次加锁)。
七、Java 多线程综合杂项面试题
1. volatile 关键字作用?
答案: 保证变量内存可见性,修改后立刻刷新主存,读取强制从主存获取最新值;不保证原子性、不能替代锁。
2. Java 多线程如何实现数据共享?
JVM把内存分成了这几个区域:
方法区,堆区,栈区,程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中,就可以让多个线程都能访问到.
3. Java 线程有几种状态?
• NEW:安排了⼯作,还未开始行动.新创建的线程,还没有调用start方法时处在这个状态.
• RUNNABLE:可工作的.又可以分成正在⼯作中和即将开始⼯作.调用start方法之后,并正在CPU上 运行/在即将准备运行的状态.
• BLOCKED:使用synchronized的时候,如果锁被其他线程占用,就会阻塞等待,从而进入该状态。
• WAITING:调用wait方法会进⼊该状态.
• TIMED_WAITING:调用sleep方法或者wait(超时时间)会进入该状态。
• TERMINATED:工作完成了.当线程run方法执行完毕后,会处于这个状态.
4. 多次调用同一个线程 start () 方法会怎样?
答案: 首次 start 正常启动线程;重复调用抛出IllegalThreadStateException异常。
5. synchronized 修饰普通方法,两个对象调用会互斥吗?
答案: synchronized 非静态方法锁当前实例对象:
- 同一对象调用:互斥串行执行;
- 不同对象调用:两把独立锁,并发执行互不阻塞。
6. 多线程数值累加有几种方案?
答案:
- synchronized/ReentrantLock 加锁;
- AtomicInteger 原子类 CAS 无锁自增。
7. Thread 和 Runnable 区别?
答案: Thread 是线程载体类;Runnable 是任务接口,解耦线程与执行任务,同一个任务可以被多个线程共用。
8. Servlet 线程安全吗?
答案: 单实例多请求,成员变量多线程共享,存在线程安全问题;局部变量栈私有安全。
9. 进程和线程区别?
• 进程是包含线程的.每个进程至少有⼀个线程存在,即主线程。
• 进程和进程之间不共享内存空间.同⼀个进程的线程之间共享同⼀个内存空间.
• 进程是系统分配资源的最小单位,线程是系统调度的最小单位
文末总结
本文汇总 Java 多线程全场景面试真题,覆盖锁体系、CAS、Synchronized 底层、JUC 常用类、并发容器、死锁六大高频板块,也是日常开发中并发编码的理论基础。实际开发根据并发量、读写比例灵活选用乐观锁 / 悲观锁、线程池、并发集合,规避死锁与 ABA 等经典并发 bug。

309

被折叠的 条评论
为什么被折叠?



