目录
[1. 线程的 4 种创建方式](#1. 线程的 4 种创建方式)
[2. 线程的 6 种状态与流转逻辑](#2. 线程的 6 种状态与流转逻辑)
[3. start () 与 run () 的核心区别](#3. start () 与 run () 的核心区别)
[4. 线程优先级与守护线程](#4. 线程优先级与守护线程)
[1. synchronized 关键字](#1. synchronized 关键字)
[2. volatile 关键字](#2. volatile 关键字)
[3. 原子类 Atomic 与 CAS 原理](#3. 原子类 Atomic 与 CAS 原理)
[1. 线程间通信的 3 种方式](#1. 线程间通信的 3 种方式)
[2. JUC 三大并发工具类](#2. JUC 三大并发工具类)
[3. ReentrantLock 与 synchronized 的核心区别](#3. ReentrantLock 与 synchronized 的核心区别)
[模块四:线程池(面试 100% 必问,重中之重)](#模块四:线程池(面试 100% 必问,重中之重))
[1. ThreadPoolExecutor 7 大核心参数](#1. ThreadPoolExecutor 7 大核心参数)
[2. 线程池完整执行流程](#2. 线程池完整执行流程)
[3. 4 种拒绝策略](#3. 4 种拒绝策略)
[4. JDK 自带 4 种线程池的弊端(绝对禁止使用)](#4. JDK 自带 4 种线程池的弊端(绝对禁止使用))
[5. 线程池参数配置最佳实践](#5. 线程池参数配置最佳实践)
[多线程与并发编程 面试模拟题(实习面试专属,附标准答案)](#多线程与并发编程 面试模拟题(实习面试专属,附标准答案))
[一、基础必考题(8 道,中小厂实习 100% 覆盖)](#一、基础必考题(8 道,中小厂实习 100% 覆盖))
[1. Java 中线程有哪 4 种创建方式?各自的优缺点是什么?](#1. Java 中线程有哪 4 种创建方式?各自的优缺点是什么?)
[2. start () 和 run () 方法的核心区别是什么?](#2. start () 和 run () 方法的核心区别是什么?)
[3. 线程的 6 种生命周期状态是什么?完整流转逻辑是怎样的?](#3. 线程的 6 种生命周期状态是什么?完整流转逻辑是怎样的?)
[4. 简述 synchronized 的锁升级完整流程(JDK6+)](#4. 简述 synchronized 的锁升级完整流程(JDK6+))
[5. volatile 关键字的三大特性是什么?为什么不保证原子性?](#5. volatile 关键字的三大特性是什么?为什么不保证原子性?)
[6. CAS 的原理是什么?ABA 问题是什么?怎么解决?](#6. CAS 的原理是什么?ABA 问题是什么?怎么解决?)
[7. CountDownLatch 和 CyclicBarrier 的核心区别是什么?](#7. CountDownLatch 和 CyclicBarrier 的核心区别是什么?)
[8. 线程池的 7 大核心参数是什么?完整执行流程是怎样的?4 种拒绝策略分别是什么?](#8. 线程池的 7 大核心参数是什么?完整执行流程是怎样的?4 种拒绝策略分别是什么?)
[二、场景坑点题(6 道,大厂实习高频业务题 / 代码题)](#二、场景坑点题(6 道,大厂实习高频业务题 / 代码题))
[1. 你的校园二手平台用 volatile 修饰 int 类型的商品浏览量,多线程并发下viewCount++计数不准,是什么原因?怎么解决?](#1. 你的校园二手平台用 volatile 修饰 int 类型的商品浏览量,多线程并发下viewCount++计数不准,是什么原因?怎么解决?)
[2. 你的校园二手平台商品扣减库存,怎么保证线程安全,避免超卖?分别说明单机和分布式场景的实现方案。](#2. 你的校园二手平台商品扣减库存,怎么保证线程安全,避免超卖?分别说明单机和分布式场景的实现方案。)
[3. 你的项目用Executors.newFixedThreadPool(10)做异步任务,运行一段时间后出现 OOM,是什么原因?怎么解决?](#3. 你的项目用Executors.newFixedThreadPool(10)做异步任务,运行一段时间后出现 OOM,是什么原因?怎么解决?)
[4. 以下代码实现订单超时取消,有什么问题?为什么?](#4. 以下代码实现订单超时取消,有什么问题?为什么?)
[5. 你的校园二手平台要实现商品批量导入功能,10 个线程分别导入不同分类的商品,主线程需要等待所有线程导入完成后,返回导入结果,应该用哪个并发工具类?为什么?](#5. 你的校园二手平台要实现商品批量导入功能,10 个线程分别导入不同分类的商品,主线程需要等待所有线程导入完成后,返回导入结果,应该用哪个并发工具类?为什么?)
[6. 以下用 ReentrantLock 实现库存扣减的代码有什么坑?怎么修复?](#6. 以下用 ReentrantLock 实现库存扣减的代码有什么坑?怎么修复?)
[三、进阶原理题(4 道,中大厂实习拔高题)](#三、进阶原理题(4 道,中大厂实习拔高题))
[1. DCL(双重检查锁)实现单例,为什么必须给单例对象加 volatile?](#1. DCL(双重检查锁)实现单例,为什么必须给单例对象加 volatile?)
[2. synchronized 和 ReentrantLock 的核心区别是什么?各自的适用场景是什么?](#2. synchronized 和 ReentrantLock 的核心区别是什么?各自的适用场景是什么?)
[3. AQS(AbstractQueuedSynchronizer)的核心原理是什么?](#3. AQS(AbstractQueuedSynchronizer)的核心原理是什么?)
[4. 线程池的核心线程数怎么配置?CPU 密集型和 IO 密集型任务分别怎么设置?](#4. 线程池的核心线程数怎么配置?CPU 密集型和 IO 密集型任务分别怎么设置?)
模块一:线程基础(面试打底必问)
1. 线程的 4 种创建方式
核心定义
Java 中线程创建有 4 种标准方式,本质都是基于Thread类实现:
- 继承 Thread 类 :重写
run()方法,实例化子类调用start()启动; - 实现 Runnable 接口 :重写
run()方法,将实例传入 Thread 对象启动,解耦线程任务和线程对象; - 实现 Callable+FutureTask :重写
call()方法,支持返回值、抛出异常,通过 FutureTask 获取执行结果; - 线程池创建 :通过
ThreadPoolExecutor提交任务,项目开发唯一推荐方式,避免手动创建线程的资源浪费。
个人理解
继承 Thread 类受 Java 单继承限制,不推荐;Runnable 无返回值,Callable 有返回值;手动创建线程缺乏统一管理,容易造成线程泛滥、OOM,企业级项目 100% 用线程池创建线程。
项目实际使用场景
结合校园二手平台开发实践:
- 异步业务场景:商品详情页浏览量统计、订单支付成功后的短信 / 站内信通知、商品图片异步压缩,全部通过自定义线程池提交任务,不手动 new Thread;
- 有返回值的异步任务:批量查询商品的销量、评价数,用 Callable+FutureTask 异步查询,聚合结果后返回给前端,降低接口响应时间;
- 避坑实践:项目中绝对禁止手动创建线程,所有异步任务统一交给线程池管理,避免线程数过多导致服务器 OOM。
面试考点标注
✅ 必问:4 种线程创建方式的优缺点对比;
✅ 细节:Runnable 和 Callable 的核心区别(有无返回值、是否抛异常);
✅ 原则:为什么项目中推荐用线程池,而不是手动创建线程。
2. 线程的 6 种状态与流转逻辑
核心定义
JVM 定义了线程的 6 种生命周期状态,流转逻辑如下:
| 状态 | 定义 | 触发场景 |
|---|---|---|
| NEW(新建) | 线程对象创建完成,还未调用 start () | new Thread () 后 |
| RUNNABLE(可运行) | 线程在 JVM 中执行,或等待 CPU 调度 | 调用 start () 后,包含就绪和运行两个子状态 |
| BLOCKED(阻塞) | 线程阻塞等待锁释放 | 等待 synchronized 锁 |
| WAITING(无限等待) | 线程无限等待其他线程唤醒 | 调用 wait ()、join ()、LockSupport.park () |
| TIMED_WAITING(计时等待) | 线程等待指定时间后自动唤醒 | Thread.sleep()、wait(time)、LockSupport.parkNanos() |
| TERMINATED(终止) | 线程执行完成,生命周期结束 | run () 执行完毕、异常退出 |
个人理解
线程状态流转是并发编程的基础,核心是理解不同等待状态的触发和唤醒条件,是排查并发问题的核心依据;BLOCKED 是等待锁,WAITING 是等待唤醒,TIMED_WAITING 是带超时的等待。
项目实际使用场景
- 订单超时取消:订单创建后,线程进入 TIMED_WAITING,超时后自动唤醒执行取消订单、释放库存;
- 分布式锁等待:线程获取分布式锁失败,进入 BLOCKED 状态等待锁释放;
- 多线程任务聚合:主线程调用子线程的 join () 方法,进入 WAITING 状态,等待所有子线程执行完成后再继续。
面试考点标注
✅ 必问:线程的 6 种状态和完整流转逻辑;
✅ 必问:wait () 和 sleep () 的核心区别;
✅ 对比:BLOCKED 和 WAITING 的区别。
3. start () 与 run () 的核心区别
核心定义
- start() :启动线程的正确方法,调用后线程进入 RUNNABLE 状态,由 JVM 调度执行
run()方法,真正实现了多线程,一个线程只能调用一次 start (); - run() :线程的业务方法,只是普通的对象方法,直接调用会在当前线程中执行,不会启动新线程,可以多次调用。
个人理解
这是多线程最基础的坑点,核心区别是「是否真正启动了新线程」,面试常考场景题,给出代码判断是否是多线程执行。
项目实际使用场景
项目中所有线程启动都是通过线程池的execute()/submit()方法,底层就是调用线程的 start () 方法,绝对不会直接调用 run () 方法。
面试考点标注
✅ 必问:start () 和 run () 的核心区别;
✅ 场景题:直接调用 run () 方法会不会启动新线程?
4. 线程优先级与守护线程
核心定义
- 线程优先级:Java 线程优先级范围 1-10,默认 5,优先级越高的线程获取 CPU 调度的概率越高,不是绝对优先;
- 守护线程(Daemon Thread):后台服务线程,是用户线程的保姆,当所有用户线程都结束后,守护线程会自动销毁;典型例子:GC 垃圾回收线程。
规则:线程默认是用户线程,调用
setDaemon(true)设置为守护线程,必须在 start () 之前设置;守护线程创建的子线程默认也是守护线程。
个人理解
线程优先级只是给 CPU 的提示,不能依赖优先级控制业务逻辑,不同操作系统的优先级实现不同;守护线程不能用来执行核心业务,因为会随着用户线程结束而突然终止。
项目实际使用场景
- 后台监控线程:项目的 JVM 监控、日志上报线程,设置为守护线程,不影响主业务退出;
- 避坑实践:订单、支付、库存等核心业务线程,绝对不能设置为守护线程,避免业务执行到一半被终止。
面试考点标注
✅ 必问:守护线程的特点和使用场景;
✅ 坑点:守护线程的 finally 代码块不一定会执行(用户线程结束后 JVM 直接退出)。
模块二:线程安全核心(面试难点核心)
1. synchronized 关键字
核心定义
synchronized 是 JVM 层面的内置锁,可重入,自动加锁释放,不会死锁,JDK6 之后做了大量锁优化,核心特性:
(1)锁升级过程(JDK6+,面试 100% 必问)
无锁 → 偏向锁 → 轻量级锁 → 重量级锁,锁只能升级不能降级:
- 无锁:对象刚创建,没有线程竞争;
- 偏向锁:第一个线程访问锁,将线程 ID 记录在对象头,后续该线程访问无需加锁解锁,无竞争场景性能极高;
- 轻量级锁:多线程轻度竞争,偏向锁撤销,线程通过 CAS 自旋获取锁,不阻塞线程,性能高;
- 重量级锁:竞争激烈,自旋超过次数,升级为重量级锁,基于操作系统的互斥量,线程阻塞,性能低。
(2)对象锁与类锁的区别
- 对象锁:锁的是实例对象,锁的是非静态方法、this 对象,不同实例的对象锁互不影响;
- 类锁:锁的是类的 Class 对象,锁的是静态方法、类名.class 对象,全局唯一,所有实例共享同一把锁。
(3)锁优化
- 锁消除:JVM 判断没有共享数据竞争,自动消除锁;
- 锁粗化:JVM 把连续的加锁解锁合并成一个大锁,减少频繁加锁解锁的开销。
个人理解
synchronized 是开发中最常用的锁,JDK 优化后性能和 ReentrantLock 几乎一致,不需要手动释放锁,不会出现死锁,优先使用;锁升级是 JVM 的性能优化,无竞争、轻度竞争场景下性能极高,只有高竞争才会升级为重量级锁。
项目实际使用场景
- 商品库存扣减:单机场景下,扣减库存的方法加 synchronized 对象锁,保证超卖;
- 用户手机号唯一性校验:注册接口加 synchronized 类锁,全局唯一,避免并发注册同一个手机号;
- 避坑实践:锁的粒度要尽可能小,只锁需要同步的代码块,不要锁整个方法,提升并发性能。
面试考点标注
✅ 必问:synchronized 的锁升级完整流程;
✅ 必问:对象锁和类锁的区别;
✅ 原理:synchronized 的可重入性原理;
✅ 对比:synchronized 和 Lock 的区别。
2. volatile 关键字
核心定义
volatile 是 JVM 提供的轻量级同步机制,三大特性:
- 可见性:一个线程修改了 volatile 变量的值,其他线程能立即看到最新值,底层通过 MESI 缓存一致性协议 + 内存屏障实现;
- 禁止指令重排:通过内存屏障禁止编译器和 CPU 对指令进行重排序,保证有序性;
- 不保证原子性:不能保证复合操作(比如 i++)的原子性,这是 volatile 和 synchronized 最核心的区别。
个人理解
volatile 是轻量级的同步机制,只能保证可见性和有序性,不能保证原子性,适合一写多读的场景,是实现 DCL 单例、服务开关的核心。
项目实际使用场景
- 服务开关配置:项目的接口限流开关、灰度发布开关,用 volatile 修饰,后台修改后所有线程立即生效,不需要加锁;
- 双重检查锁(DCL)单例:项目的单例线程池、单例工具类,用 volatile 修饰单例对象,禁止指令重排,避免半初始化对象;
- 避坑实践:计数场景(i++)绝对不能用 volatile,必须用原子类或者 synchronized,因为 volatile 不保证原子性,并发下会出现计数错误。
面试考点标注
✅ 必问:volatile 的三大特性,为什么不保证原子性?
✅ 必问:DCL 单例为什么要加 volatile?
✅ 原理:volatile 的可见性实现原理(内存屏障、MESI 协议);
✅ 必问:synchronized 和 volatile 的核心区别。
3. 原子类 Atomic 与 CAS 原理
核心定义
(1)CAS(Compare And Swap,比较并交换)
是原子类的底层原理,是 CPU 级别的原子指令,核心逻辑:
- 内存值 V、预期值 A、要更新的值 B;
- 只有当 V == A 时,才把 V 更新为 B,否则什么都不做,整个操作是原子的;
- Java 中通过 Unsafe 类的 native 方法实现,无锁,性能远高于加锁。
(2)ABA 问题与解决方案
- 问题:一个值从 A 改成 B,又改回 A,CAS 会认为没有变化,实际已经被修改过;
- 解决方案:加版本号,用
AtomicStampedReference,每次修改更新版本号,CAS 同时比较值和版本号。
(3)原子类分类
- 基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean;
- 引用类型原子类:AtomicReference、AtomicStampedReference;
- 字段更新原子类:AtomicIntegerFieldUpdater。
个人理解
CAS 是无锁编程的核心,适合高并发下的计数、统计场景,性能比加锁高几个数量级;ABA 问题在绝大多数业务场景下不影响,只有需要记录修改过程的场景才需要解决。
项目实际使用场景
- 接口限流计数器:接口的请求次数统计,用 AtomicInteger,线程安全,无锁,性能高;
- 商品浏览量、收藏量统计:高并发下的计数,用 AtomicLong,避免加锁的性能开销;
- 避坑实践:高并发计数场景,优先用原子类,不要用 volatile 或者 synchronized,性能最优。
面试考点标注
✅ 必问:CAS 的原理是什么?有什么缺点?
✅ 必问:ABA 问题是什么?怎么解决?
✅ 对比:原子类和 synchronized 的性能对比、适用场景。
模块三:线程间通信与并发工具类
1. 线程间通信的 3 种方式
核心定义
| 通信方式 | 所属类 | 使用条件 | 核心特点 |
|---|---|---|---|
| wait()/notify()/notifyAll() | Object 类 | 必须在 synchronized 同步块中使用 | wait 会释放锁,notify 随机唤醒一个线程,先 wait 再 notify 才有效 |
| LockSupport.park()/unpark() | LockSupport 类 | 不需要加锁 | 基于许可,先 unpark 再 park 也不会死锁,不会释放锁 |
| Condition.await()/signal() | Condition 接口 | 必须在 Lock 锁中使用 | 支持多个条件队列,精准唤醒指定线程 |
个人理解
wait/notify 是最基础的,但是必须加锁,容易死锁;LockSupport 最灵活,不需要加锁,是 JUC 包的底层实现;Condition 支持精准唤醒,适合复杂的生产者消费者场景。
项目实际使用场景
- 订单支付回调:支付成功后,用 Condition 精准唤醒等待支付结果的订单线程,继续执行后续逻辑;
- 多线程批量处理商品:主线程用 CountDownLatch 等待所有子线程处理完成,本质是基于 LockSupport 实现的。
面试考点标注
✅ 必问:wait () 和 sleep () 的核心区别:
| 对比维度 | wait() | sleep() |
|---|---|---|
| 所属类 | Object 类 | Thread 类 |
| 锁释放 | 会释放锁 | 不会释放锁 |
| 使用条件 | 必须在 synchronized 中 | 不需要加锁 |
| 唤醒方式 | 等待 notify / 超时 | 等待超时 / 中断 |
2. JUC 三大并发工具类
核心定义
| 工具类 | 核心作用 | 特点 | 适用场景 |
|---|---|---|---|
| CountDownLatch(倒计时器) | 一个线程等待 N 个线程执行完成,再继续执行 | 一次性,计数到 0 就不能再用 | 批量任务聚合、多线程查询结果合并 |
| CyclicBarrier(循环栅栏) | N 个线程互相等待,所有线程都到达栅栏后,再一起执行 | 可复用,计数到 0 后重置 | 多线程数据校验、并行计算 |
| Semaphore(信号量) | 控制同时访问资源的最大线程数 | 可控制并发度 | 接口限流、资源池控制 |
个人理解
CountDownLatch 是主线程等子线程,CyclicBarrier 是子线程互相等,Semaphore 是控制并发数,三个工具类是高并发开发的常用工具,面试常考场景选型。
项目实际使用场景
- 商品批量导入:用 CountDownLatch,主线程等待所有导入线程处理完成,再返回导入结果;
- 订单批量导出:用 CyclicBarrier,所有导出线程都读取完数据后,一起执行数据校验和合并;
- 图片上传接口限流:用 Semaphore,控制同时上传的最大线程数,避免服务器带宽被打满。
面试考点标注
✅ 必问:CountDownLatch 和 CyclicBarrier 的核心区别;
✅ 场景题:三个工具类分别适合什么场景。
3. ReentrantLock 与 synchronized 的核心区别
核心定义
ReentrantLock 是 JDK 提供的 API 层面的可重入锁,和 synchronized 核心区别:
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 层面,内置锁 | JDK API 层面,基于 AQS |
| 锁释放 | 自动释放,异常也会释放 | 必须手动在 finally 中释放,否则会死锁 |
| 可中断 | 不可中断 | 支持中断等待锁的线程 |
| 公平锁 | 只能非公平锁 | 支持公平锁 / 非公平锁(默认非公平) |
| 条件队列 | 只有一个条件队列 | 支持多个 Condition 条件队列,精准唤醒 |
| 性能 | JDK6 优化后几乎一致 | 高竞争场景下略优 |
个人理解
synchronized 足够应对 90% 的场景,代码更安全,不会漏释放锁;只有需要公平锁、可中断、多条件唤醒的特殊场景,才用 ReentrantLock。
项目实际使用场景
- 用户下单公平队列:下单接口用 ReentrantLock 的公平锁,保证用户请求按顺序处理,避免插队;
- 生产者消费者模型:用 ReentrantLock 的两个 Condition,分别控制生产者队列和消费者队列,精准唤醒。
面试考点标注
✅ 必问:ReentrantLock 和 synchronized 的核心区别,各自的适用场景;
✅ 原理:ReentrantLock 的可重入性、公平锁实现原理(基于 AQS)。
模块四:线程池(面试 100% 必问,重中之重)
1. ThreadPoolExecutor 7 大核心参数
核心定义
自定义线程池必须通过ThreadPoolExecutor创建,7 大核心参数:
| 参数 | 定义 | 说明 |
|---|---|---|
| corePoolSize | 核心线程数 | 线程池长期保留的线程数,即使空闲也不会销毁 |
| maximumPoolSize | 最大线程数 | 线程池能创建的最大线程数 |
| keepAliveTime | 空闲线程存活时间 | 非核心线程空闲超过这个时间会被销毁 |
| unit | 时间单位 | keepAliveTime 的时间单位 |
| workQueue | 阻塞队列 | 核心线程满了之后,任务存入阻塞队列 |
| threadFactory | 线程工厂 | 创建线程的工厂,用来设置线程名、优先级、守护线程属性 |
| handler | 拒绝策略 | 线程池满了之后,对新任务的处理策略 |
2. 线程池完整执行流程
- 提交新任务:
- 核心线程数 < corePoolSize:创建新的核心线程执行任务;
- 核心线程数已满:任务加入阻塞队列 workQueue;
- 阻塞队列已满:创建非核心线程执行任务,直到线程数达到 maximumPoolSize;
- 线程数达到 maximumPoolSize:执行拒绝策略 handler。
3. 4 种拒绝策略
| 拒绝策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException 异常 | 核心业务,不允许任务丢失,提前暴露问题 |
| CallerRunsPolicy | 由提交任务的线程(主线程)执行任务 | 不允许任务丢失,流量平缓的场景 |
| DiscardOldestPolicy | 丢弃队列中最老的任务,执行当前任务 | 非核心业务,允许丢弃旧任务 |
| DiscardPolicy | 直接丢弃当前任务,不抛异常 | 非核心、不重要的任务 |
4. JDK 自带 4 种线程池的弊端(绝对禁止使用)
| 自带线程池 | 核心问题 | 风险 |
|---|---|---|
| FixedThreadPool | 核心线程数 = 最大线程数,无界阻塞队列 LinkedBlockingQueue | 队列无限增长,OOM |
| SingleThreadExecutor | 单线程,无界阻塞队列 | 队列无限增长,OOM |
| CachedThreadPool | 最大线程数 Integer.MAX_VALUE,同步队列 | 无限创建线程,OOM |
| ScheduledThreadPool | 最大线程数 Integer.MAX_VALUE,延迟队列 | 无限创建线程,OOM |
阿里开发规范强制要求:禁止用 Executors 创建线程池,必须通过 ThreadPoolExecutor 自定义。
5. 线程池参数配置最佳实践
核心配置原则
- CPU 密集型任务(计算、加密、压缩):corePoolSize = CPU 核心数 + 1,避免线程上下文切换开销;
- IO 密集型任务(数据库、Redis、网络请求,90% 业务场景):corePoolSize = CPU 核心数 * 2 或者 CPU 核心数 / (1 - 阻塞系数),阻塞系数一般 0.8-0.9;
- 阻塞队列必须用有界队列:ArrayBlockingQueue,指定队列容量,避免 OOM;
- 自定义线程工厂 :设置线程名前缀,方便排查问题,比如
goods-async-thread-。
个人理解
线程池是高并发开发的核心,参数配置没有固定值,要根据业务场景压测调整,核心是避免 OOM、控制并发度、提升资源利用率。
项目实际使用场景
- 通用异步任务线程池 :校园二手平台的浏览量统计、短信通知、图片压缩等 IO 密集型任务,自定义线程池:
- corePoolSize=8,maximumPoolSize=16,keepAliveTime=60s;
- 阻塞队列:ArrayBlockingQueue (1024),有界队列;
- 线程工厂:自定义线程名
goods-async-thread-; - 拒绝策略:CallerRunsPolicy,保证任务不丢失;
- 订单定时任务线程池:订单超时取消、自动确认收货,自定义 Scheduled 线程池,核心线程数 4,有界队列,避免 OOM。
面试考点标注
✅ 必问:线程池的 7 大核心参数、完整执行流程;
✅ 必问:4 种拒绝策略的区别;
✅ 必问:为什么不能用 Executors 创建线程池?有什么风险?
✅ 场景题:线程池参数怎么配置?CPU 密集型和 IO 密集型怎么设置?
当日验收清单
- 不看资料口述:线程 6 种状态流转图、线程池完整执行流程、synchronized 的锁升级过程;
- 结合项目口述:你的项目哪些接口用了线程池做异步处理,参数怎么配置的,哪些场景需要保证线程安全;
- 避坑确认:volatile 不保证原子性的坑、ReentrantLock 必须在 finally 释放锁、Executors 创建线程池的 OOM 风险。
多线程与并发编程 面试模拟题(实习面试专属,附标准答案)
一、基础必考题(8 道,中小厂实习 100% 覆盖)
1. Java 中线程有哪 4 种创建方式?各自的优缺点是什么?
【标准答案】
| 创建方式 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 继承 Thread 类 | 继承 Thread,重写 run () | 代码简单,直接操作线程 | 受 Java 单继承限制,无法继承其他类,任务和线程耦合 |
| 实现 Runnable 接口 | 实现 Runnable,重写 run (),传入 Thread | 解耦任务和线程,可多线程共享同一个任务 | 无返回值,无法抛出异常 |
| 实现 Callable+FutureTask | 实现 Callable,重写 call (),包装为 FutureTask | 支持返回值,支持抛出异常 | 代码略复杂 |
| 线程池创建 | ThreadPoolExecutor 提交任务 | 统一管理线程,复用线程,避免资源浪费,控制并发度 | 需要合理配置参数 |
企业级项目最佳实践:禁止手动创建线程,100% 使用自定义线程池。
【考点对应】模块一:线程创建方式
2. start () 和 run () 方法的核心区别是什么?
【标准答案】
- 本质区别 :
start():真正启动新线程的方法,调用后线程进入 RUNNABLE 状态,由 JVM 调度执行run()方法,实现了多线程并行执行;run():只是普通的业务方法,直接调用会在当前线程中同步执行,不会启动新线程,没有多线程效果。
- 调用限制 :一个线程只能调用一次
start(),多次调用会抛IllegalThreadStateException;run()可以多次调用。
【考点对应】模块一:start () 与 run () 核心区别
3. 线程的 6 种生命周期状态是什么?完整流转逻辑是怎样的?
【标准答案】
6 种状态定义
- NEW(新建):线程对象创建完成,未调用 start ();
- RUNNABLE(可运行):调用 start () 后,包含就绪(等待 CPU 调度)和运行(CPU 执行)两个子状态;
- BLOCKED(阻塞):等待 synchronized 锁释放;
- WAITING(无限等待):调用 wait ()、join ()、LockSupport.park (),等待其他线程唤醒;
- TIMED_WAITING(计时等待):调用 sleep ()、wait (time)、parkNanos (),超时自动唤醒;
- TERMINATED(终止):线程执行完成或异常退出,生命周期结束。
核心流转逻辑
NEW → RUNNABLE ↔ BLOCKED/WAITING/TIMED_WAITING → TERMINATED
【考点对应】模块一:线程状态与流转
4. 简述 synchronized 的锁升级完整流程(JDK6+)
【标准答案】锁升级是 JVM 为了优化 synchronized 性能的机制,锁只能升级不能降级,完整流程:
- 无锁:对象刚创建,无线程竞争,对象头标记为无锁状态;
- 偏向锁:第一个线程访问锁,将线程 ID 记录在对象头的 Mark Word 中,后续该线程访问无需加锁解锁,无竞争场景性能极高;
- 轻量级锁:多线程轻度竞争,偏向锁撤销,线程通过 CAS 自旋获取锁,不阻塞线程,避免用户态内核态切换;
- 重量级锁:竞争激烈,自旋超过次数(默认 10 次),升级为重量级锁,基于操作系统的互斥量实现,线程阻塞,性能较低。
【考点对应】模块二:synchronized 锁优化
5. volatile 关键字的三大特性是什么?为什么不保证原子性?
【标准答案】
三大特性
- 可见性:一个线程修改 volatile 变量,其他线程能立即看到最新值,底层通过内存屏障 + MESI 缓存一致性协议实现;
- 有序性:通过内存屏障禁止指令重排序;
- 不保证原子性:无法保证复合操作(如 i++)的原子性。
为什么不保证原子性
复合操作(比如 i++)分为 3 步:读取 i 的值、+1、写回主存;volatile 只能保证每一步的可见性,多线程并发下,多个线程同时读取到同一个值,分别 + 1 写回,会出现覆盖,最终计数小于预期。
【考点对应】模块二:volatile 核心特性
6. CAS 的原理是什么?ABA 问题是什么?怎么解决?
【标准答案】
CAS 原理
CAS(Compare And Swap,比较并交换)是 CPU 级别的原子指令,核心逻辑:
- 包含三个值:内存值 V、预期值 A、更新值 B;
- 只有当内存值 V == 预期值 A 时,才将 V 更新为 B,否则重试,整个操作是原子的,无锁,性能远高于加锁。
ABA 问题
一个值从 A 改成 B,又改回 A,CAS 会认为值没有变化,实际已经被修改过,在需要记录修改过程的场景会出问题。
解决方案
加版本号,使用AtomicStampedReference,每次修改同时更新版本号,CAS 时同时比较值和版本号,只要版本号变化就认为被修改过。
【考点对应】模块二:CAS 与原子类
7. CountDownLatch 和 CyclicBarrier 的核心区别是什么?
【标准答案】
| 对比维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 核心作用 | 一个主线程等待 N 个子线程全部执行完成,再继续执行 | N 个线程互相等待,所有线程都到达栅栏后,一起执行后续逻辑 |
| 计数特性 | 一次性,计数到 0 就无法再使用 | 可循环复用,计数到 0 后自动重置,可重复使用 |
| 执行动作 | 无内置回调动作 | 支持传入 Runnable 回调,所有线程到达后执行 |
| 适用场景 | 批量任务结果聚合、多线程查询合并 | 多线程并行计算、数据一致性校验 |
【考点对应】模块三:并发工具类
8. 线程池的 7 大核心参数是什么?完整执行流程是怎样的?4 种拒绝策略分别是什么?
【标准答案】
7 大核心参数
corePoolSize:核心线程数,长期保留的线程数;maximumPoolSize:最大线程数,线程池能创建的最大线程数;keepAliveTime:非核心线程空闲存活时间;unit:时间单位;workQueue:阻塞队列,核心线程满了之后任务存入队列;threadFactory:线程工厂,自定义线程名、优先级;handler:拒绝策略,线程池满了之后处理新任务的策略。
完整执行流程
- 新任务提交,核心线程数 < corePoolSize → 创建核心线程执行;
- 核心线程满了 → 任务加入阻塞队列;
- 队列满了 → 创建非核心线程执行,直到线程数达到 maximumPoolSize;
- 线程数达到最大 → 执行拒绝策略。
4 种拒绝策略
AbortPolicy(默认):抛出异常,核心业务首选;CallerRunsPolicy:由提交任务的线程执行,保证任务不丢失;DiscardOldestPolicy:丢弃队列最老的任务,执行当前任务;DiscardPolicy:直接丢弃当前任务,不抛异常。
【考点对应】模块四:线程池核心机制
二、场景坑点题(6 道,大厂实习高频业务题 / 代码题)
1. 你的校园二手平台用 volatile 修饰 int 类型的商品浏览量,多线程并发下viewCount++计数不准,是什么原因?怎么解决?
【标准答案】
原因
viewCount++是复合操作,分为「读取值→+1→写回」三步,volatile 只保证可见性,不保证原子性:多线程同时读取到同一个值,分别 + 1 后写回,会出现覆盖,最终计数比实际访问量少。
解决方案
- 方案 1:用
AtomicInteger原子类,底层基于 CAS 实现,无锁,保证计数原子性,性能最高; - 方案 2:用 synchronized 锁包裹计数操作,保证原子性,性能略低。
最佳实践:高并发计数场景优先用原子类,不要用 volatile。
【考点对应】模块二:volatile 原子性坑点、原子类适用场景
2. 你的校园二手平台商品扣减库存,怎么保证线程安全,避免超卖?分别说明单机和分布式场景的实现方案。
【标准答案】
单机场景(单实例部署)
- 方案 1:synchronized 锁:给扣减库存的方法加锁,保证同一时间只有一个线程扣减,实现简单,适合并发不高的场景;
- 方案 2:AtomicInteger 原子类 :库存用 AtomicInteger,调用
decrementAndGet()原子扣减,无锁,性能更高,适合高并发场景; - 避坑:锁粒度要小,只锁扣减逻辑,不要锁整个方法,不要用 this 对象锁,用库存对象作为锁,提升并发度。
分布式场景(多实例部署)
单机锁无效,必须用分布式锁:
- Redis 分布式锁 :用
SETNX+ 过期时间,保证同一时间只有一个实例扣减库存,性能高,实现简单; - 数据库乐观锁 :update 语句加版本号,
update goods set stock = stock -1, version = version +1 where id = ? and version = ?,无锁,适合并发不高的场景; - 数据库悲观锁:select ... for update,性能低,不推荐。
【考点对应】模块二:线程安全方案选型、业务场景落地
3. 你的项目用Executors.newFixedThreadPool(10)做异步任务,运行一段时间后出现 OOM,是什么原因?怎么解决?
【标准答案】
原因
newFixedThreadPool的底层实现:
java
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
核心问题是阻塞队列是无界的 LinkedBlockingQueue,任务量突增时,队列会无限增长,占用大量内存,最终 OOM。
阿里开发规范强制禁止用 Executors 创建线程池,就是因为所有自带线程池都有 OOM 风险。
解决方案
用ThreadPoolExecutor自定义线程池:
- 用有界队列 ArrayBlockingQueue,指定队列容量,避免无限增长;
- 合理配置核心线程数、最大线程数;
- 自定义拒绝策略,任务满了之后按业务规则处理。
【考点对应】模块四:Executors 线程池 OOM 坑点、自定义线程池最佳实践
4. 以下代码实现订单超时取消,有什么问题?为什么?
java
synchronized (lock) {
while (order.isPayed()) {
Thread.sleep(30000);
}
cancelOrder(order);
}
【标准答案】问题:会导致其他线程永远无法获取锁,所有订单都无法取消,程序卡死。原因:Thread.sleep()不会释放锁,线程睡眠 30 秒的过程中,一直持有锁,其他线程无法进入同步块,所有订单都被阻塞。修复:用wait(30000)代替Thread.sleep(),wait 会释放锁,其他线程可以正常执行。
【考点对应】模块三:wait () 和 sleep () 的核心区别
5. 你的校园二手平台要实现商品批量导入功能,10 个线程分别导入不同分类的商品,主线程需要等待所有线程导入完成后,返回导入结果,应该用哪个并发工具类?为什么?
【标准答案】用CountDownLatch(倒计时器),原因:
- 场景是主线程等待所有子线程执行完成,完全符合 CountDownLatch 的设计目标;
- 实现简单:初始化 CountDownLatch (10),每个子线程导入完成后调用
countDown()计数减 1,主线程调用await()等待计数到 0,再返回结果; - 不需要复用,一次性场景,CountDownLatch 比 CyclicBarrier 更轻量。
补充:如果是 10 个线程都导入完成后,一起执行数据校验,就用 CyclicBarrier。
【考点对应】模块三:并发工具类场景选型
6. 以下用 ReentrantLock 实现库存扣减的代码有什么坑?怎么修复?
java
Lock lock = new ReentrantLock();
public void deductStock() {
lock.lock();
// 扣减库存逻辑
int stock = goods.getStock();
if (stock > 0) {
goods.setStock(stock - 1);
}
lock.unlock();
}
【标准答案】坑点:如果扣减库存逻辑抛出异常,lock.unlock()永远不会执行,锁永远不会释放,导致死锁,所有线程都阻塞。修复:必须把unlock()放在 finally 代码块中,保证无论是否异常,锁都会释放:
java
public void deductStock() {
lock.lock();
try {
// 扣减库存逻辑
} finally {
lock.unlock();
}
}
【考点对应】模块三:ReentrantLock 释放锁坑点
三、进阶原理题(4 道,中大厂实习拔高题)
1. DCL(双重检查锁)实现单例,为什么必须给单例对象加 volatile?
【标准答案】不加 volatile 会出现半初始化对象的问题,根源是指令重排序:
- 正常对象创建的指令顺序:分配内存空间 → 初始化对象 → 把引用指向内存地址;
- 编译器 / CPU 可能会重排序为:分配内存空间 → 把引用指向内存地址 → 初始化对象;
- 多线程场景下,线程 A 执行到「把引用指向地址」,还没初始化对象,线程 B 进来判断 instance 不为 null,直接返回这个半初始化的对象,使用时会报错。
volatile 的作用是禁止指令重排序,保证对象创建的指令顺序不会被重排,避免半初始化对象的问题。
【考点对应】模块二:volatile 禁止指令重排、DCL 单例原理
2. synchronized 和 ReentrantLock 的核心区别是什么?各自的适用场景是什么?
【标准答案】
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 层面内置锁,native 实现 | JDK API 层面,基于 AQS 实现 |
| 锁释放 | 自动释放,异常也会自动释放,不会死锁 | 必须手动在 finally 中释放,否则会死锁 |
| 可中断 | 不可中断,获取锁的过程中无法中断 | 支持中断等待锁的线程,可超时获取锁 |
| 公平性 | 只能是非公平锁 | 支持公平锁 / 非公平锁(默认非公平) |
| 条件队列 | 只有 1 个条件队列 | 支持多个 Condition 条件队列,可精准唤醒指定线程 |
| 性能 | JDK6 优化后几乎一致 | 高竞争场景下略优 |
适用场景
- 90% 的普通场景优先用 synchronized:代码简单,自动释放锁,不会出现死锁;
- 特殊场景用 ReentrantLock:需要公平锁、可中断、超时获取锁、多条件精准唤醒的场景。
【考点对应】模块三:synchronized 与 ReentrantLock 对比
3. AQS(AbstractQueuedSynchronizer)的核心原理是什么?
【标准答案】AQS 是 JUC 包的核心基础框架,是所有锁、同步工具类的底层实现(ReentrantLock、CountDownLatch、Semaphore 等都基于 AQS),核心原理:
- 核心状态 :用
volatile int state表示同步状态,0 表示无锁,大于 0 表示有锁; - 双向队列:获取锁失败的线程,封装为 Node 节点,加入 FIFO 双向等待队列,阻塞等待;
- 两种模式 :
- 独占模式:同一时间只有一个线程能获取锁(ReentrantLock);
- 共享模式:多个线程可以同时获取锁(CountDownLatch、Semaphore);
- 核心逻辑:线程通过 CAS 修改 state,修改成功则获取锁,失败则加入等待队列,LockSupport.park () 阻塞,锁释放后唤醒队列头节点的线程。
【考点对应】模块三:JUC 核心底层原理
4. 线程池的核心线程数怎么配置?CPU 密集型和 IO 密集型任务分别怎么设置?
【标准答案】核心原则是让 CPU 的利用率最大化,没有固定值,需要根据业务压测调整,通用公式:
- CPU 密集型任务 (计算、加密、压缩、排序,CPU 使用率高):
- 核心线程数 = CPU 核心数 + 1
- 原因:避免线程上下文切换的开销,+1 是为了防止线程偶发缺页中断,保证 CPU 利用率;
- IO 密集型任务 (数据库、Redis、网络请求、文件 IO,90% 的业务场景):
- 核心线程数 = CPU 核心数 * 2 或者 CPU 核心数 / (1 - 阻塞系数)
- 阻塞系数一般是 0.8-0.9,比如 8 核 CPU,核心线程数 = 8/(1-0.9)=80;
- 原因:IO 密集型任务线程大部分时间在阻塞等待,多开线程可以让 CPU 在等待时处理其他任务,提升 CPU 利用率。
补充:你的校园二手平台的异步任务(短信通知、图片压缩、浏览量统计)都是 IO 密集型,核心线程数配置为 CPU 核心数 * 2 即可,最大线程数是核心线程数的 2 倍,用有界队列。
【考点对应】模块四:线程池参数配置最佳实践
实习面试答题加分技巧(绑定你的校园二手平台项目)
- 所有并发题都绑定项目实践 :
- 问线程池:主动说「我做校园二手平台的时候,所有异步任务都是用自定义线程池,核心线程数 8,最大 16,有界队列 1024,拒绝策略用 CallerRunsPolicy,之前踩过 Executors.newFixedThreadPool 无界队列 OOM 的坑」;
- 问线程安全:主动说「我做商品库存扣减的时候,单机用 AtomicInteger 原子类,分布式用 Redis 分布式锁,避免超卖」;
- 主动说踩坑经验:比如问 volatile,就说「我之前做浏览量统计的时候,踩过 volatile 不保证原子性的坑,计数不准,后来改成 AtomicInteger 就解决了」;
- 答题逻辑固定为「结论→原理→我的项目实践 / 踩坑」,比单纯背知识点的候选人认可度高很多。