Java 多线程高频面试题全解|锁、CAS、Synchronized、JUC、并发集合、死锁汇总

目录

  1. 前言
  2. 一、各类锁相关面试题汇总
  3. 二、CAS 机制相关面试题
  4. 三、Synchronized 面试问题
  5. 四、JUC 工具类(Callable、ReentrantLock、线程池、Semaphore、CountDownLatch)面试题
  6. 五、并发安全集合面试题
  7. 六、死锁相关面试题
  8. 七、Java 多线程综合杂项面试题
  9. 文末总结

前言

多线程是 Java 后端面试必考重难点,锁原理、CAS、Synchronized 底层、JUC 工具包、ConcurrentHashMap、死锁等问题频繁出现在校招与社招面试中。本文基于多线程进阶知识点,整理全量面试真题 + 标准答案,方便面试复习与日常查阅。

一、各类锁相关面试题汇总

1. 怎么理解乐观锁和悲观锁?如何实现?

答案: 悲观锁默认并发冲突概率很高,访问资源前必须加锁,其他线程阻塞等待,依托操作系统 mutex 实现。 乐观锁默认冲突概率低,不加锁直接访问数据,更新时校验冲突;冲突则报错交由业务处理,主流实现为版本号 + CAS。 Synchronized 初始采用乐观锁,竞争激烈自动转为悲观锁。

  • 悲观锁:synchronized、ReentrantLock;
  • 乐观锁:CAS + 版本号、数据库 version 字段。

2. 介绍读写锁原理与适用场景

答案: 读写锁区分读、写两种锁:

  1. 读锁与读锁:不互斥,多线程可并发读取;
  2. 写锁与写锁:互斥,同一时间只能一个线程写入;
  3. 读锁与写锁:互斥,读写不能同时进行。 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。

  1. 比较 V 和 A 是否一致;
  2. 一致则将 B 赋值给 V,返回 true;不一致修改失败返回 false。 是乐观锁底层实现,依托 CPU 硬件 lock 指令保证原子性,Unsafe 类提供底层 CAS 方法。

2. 什么是 ABA 问题?如何解决?

答案: 线程读取数据为 A,期间数据被其他线程 A→B→A,原线程 CAS 校验数值一致,误以为数据未改动,引发业务异常(如重复扣款)。 解决方案:数据 + 版本号,每次修改版本号自增,CAS 同时校验数值与版本,只要版本号增加了,就说明该数据已经被改了;Java 对应AtomicStampedReference

3.AtomicInteger实现原理

AtomicInteger 是 Java 提供的原子整型类,基于 CAS + 自旋 实现无锁线程安全,底层依赖 Unsafe 类提供的硬件级原子指令。

  1. 3.1value 使用 volatile 修饰
    • 保证多线程之间的内存可见性
    • 一个线程修改,其他线程立刻能看到最新值
  2. 3.2CAS(Compare And Swap)比较并交换
    • 包含三个参数:内存值 V、预期值 A、更新值 B
    • 逻辑:
      • 如果 V == A,说明没被修改,赋值 V = B
      • 如果 V != A,说明被修改过,更新失败
  3. 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 之后锁分级升级:无锁→偏向锁→轻量级锁→重量级锁(锁只能升级不能降级)。

  1. 偏向锁:单线程无竞争,标记线程 ID;
  2. 轻量级锁:少量竞争,CAS + 自适应自旋(用户态);
  3. 重量级锁:高并发竞争,依赖 OS 的 mutex,线程阻塞挂起(内核态切换,开销大)。 额外优化:锁消除(JVM 剔除无效锁)、锁粗化(合并频繁加解锁);特性:可重入、非公平、自适应升降级、自动释放锁。 字节码层面依靠 monitorenter、monitorexit 指令,对象头 Mark Word 存储所有锁标记。

四、JUC 工具类面试题(观看上一篇博客,比较详细)

1. synchronized 和 ReentrantLock 区别?为什么 JDK 还要新增 Lock 锁?

答案:

  1. 实现:synchronized 是 JVM 关键字 (C++ 底层),自动释放锁;ReentrantLock 是 Java API,需要手动 finally 解锁;
  2. 抢锁:synchronized 阻塞死等;Lock 支持 tryLock 超时放弃获取;
  3. 公平:synchronized 固定非公平;Lock 可自定义公平 / 非公平;
  4. 等待唤醒:synchronized 依靠 wait/notify 随机唤醒;Lock 配合 Condition 精准唤醒指定线程。 竞争激烈、需要公平锁、限时等待场景优先使用 ReentrantLock。

2. Callable 和 Runnable 区别,FutureTask 作用?

答案: Runnable 无返回值、不能抛出受检异常;Callable 带泛型返回值、可抛出异常。 FutureTask 包装 Callable,用于保存异步任务结果,get () 阻塞等待线程执行结束获取返回值。

3. ThreadPoolExecutor 七大参数含义?

答案:

  1. corePoolSize:核心线程数(常驻线程,不会被回收);
  2. maximumPoolSize:最大线程数 = 核心 + 临时线程;
  3. keepAliveTime:临时线程空闲超时时间;
  4. unit:时间单位;
  5. workQueue:阻塞任务队列,存等待执行的任务;
  6. threadFactory:线程工厂,创建线程;
  7. handler:拒绝策略,任务满载后的处理规则。 拒绝策略四种:Abort 抛异常、CallerRuns 调用线程执行、DiscardOldest 丢弃队首任务、Discard 丢弃新任务。

4. Executors 创建线程池四种方式?

答案:

  1. newFixedThreadPool:固定线程数量;
  2. newCachedThreadPool:线程动态扩容,空闲自动回收;
  3. newSingleThreadExecutor:单线程池;
  4. 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 三者区别?

特性HashMapHashtableConcurrentHashMap(JDK1.8)
线程安全不安全安全安全
加锁位置无锁锁整个对象(数组 + 全集合一把锁)锁单个链表头(桶锁),分段淘汰
null 键 / 值key、value 都允许 nullkey、value 都不允许 nullkey 不能 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. 线程安全与锁机制
  1. HashMap 无同步锁,多线程并发 put 会出现数据丢失、链表循环死循环,禁止多线程直接使用

  2. Hashtable 所有put()、get()、remove()全部加synchronized修饰,锁是当前 Hashtable 实例对象,整张哈希表只有一把锁。 任意线程操作任意位置数据,独占整张表,其他线程全部阻塞,并发性能极差。

  3. 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. 底层存储结构
  1. HashMap: 数组 + 单向链表,链表长度≥8 转为红黑树,≤6 退回链表。
  2. Hashtable: 纯数组 + 单向链表,无红黑树优化,哈希冲突多时查询效率 O (n)。
  3. ConcurrentHashMap: 和 HashMap 结构一致:数组 + 链表 / 红黑树,冲突过长树化。
3.7. 扩容机制
  1. HashMap:单线程一次性扩容,原数组全部拷贝到新数组;并发扩容容易死链。
  2. Hashtable:单线程独占锁全量迁移,扩容全程独占锁,其他线程阻塞等待,性能拉胯。
  3. ConcurrentHashMap:分段式多线程协同扩容
  • 扩容时新、老数组共存;
  • 访问元素的线程顺便帮忙迁移一小段数据;
  • 插入只往新数组,查询同时查新 + 老数组;
  • 多线程分担迁移任务,扩容效率极高。
3.8 应用场景
  1. HashMap:单线程场景(普通业务缓存、局部集合)
  2. Hashtable:基本淘汰,老项目遗留代码,新项目禁止使用
  3. ConcurrentHashMap:多线程高并发存储(接口缓存、全局 Map、线程安全缓存)
3.9.补充:JDK1.7 ConcurrentHashMap 简单拓展(面试偶尔问到)

JDK1.7 采用Segment 分段锁,把整个数组拆分成多个分段,一段一把锁,同段竞争、跨段并发,JDK1.8 废弃该设计改用桶锁。

Segment0🔒  Segment1🔒 Segment2🔒
 子数组       子数组     子数组
总结
  • 单线程选 HashMap,速度最快;
  • 多线程高并发选 ConcurrentHashMap,分段桶锁兼顾安全与性能;
  • Hashtable 全表上锁效率过低,已淘汰。

4. CopyOnWriteArrayList 原理与优缺点?

答案: 写时复制:新增 / 修改集合先复制新数组,修改新数组后替换原引用;读原数组不加锁。 优点:读多写少并发性能高;缺点:写入开销大、占用内存,新数据不能实时读取。

六、死锁相关面试题

1. 死锁产生四个必要条件,如何避免死锁?

答案: 四个必要条件必须同时满足才会死锁:

  1. 互斥:资源同一时间只能一个线程占用;
  2. 不可抢占:资源只能持有者主动释放,无法强行抢夺;
  3. 请求保持:持有旧锁同时申请新锁;
  4. 循环等待:多个线程循环互相持有对方需要的锁。 最常用方案:破坏循环等待,统一锁的获取顺序(从小到大编号依次加锁)。

七、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. 多线程数值累加有几种方案?

答案:

  1. synchronized/ReentrantLock 加锁;
  2. AtomicInteger 原子类 CAS 无锁自增。

7. Thread 和 Runnable 区别?

答案: Thread 是线程载体类;Runnable 是任务接口,解耦线程与执行任务,同一个任务可以被多个线程共用。

8. Servlet 线程安全吗?

答案: 单实例多请求,成员变量多线程共享,存在线程安全问题;局部变量栈私有安全。

9. 进程和线程区别?

• 进程是包含线程的.每个进程至少有⼀个线程存在,即主线程。
• 进程和进程之间不共享内存空间.同⼀个进程的线程之间共享同⼀个内存空间.
• 进程是系统分配资源的最小单位,线程是系统调度的最小单位

文末总结

本文汇总 Java 多线程全场景面试真题,覆盖锁体系、CAS、Synchronized 底层、JUC 常用类、并发容器、死锁六大高频板块,也是日常开发中并发编码的理论基础。实际开发根据并发量、读写比例灵活选用乐观锁 / 悲观锁、线程池、并发集合,规避死锁与 ABA 等经典并发 bug。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值