多线程与并发编程 专属复习笔记

目录

模块一:线程基础(面试打底必问)

[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类实现:

  1. 继承 Thread 类 :重写run()方法,实例化子类调用start()启动;
  2. 实现 Runnable 接口 :重写run()方法,将实例传入 Thread 对象启动,解耦线程任务和线程对象;
  3. 实现 Callable+FutureTask :重写call()方法,支持返回值、抛出异常,通过 FutureTask 获取执行结果;
  4. 线程池创建 :通过ThreadPoolExecutor提交任务,项目开发唯一推荐方式,避免手动创建线程的资源浪费。
个人理解

继承 Thread 类受 Java 单继承限制,不推荐;Runnable 无返回值,Callable 有返回值;手动创建线程缺乏统一管理,容易造成线程泛滥、OOM,企业级项目 100% 用线程池创建线程。

项目实际使用场景

结合校园二手平台开发实践:

  1. 异步业务场景:商品详情页浏览量统计、订单支付成功后的短信 / 站内信通知、商品图片异步压缩,全部通过自定义线程池提交任务,不手动 new Thread;
  2. 有返回值的异步任务:批量查询商品的销量、评价数,用 Callable+FutureTask 异步查询,聚合结果后返回给前端,降低接口响应时间;
  3. 避坑实践:项目中绝对禁止手动创建线程,所有异步任务统一交给线程池管理,避免线程数过多导致服务器 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 是带超时的等待。

项目实际使用场景
  1. 订单超时取消:订单创建后,线程进入 TIMED_WAITING,超时后自动唤醒执行取消订单、释放库存;
  2. 分布式锁等待:线程获取分布式锁失败,进入 BLOCKED 状态等待锁释放;
  3. 多线程任务聚合:主线程调用子线程的 join () 方法,进入 WAITING 状态,等待所有子线程执行完成后再继续。
面试考点标注

✅ 必问:线程的 6 种状态和完整流转逻辑;

✅ 必问:wait () 和 sleep () 的核心区别;

✅ 对比:BLOCKED 和 WAITING 的区别。


3. start () 与 run () 的核心区别

核心定义
  1. start() :启动线程的正确方法,调用后线程进入 RUNNABLE 状态,由 JVM 调度执行run()方法,真正实现了多线程,一个线程只能调用一次 start ();
  2. run() :线程的业务方法,只是普通的对象方法,直接调用会在当前线程中执行,不会启动新线程,可以多次调用。
个人理解

这是多线程最基础的坑点,核心区别是「是否真正启动了新线程」,面试常考场景题,给出代码判断是否是多线程执行。

项目实际使用场景

项目中所有线程启动都是通过线程池的execute()/submit()方法,底层就是调用线程的 start () 方法,绝对不会直接调用 run () 方法。

面试考点标注

✅ 必问:start () 和 run () 的核心区别;

✅ 场景题:直接调用 run () 方法会不会启动新线程?


4. 线程优先级与守护线程

核心定义
  1. 线程优先级:Java 线程优先级范围 1-10,默认 5,优先级越高的线程获取 CPU 调度的概率越高,不是绝对优先;
  2. 守护线程(Daemon Thread):后台服务线程,是用户线程的保姆,当所有用户线程都结束后,守护线程会自动销毁;典型例子:GC 垃圾回收线程。

规则:线程默认是用户线程,调用setDaemon(true)设置为守护线程,必须在 start () 之前设置;守护线程创建的子线程默认也是守护线程。

个人理解

线程优先级只是给 CPU 的提示,不能依赖优先级控制业务逻辑,不同操作系统的优先级实现不同;守护线程不能用来执行核心业务,因为会随着用户线程结束而突然终止。

项目实际使用场景
  1. 后台监控线程:项目的 JVM 监控、日志上报线程,设置为守护线程,不影响主业务退出;
  2. 避坑实践:订单、支付、库存等核心业务线程,绝对不能设置为守护线程,避免业务执行到一半被终止。
面试考点标注

✅ 必问:守护线程的特点和使用场景;

✅ 坑点:守护线程的 finally 代码块不一定会执行(用户线程结束后 JVM 直接退出)。


模块二:线程安全核心(面试难点核心)

1. synchronized 关键字

核心定义

synchronized 是 JVM 层面的内置锁,可重入,自动加锁释放,不会死锁,JDK6 之后做了大量锁优化,核心特性:

(1)锁升级过程(JDK6+,面试 100% 必问)

无锁 → 偏向锁 → 轻量级锁 → 重量级锁,锁只能升级不能降级:

  1. 无锁:对象刚创建,没有线程竞争;
  2. 偏向锁:第一个线程访问锁,将线程 ID 记录在对象头,后续该线程访问无需加锁解锁,无竞争场景性能极高;
  3. 轻量级锁:多线程轻度竞争,偏向锁撤销,线程通过 CAS 自旋获取锁,不阻塞线程,性能高;
  4. 重量级锁:竞争激烈,自旋超过次数,升级为重量级锁,基于操作系统的互斥量,线程阻塞,性能低。
(2)对象锁与类锁的区别
  • 对象锁:锁的是实例对象,锁的是非静态方法、this 对象,不同实例的对象锁互不影响;
  • 类锁:锁的是类的 Class 对象,锁的是静态方法、类名.class 对象,全局唯一,所有实例共享同一把锁。
(3)锁优化
  • 锁消除:JVM 判断没有共享数据竞争,自动消除锁;
  • 锁粗化:JVM 把连续的加锁解锁合并成一个大锁,减少频繁加锁解锁的开销。
个人理解

synchronized 是开发中最常用的锁,JDK 优化后性能和 ReentrantLock 几乎一致,不需要手动释放锁,不会出现死锁,优先使用;锁升级是 JVM 的性能优化,无竞争、轻度竞争场景下性能极高,只有高竞争才会升级为重量级锁。

项目实际使用场景
  1. 商品库存扣减:单机场景下,扣减库存的方法加 synchronized 对象锁,保证超卖;
  2. 用户手机号唯一性校验:注册接口加 synchronized 类锁,全局唯一,避免并发注册同一个手机号;
  3. 避坑实践:锁的粒度要尽可能小,只锁需要同步的代码块,不要锁整个方法,提升并发性能。
面试考点标注

✅ 必问:synchronized 的锁升级完整流程;

✅ 必问:对象锁和类锁的区别;

✅ 原理:synchronized 的可重入性原理;

✅ 对比:synchronized 和 Lock 的区别。


2. volatile 关键字

核心定义

volatile 是 JVM 提供的轻量级同步机制,三大特性:

  1. 可见性:一个线程修改了 volatile 变量的值,其他线程能立即看到最新值,底层通过 MESI 缓存一致性协议 + 内存屏障实现;
  2. 禁止指令重排:通过内存屏障禁止编译器和 CPU 对指令进行重排序,保证有序性;
  3. 不保证原子性:不能保证复合操作(比如 i++)的原子性,这是 volatile 和 synchronized 最核心的区别。
个人理解

volatile 是轻量级的同步机制,只能保证可见性和有序性,不能保证原子性,适合一写多读的场景,是实现 DCL 单例、服务开关的核心。

项目实际使用场景
  1. 服务开关配置:项目的接口限流开关、灰度发布开关,用 volatile 修饰,后台修改后所有线程立即生效,不需要加锁;
  2. 双重检查锁(DCL)单例:项目的单例线程池、单例工具类,用 volatile 修饰单例对象,禁止指令重排,避免半初始化对象;
  3. 避坑实践:计数场景(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 问题在绝大多数业务场景下不影响,只有需要记录修改过程的场景才需要解决。

项目实际使用场景
  1. 接口限流计数器:接口的请求次数统计,用 AtomicInteger,线程安全,无锁,性能高;
  2. 商品浏览量、收藏量统计:高并发下的计数,用 AtomicLong,避免加锁的性能开销;
  3. 避坑实践:高并发计数场景,优先用原子类,不要用 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 支持精准唤醒,适合复杂的生产者消费者场景。

项目实际使用场景
  1. 订单支付回调:支付成功后,用 Condition 精准唤醒等待支付结果的订单线程,继续执行后续逻辑;
  2. 多线程批量处理商品:主线程用 CountDownLatch 等待所有子线程处理完成,本质是基于 LockSupport 实现的。
面试考点标注

✅ 必问:wait () 和 sleep () 的核心区别:

对比维度 wait() sleep()
所属类 Object 类 Thread 类
锁释放 会释放锁 不会释放锁
使用条件 必须在 synchronized 中 不需要加锁
唤醒方式 等待 notify / 超时 等待超时 / 中断

2. JUC 三大并发工具类

核心定义
工具类 核心作用 特点 适用场景
CountDownLatch(倒计时器) 一个线程等待 N 个线程执行完成,再继续执行 一次性,计数到 0 就不能再用 批量任务聚合、多线程查询结果合并
CyclicBarrier(循环栅栏) N 个线程互相等待,所有线程都到达栅栏后,再一起执行 可复用,计数到 0 后重置 多线程数据校验、并行计算
Semaphore(信号量) 控制同时访问资源的最大线程数 可控制并发度 接口限流、资源池控制
个人理解

CountDownLatch 是主线程等子线程,CyclicBarrier 是子线程互相等,Semaphore 是控制并发数,三个工具类是高并发开发的常用工具,面试常考场景选型。

项目实际使用场景
  1. 商品批量导入:用 CountDownLatch,主线程等待所有导入线程处理完成,再返回导入结果;
  2. 订单批量导出:用 CyclicBarrier,所有导出线程都读取完数据后,一起执行数据校验和合并;
  3. 图片上传接口限流:用 Semaphore,控制同时上传的最大线程数,避免服务器带宽被打满。
面试考点标注

✅ 必问:CountDownLatch 和 CyclicBarrier 的核心区别;

✅ 场景题:三个工具类分别适合什么场景。


3. ReentrantLock 与 synchronized 的核心区别

核心定义

ReentrantLock 是 JDK 提供的 API 层面的可重入锁,和 synchronized 核心区别:

对比维度 synchronized ReentrantLock
实现层面 JVM 层面,内置锁 JDK API 层面,基于 AQS
锁释放 自动释放,异常也会释放 必须手动在 finally 中释放,否则会死锁
可中断 不可中断 支持中断等待锁的线程
公平锁 只能非公平锁 支持公平锁 / 非公平锁(默认非公平)
条件队列 只有一个条件队列 支持多个 Condition 条件队列,精准唤醒
性能 JDK6 优化后几乎一致 高竞争场景下略优
个人理解

synchronized 足够应对 90% 的场景,代码更安全,不会漏释放锁;只有需要公平锁、可中断、多条件唤醒的特殊场景,才用 ReentrantLock。

项目实际使用场景
  1. 用户下单公平队列:下单接口用 ReentrantLock 的公平锁,保证用户请求按顺序处理,避免插队;
  2. 生产者消费者模型:用 ReentrantLock 的两个 Condition,分别控制生产者队列和消费者队列,精准唤醒。
面试考点标注

✅ 必问:ReentrantLock 和 synchronized 的核心区别,各自的适用场景;

✅ 原理:ReentrantLock 的可重入性、公平锁实现原理(基于 AQS)。


模块四:线程池(面试 100% 必问,重中之重)

1. ThreadPoolExecutor 7 大核心参数

核心定义

自定义线程池必须通过ThreadPoolExecutor创建,7 大核心参数:

参数 定义 说明
corePoolSize 核心线程数 线程池长期保留的线程数,即使空闲也不会销毁
maximumPoolSize 最大线程数 线程池能创建的最大线程数
keepAliveTime 空闲线程存活时间 非核心线程空闲超过这个时间会被销毁
unit 时间单位 keepAliveTime 的时间单位
workQueue 阻塞队列 核心线程满了之后,任务存入阻塞队列
threadFactory 线程工厂 创建线程的工厂,用来设置线程名、优先级、守护线程属性
handler 拒绝策略 线程池满了之后,对新任务的处理策略

2. 线程池完整执行流程

  1. 提交新任务:
    • 核心线程数 < 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. 线程池参数配置最佳实践

核心配置原则
  1. CPU 密集型任务(计算、加密、压缩):corePoolSize = CPU 核心数 + 1,避免线程上下文切换开销;
  2. IO 密集型任务(数据库、Redis、网络请求,90% 业务场景):corePoolSize = CPU 核心数 * 2 或者 CPU 核心数 / (1 - 阻塞系数),阻塞系数一般 0.8-0.9;
  3. 阻塞队列必须用有界队列:ArrayBlockingQueue,指定队列容量,避免 OOM;
  4. 自定义线程工厂 :设置线程名前缀,方便排查问题,比如goods-async-thread-
个人理解

线程池是高并发开发的核心,参数配置没有固定值,要根据业务场景压测调整,核心是避免 OOM、控制并发度、提升资源利用率。

项目实际使用场景
  1. 通用异步任务线程池 :校园二手平台的浏览量统计、短信通知、图片压缩等 IO 密集型任务,自定义线程池:
    • corePoolSize=8,maximumPoolSize=16,keepAliveTime=60s;
    • 阻塞队列:ArrayBlockingQueue (1024),有界队列;
    • 线程工厂:自定义线程名goods-async-thread-
    • 拒绝策略:CallerRunsPolicy,保证任务不丢失;
  2. 订单定时任务线程池:订单超时取消、自动确认收货,自定义 Scheduled 线程池,核心线程数 4,有界队列,避免 OOM。
面试考点标注

✅ 必问:线程池的 7 大核心参数、完整执行流程;

✅ 必问:4 种拒绝策略的区别;

✅ 必问:为什么不能用 Executors 创建线程池?有什么风险?

✅ 场景题:线程池参数怎么配置?CPU 密集型和 IO 密集型怎么设置?


当日验收清单

  1. 不看资料口述:线程 6 种状态流转图、线程池完整执行流程、synchronized 的锁升级过程;
  2. 结合项目口述:你的项目哪些接口用了线程池做异步处理,参数怎么配置的,哪些场景需要保证线程安全;
  3. 避坑确认: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 () 方法的核心区别是什么?

【标准答案】

  1. 本质区别
    • start():真正启动新线程的方法,调用后线程进入 RUNNABLE 状态,由 JVM 调度执行run()方法,实现了多线程并行执行
    • run():只是普通的业务方法,直接调用会在当前线程中同步执行,不会启动新线程,没有多线程效果。
  2. 调用限制 :一个线程只能调用一次start(),多次调用会抛IllegalThreadStateExceptionrun()可以多次调用。

【考点对应】模块一:start () 与 run () 核心区别


3. 线程的 6 种生命周期状态是什么?完整流转逻辑是怎样的?

【标准答案】

6 种状态定义
  1. NEW(新建):线程对象创建完成,未调用 start ();
  2. RUNNABLE(可运行):调用 start () 后,包含就绪(等待 CPU 调度)和运行(CPU 执行)两个子状态;
  3. BLOCKED(阻塞):等待 synchronized 锁释放;
  4. WAITING(无限等待):调用 wait ()、join ()、LockSupport.park (),等待其他线程唤醒;
  5. TIMED_WAITING(计时等待):调用 sleep ()、wait (time)、parkNanos (),超时自动唤醒;
  6. TERMINATED(终止):线程执行完成或异常退出,生命周期结束。
核心流转逻辑

NEW → RUNNABLE ↔ BLOCKED/WAITING/TIMED_WAITING → TERMINATED

【考点对应】模块一:线程状态与流转


4. 简述 synchronized 的锁升级完整流程(JDK6+)

【标准答案】锁升级是 JVM 为了优化 synchronized 性能的机制,锁只能升级不能降级,完整流程:

  1. 无锁:对象刚创建,无线程竞争,对象头标记为无锁状态;
  2. 偏向锁:第一个线程访问锁,将线程 ID 记录在对象头的 Mark Word 中,后续该线程访问无需加锁解锁,无竞争场景性能极高;
  3. 轻量级锁:多线程轻度竞争,偏向锁撤销,线程通过 CAS 自旋获取锁,不阻塞线程,避免用户态内核态切换;
  4. 重量级锁:竞争激烈,自旋超过次数(默认 10 次),升级为重量级锁,基于操作系统的互斥量实现,线程阻塞,性能较低。

【考点对应】模块二:synchronized 锁优化


5. volatile 关键字的三大特性是什么?为什么不保证原子性?

【标准答案】

三大特性
  1. 可见性:一个线程修改 volatile 变量,其他线程能立即看到最新值,底层通过内存屏障 + MESI 缓存一致性协议实现;
  2. 有序性:通过内存屏障禁止指令重排序;
  3. 不保证原子性:无法保证复合操作(如 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 大核心参数
  1. corePoolSize:核心线程数,长期保留的线程数;
  2. maximumPoolSize:最大线程数,线程池能创建的最大线程数;
  3. keepAliveTime:非核心线程空闲存活时间;
  4. unit:时间单位;
  5. workQueue:阻塞队列,核心线程满了之后任务存入队列;
  6. threadFactory:线程工厂,自定义线程名、优先级;
  7. handler:拒绝策略,线程池满了之后处理新任务的策略。
完整执行流程
  1. 新任务提交,核心线程数 < corePoolSize → 创建核心线程执行;
  2. 核心线程满了 → 任务加入阻塞队列;
  3. 队列满了 → 创建非核心线程执行,直到线程数达到 maximumPoolSize;
  4. 线程数达到最大 → 执行拒绝策略。
4 种拒绝策略
  1. AbortPolicy(默认):抛出异常,核心业务首选;
  2. CallerRunsPolicy:由提交任务的线程执行,保证任务不丢失;
  3. DiscardOldestPolicy:丢弃队列最老的任务,执行当前任务;
  4. DiscardPolicy:直接丢弃当前任务,不抛异常。

【考点对应】模块四:线程池核心机制


二、场景坑点题(6 道,大厂实习高频业务题 / 代码题)

1. 你的校园二手平台用 volatile 修饰 int 类型的商品浏览量,多线程并发下viewCount++计数不准,是什么原因?怎么解决?

【标准答案】

原因

viewCount++是复合操作,分为「读取值→+1→写回」三步,volatile 只保证可见性,不保证原子性:多线程同时读取到同一个值,分别 + 1 后写回,会出现覆盖,最终计数比实际访问量少。

解决方案
  1. 方案 1:用AtomicInteger原子类,底层基于 CAS 实现,无锁,保证计数原子性,性能最高;
  2. 方案 2:用 synchronized 锁包裹计数操作,保证原子性,性能略低。

最佳实践:高并发计数场景优先用原子类,不要用 volatile。

【考点对应】模块二:volatile 原子性坑点、原子类适用场景


2. 你的校园二手平台商品扣减库存,怎么保证线程安全,避免超卖?分别说明单机和分布式场景的实现方案。

【标准答案】

单机场景(单实例部署)
  1. 方案 1:synchronized 锁:给扣减库存的方法加锁,保证同一时间只有一个线程扣减,实现简单,适合并发不高的场景;
  2. 方案 2:AtomicInteger 原子类 :库存用 AtomicInteger,调用decrementAndGet()原子扣减,无锁,性能更高,适合高并发场景;
  3. 避坑:锁粒度要小,只锁扣减逻辑,不要锁整个方法,不要用 this 对象锁,用库存对象作为锁,提升并发度。
分布式场景(多实例部署)

单机锁无效,必须用分布式锁:

  1. Redis 分布式锁 :用SETNX+ 过期时间,保证同一时间只有一个实例扣减库存,性能高,实现简单;
  2. 数据库乐观锁 :update 语句加版本号,update goods set stock = stock -1, version = version +1 where id = ? and version = ?,无锁,适合并发不高的场景;
  3. 数据库悲观锁: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自定义线程池:

  1. 有界队列 ArrayBlockingQueue,指定队列容量,避免无限增长;
  2. 合理配置核心线程数、最大线程数;
  3. 自定义拒绝策略,任务满了之后按业务规则处理。

【考点对应】模块四: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(倒计时器),原因:

  1. 场景是主线程等待所有子线程执行完成,完全符合 CountDownLatch 的设计目标;
  2. 实现简单:初始化 CountDownLatch (10),每个子线程导入完成后调用countDown()计数减 1,主线程调用await()等待计数到 0,再返回结果;
  3. 不需要复用,一次性场景,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 会出现半初始化对象的问题,根源是指令重排序:

  1. 正常对象创建的指令顺序:分配内存空间 → 初始化对象 → 把引用指向内存地址;
  2. 编译器 / CPU 可能会重排序为:分配内存空间 → 把引用指向内存地址 → 初始化对象;
  3. 多线程场景下,线程 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),核心原理:

  1. 核心状态 :用volatile int state表示同步状态,0 表示无锁,大于 0 表示有锁;
  2. 双向队列:获取锁失败的线程,封装为 Node 节点,加入 FIFO 双向等待队列,阻塞等待;
  3. 两种模式
    • 独占模式:同一时间只有一个线程能获取锁(ReentrantLock);
    • 共享模式:多个线程可以同时获取锁(CountDownLatch、Semaphore);
  4. 核心逻辑:线程通过 CAS 修改 state,修改成功则获取锁,失败则加入等待队列,LockSupport.park () 阻塞,锁释放后唤醒队列头节点的线程。

【考点对应】模块三:JUC 核心底层原理


4. 线程池的核心线程数怎么配置?CPU 密集型和 IO 密集型任务分别怎么设置?

【标准答案】核心原则是让 CPU 的利用率最大化,没有固定值,需要根据业务压测调整,通用公式:

  1. CPU 密集型任务 (计算、加密、压缩、排序,CPU 使用率高):
    • 核心线程数 = CPU 核心数 + 1
    • 原因:避免线程上下文切换的开销,+1 是为了防止线程偶发缺页中断,保证 CPU 利用率;
  2. 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 倍,用有界队列。

【考点对应】模块四:线程池参数配置最佳实践


实习面试答题加分技巧(绑定你的校园二手平台项目)

  1. 所有并发题都绑定项目实践
    • 问线程池:主动说「我做校园二手平台的时候,所有异步任务都是用自定义线程池,核心线程数 8,最大 16,有界队列 1024,拒绝策略用 CallerRunsPolicy,之前踩过 Executors.newFixedThreadPool 无界队列 OOM 的坑」;
    • 问线程安全:主动说「我做商品库存扣减的时候,单机用 AtomicInteger 原子类,分布式用 Redis 分布式锁,避免超卖」;
  2. 主动说踩坑经验:比如问 volatile,就说「我之前做浏览量统计的时候,踩过 volatile 不保证原子性的坑,计数不准,后来改成 AtomicInteger 就解决了」;
  3. 答题逻辑固定为「结论→原理→我的项目实践 / 踩坑」,比单纯背知识点的候选人认可度高很多。
相关推荐
adrninistrat0r1 小时前
Java调用链MCP分析工具
java·python·ai编程
Oll Correct2 小时前
实验二十九:TCP的运输连接管理
网络·笔记
杨充2 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~2 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball6162 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
春生野草2 小时前
反射、Tomcat执行
java·开发语言
_日拱一卒3 小时前
LeetCode:207课程表
java·数据结构·算法·leetcode·职场和发展
飞翔中文网3 小时前
Java学习笔记之抽象类与接口(设计思想)
java·笔记·学习
雪的季节3 小时前
企业级 Qt 全功能项目
开发语言·数据库·qt