一、基础概念(1-20 题)
1.什么是进程?什么是线程?
1、进程(Process) :操作系统资源分配的最小单位 ,是一段程序的一次运行过程。进程拥有独立内存空间、堆、方法区、文件句柄,进程之间内存完全隔离,互不共享。操作系统调度成本极高,进程切换开销大。
2、线程(Thread) :操作系统CPU调度的最小单位 ,隶属于进程,一个进程至少包含一条主线程。线程共享所属进程的堆、方法区、常量池;仅私有程序计数器、虚拟机栈、本地方法栈。线程切换成本极低,轻量化。
通俗理解 :进程=独立工厂;线程=工厂里工人;工厂独立厂房材料互不通用,工人共享工厂资源,协同干活。
核心总结:进程隔离资源,线程共享资源;进程开销重,线程开销轻。
2.线程和进程区别?
进程独立内存,切换开销大;线程共享内存,切换轻量,通信简单。
3.并发与并行区别?
1、并发(Concurrency) :同一时间段 内多个任务交替执行。单CPU核心通过时间片快速切换线程,宏观看上去同时运行,微观上同一时刻只有一个任务执行。线程之间频繁切换、交替执行。
2、并行(Parallelism) :同一时刻 多个任务同时执行。必须依赖多核CPU,多个物理核心同时处理多条线程,真正意义上一起运行,无切换执行。
通俗比喻 :
并发:一个人吃两碗饭,来回交替扒饭;
并行:两个人同时各吃一碗饭。
核心总结(面试必背) :并发是交替执行、宏观同时 ;并行是同时执行、物理多核。Java多线程默认大多是并发,只有多核才能实现并行。
4.Java 程序默认有几个线程?
1、标准答案 :一个普通 Java 程序默认至少 2 个线程 ,分别是主线程、垃圾回收线程。
2、详细底层拆解(面试加分)
① 主线程(Main Thread) :JVM 自动创建,执行 main() 方法,处理业务代码、程序入口。
② GC 垃圾回收线程 :守护线程,后台持续监控堆内存,自动回收无效对象,防止内存溢出。
3、拓展:真实运行默认线程数量
在 JDK8 环境下,简单空main程序实际默认启动 **5~7条线程**:主线程、GC线程、监控线程、信号分发线程、销毁钩子线程。
4、面试总结 :背诵极简答案:最少2条:主线程+GC线程;实际运行含后台监控线程共7条左右。
5.创建线程有几种方式?(六种+完整代码+面试必背)
面试标准答案 :Java 创建线程一共6种方式 ,分别为:继承Thread类、实现Runnable接口、Callable+FutureTask、匿名内部类、Lambda表达式、线程池。
1、继承 Thread 类(底层源码)
特点:单继承、无法复用、无返回值。
java
class MyThread extends Thread{
@Override
public void run() {
System.out.println("继承Thread方式创建线程");
}
}
// 使用:new MyThread().start();
2、实现 Runnable 接口(最常用)
特点:避免单继承、代码解耦、无返回值。
java
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口创建线程");
}
}
// 使用:new Thread(new MyRunnable()).start();
3、Callable + FutureTask(有返回值)
特点:可抛出异常、有返回结果、可获取执行结果。
java
Callable<Integer> callable = () -> {
System.out.println("Callable有返回值线程");
return 1024;
};
FutureTask<Integer> task = new FutureTask<>(callable);
new Thread(task).start();
4、匿名内部类(简化写法)
特点:临时线程、无需单独定义类。
java
new Thread(){
@Override
public void run() {
System.out.println("匿名内部类线程");
}
}.start();
5、Lambda 表达式(JDK8+最简)
特点:代码极简、开发最常用。
java
new Thread(() -> System.out.println("Lambda简洁线程")).start();
6、线程池创建(生产唯一推荐)
特点:复用线程、管控并发、禁止频繁创建销毁。
java
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.execute(() -> System.out.println("线程池执行任务"));
面试高频总结 :
① 无返回值优先 Runnable ;
② 需要返回值用 Callable ;
③ 线上业务全部使用线程池,禁止手动new Thread。
6.Runnable 和 Callable 区别?(超详细背诵版+源码对比)
1、核心定义
两者都是用于定义线程任务的函数式接口,用于封装线程执行逻辑。
2、五大核心区别(面试必背)
| 对比维度 | Runnable | Callable |
|---|---|---|
| 返回值 | 无返回值,核心方法为 void run() |
有泛型返回值,核心方法为 V call() |
| 异常抛出 | 方法无 throws 声明, 异常只能在内部 try-catch 处理 |
方法声明 throws Exception, 异常可向上层抛出 |
| JDK 版本 | JDK1.0 原生古老接口 | JDK1.5 新增接口 |
| 使用载体 | 可直接传入 Thread、线程池执行 | 必须包装为 FutureTask 后,才能放入 Thread 执行 |
| 应用场景 | 单纯执行异步任务,无需获取执行结果 | 需要获取线程执行返回结果的业务场景 |
3、底层源码极简展示
java
// Runnable源码
@FunctionalInterface
public interface Runnable {
void run();
}
// Callable源码
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
4、补充面试高频考点
① Thread类只能接收Runnable,不能直接接收Callable;
② Callable必须依赖 FutureTask 适配器包装才能放入线程;
③ 线程池submit()方法支持Callable,execute()只支持Runnable;
5、一句话总结:
不需要返回值用Runnable,需要返回值、抛异常用Callable。
7.start() 和 run() 区别?(满分详细版+底层原理+代码演示)
1、核心本质区别(面试必背)
① start():真正开启线程 。属于Thread原生方法,调用后进入就绪状态 ,由JVM获取CPU时间片后自动回调run(),存在线程切换,异步执行。
② run():普通成员方法 。重写业务逻辑,直接调用不会开启新线程,仅在当前线程同步执行,无多线程特性。
2、五大详细区别
| 对比维度 | start() | run() |
|---|---|---|
| 线程状态 | 新建→就绪,开启新线程 | 无状态变化,当前线程执行 |
| 调用次数 | 只能调用一次,二次调用抛异常 | 可重复无限调用 |
| 执行方式 | 异步、多线程并发 | 同步、串行执行 |
| 底层原理 | 调用 native 本地方法,向操作系统申请线程 | 纯 Java 代码,普通方法压栈执行 |
| 异常抛出 | 线程内部异常,不影响主线程 | 同步抛出,直接阻塞当前主线程 |
3、代码演示直观区别
java
Thread thread = new Thread(() -> System.out.println("执行线程任务"));
thread.run(); // 主线程执行,无新线程
thread.start();// 开启新子线程,异步执行
// thread.start(); 二次调用直接抛出:IllegalThreadStateException
4、高频面试坑点
① start()底层是native 方法,由操作系统创建内核线程;
② 一个线程对象只能start一次,重复调用抛出非法线程状态异常;
③ 业务绝对不要手动调用run(),会丧失多线程并发能力。
5、一句话总结 :start()申请资源开启新线程;run()只是一段普通业务代码。
8.线程有几种生命周期状态?(七种完整版+流转图+面试必考)
1、标准答案 :Java 线程生命周期一共 7 种状态,出自 Thread.State 枚举类,分别为:
新建、就绪、运行、阻塞、等待、超时等待、终止。
2、七种状态详细解释(面试必背)
① NEW(新建) :创建线程对象,未调用start(),没有分配CPU资源。
② RUNNABLE(就绪) :调用start(),准备就绪,等待CPU时间片,包含操作系统就绪+运行。
③ RUNNING(运行) :获取CPU时间片,正在执行run()业务逻辑。
④ BLOCKED(阻塞) :未抢到synchronized重量级锁,阻塞挂起,不释放CPU。
⑤ WAITING(无限等待) :无限等待别人唤醒,调用wait()、join(),必须手动唤醒 ,释放锁。
⑥ TIMED_WAITING(超时等待) :限时等待,sleep(time)、wait(time),时间到自动唤醒。
⑦ TERMINATED(终止) :线程代码执行完毕,线程彻底死亡,不可重启。
3、线程核心流转流程
新建 → start() → 就绪 → 抢占CPU → 运行 →(阻塞/等待/超时等待)→ 唤醒回到就绪 → 运行 → 执行完毕 → 终止。
4、高频面试坑点
① Java源码枚举没有运行态RUNNING ,底层把就绪+运行合并为RUNNABLE;
② BLOCKED只针对synchronized,Lock锁阻塞是WAITING;
③ 死亡线程不能再次start(),直接抛异常;
④ sleep进入超时等待、wait进入无限等待,都会释放CPU,sleep不释放锁、wait释放锁。
5、一句话总结 :七态核心:两等一阻、新建就绪、运行终止。
9.sleep() 和 wait() 区别?(满分完整版+表格对比+面试必考)
1、一句话极简背诵
sleep 属于线程、不释放锁、不必须同步;wait 属于对象、释放锁、必须在同步代码块。
2、六大核心区别(面试必背表格)
| 对比维度 | sleep() | wait() |
|---|---|---|
| 归属类 | Thread 静态方法 | Object 成员方法 |
| 锁行为 | 不释放锁、不释放资源 | 主动释放锁 |
| 使用位置 | 任意位置使用,无需同步 | 必须在 synchronized 同步代码块 / 方法内 |
| 唤醒条件 | 时间到自动唤醒 | 必须被 notify ()/notifyAll () 手动唤醒 |
| 线程状态 | 进入 TIMED_WAITING 超时等待 | 进入 WAITING 无限等待 |
| 异常抛出 | 必须捕获 InterruptedException | 必须捕获 InterruptedException |
3、底层原理通俗解释
① sleep() :单纯让线程休眠指定时间,CPU暂停执行,线程抱着锁睡觉,其他线程无法抢占锁;
② wait() :主动放弃当前持有的锁,进入对象等待池,别人唤醒后再重新竞争锁。
4、代码直观演示锁区别
java
// sleep不释放锁:别的线程进不来
synchronized (obj){
Thread.sleep(2000); // 抱着锁休眠
}
// wait释放锁:别的线程可以抢占锁
synchronized (obj){
obj.wait(); // 直接放开锁进入等待
}
5、高频面试坑点
① wait 必须搭配 synchronized,不加锁直接调用报:IllegalMonitorStateException;
② sleep 不会释放锁,生产高并发不要写长时间sleep;
③ wait 一般写在 while 循环防止虚假唤醒。
6、终极总结 :sleep 休眠不撒手、wait 等待放开手。
10.yield () 作用?
让出 CPU 执行权,回到就绪态,不释放锁。
11.join () 作用?
等待当前线程执行完毕,再继续执行主线程。
12.线程终止方式?(四种完整版+全部代码+生产规范)
面试标准答案:
Java 线程终止一共 4种方式 :正常执行结束、volatile标记位终止、interrupt中断终止、stop暴力终止(废弃)。
1、方式一:正常执行结束(天然终止)
线程run()方法代码执行完毕,自动进入TERMINATED终止状态,无任何额外代码,最安全。
2、方式二:volatile 布尔标记位终止(生产最推荐)
原理:利用volatile可见性,外部修改标记位,线程主动判断退出,优雅安全、无锁、无异常。
java
// volatile标记位终止线程
public class ThreadStopFlag {
// volatile保证可见性
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag){
// 业务逻辑
}
System.out.println("线程优雅退出");
}).start();
Thread.sleep(2000);
// 修改标记位,终止线程
flag = false;
}
}
3、方式三:interrupt() 中断终止(官方推荐)
原理:修改线程中断标记,不会强制杀死线程,分为两种中断:
① 阻塞状态中断 :线程处于sleep/wait/join,直接抛出异常、清除中断标记;
②运行状态中断 :仅打标记,线程自行判断isInterrupted()退出。
java
// interrupt优雅终止代码
public class ThreadInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()){
// 执行业务
}
System.out.println("线程收到中断信号,主动退出");
});
t1.start();
Thread.sleep(2000);
// 发送中断信号
t1.interrupt();
}
}
4、方式四:stop() 暴力终止(官方废弃、禁止使用)
java
// 禁止使用!已废弃
thread.stop();
5、四种方式优缺点总结(面试必背表格)
| 终止方式 | 优点 | 缺点 |
|---|---|---|
| 正常执行结束 | 安全、无副作用,线程天然终止 | 无法手动控制终止时机,仅能等待代码执行完毕 |
| volatile 标记位 | 简单优雅、可见性强、无异常,代码侵入性低 | 无法终止阻塞中的线程,仅能控制运行态线程主动退出 |
| interrupt 中断 | 官方推荐、可处理阻塞线程,适配全场景线程终止 | 需要手动判断中断标记,编码复杂度略高于标记位方案 |
| stop 暴力终止 | 可立即杀死线程,强制终止速度快 | 会造成数据错乱、锁不释放、内存损坏,官方已废弃,生产禁止使用 |
6、高频面试坑点
① interrupt 只是打标记 ,不会直接杀死线程;
② 线程处于阻塞状态,调用interrupt会直接抛异常;
③ 生产环境禁止使用stop、destroy ;
④ 阻塞线程终止优先interrupt,运行中线程终止优先volatile标记位。
7、一句话总结 :运行线程用标记、阻塞线程用中断、禁止暴力stop。
13.interrupt () 中断原理?
修改中断标记,不会立刻终止,线程自行判断处理。
14.守护线程和用户线程区别?(满分完整版+通俗比喻+面试必考)
1、核心定义
① 用户线程(非守护线程) :普通业务线程,JVM 必须等待所有用户线程执行完毕,才会正常退出,属于前台线程 。
② 守护线程(后台线程) :专门为用户线程服务的后台线程,只要全部用户线程结束,无论守护线程是否执行完毕,JVM 强制杀死守护线程并退出。
2、五大核心区别(面试必背表格)
| 对比维度 | 用户线程(非守护线程) | 守护线程(后台线程) |
|---|---|---|
| 生命周期 | 独立运行,代码执行完毕才自动终止,生命周期自主可控 | 完全依附用户线程,所有用户线程结束后,守护线程会被 JVM 强制销毁,无自主生命周期 |
| JVM 退出条件 | 只要存在任意一条用户线程,JVM 就不会正常退出 | 当 JVM 中仅剩余守护线程时,JVM 会直接强制退出,无需等待守护线程执行完毕 |
| 线程优先级 | 默认优先级为 5,优先级较高,可优先抢占 CPU 资源 | 优先级偏低,仅在用户线程空闲时获取 CPU 资源,后台静默运行 |
| 异常处理 | 线程抛出异常会独立向上抛出,不影响其他线程的正常执行 | 线程抛出异常一般会被 JVM 静默忽略,不会中断其他业务线程 |
| 典型应用场景 | 核心业务逻辑、接口请求处理、数据计算、订单交易等前台核心业务 | GC 垃圾回收、JVM 监控线程、服务心跳检测、日志采集、定时巡检等后台辅助服务 |
3、通俗比喻
用户线程 = 顾客 ;守护线程 = 保洁服务员 ;
顾客全部走光(用户线程结束),保洁立刻下班(守护线程强制销毁),门店关门(JVM退出)。
4、代码演示区别
java
public class DaemonThreadDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true){
// 无限循环任务
}
});
// 设置为守护线程,必须在start()之前
daemonThread.setDaemon(true);
daemonThread.start();
// 主线程(用户线程)休眠2秒结束
Thread.sleep(2000);
System.out.println("主线程执行结束");
// 主线程结束,JVM直接关闭,守护线程被强制杀死
}
}
5、高频面试坑点
① setDaemon(true) 必须在 start() 之前执行 ,否则报错;
② 守护线程不能用于写入IO、事务等关键业务,JVM会强制终止导致数据丢失;
③ Java默认全部线程都是用户线程,GC是典型守护线程;
④ 守护线程适合做:监控、日志、定时巡检、心跳保活。
6、一句话总结 :用户线程决定JVM生死,守护线程陪跑服务、随用户线程一起消亡。
15.如何设置守护线程?(完整版+代码+硬性规则)
1、核心方法 :使用线程自带方法 setDaemon(boolean on) ,传入 true 即为守护线程,false 为默认用户线程。
2、硬性语法规则(面试必考)
① 必须在 start() 之前设置 ,线程启动后无法修改守护状态;
② 不能把正在运行的线程改为守护线程,会抛出 IllegalThreadStateException;
③ 主线程默认是用户线程,不可修改为守护线程。
3、完整标准代码示例
java
public class SetDaemonDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true){
System.out.println("守护线程后台执行...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 必须在start()之前设置守护线程
thread.setDaemon(true);
thread.start();
// 主线程休眠3秒后结束
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束,JVM退出,守护线程强制终止");
}
}
4、生产开发规范
① 守护线程不建议执行业务事务、文件写入、数据落库;
② 守护线程适合:日志采集、心跳检测、监控、定时巡检;
③ 线程池中一般全部设置为用户线程 ,不建议配置守护线程。
5、一句话总结 :setDaemon(true)设置守护线程,严格遵循先设置、后启动,启动后不可修改。
16.线程优先级有几级?(满分完整版+底层原理+面试坑点)
1、标准答案 :Java 线程优先级一共分为 1~10 级,一共10个等级。
2、三级划分(面试必背)
① 最低优先级(MIN_PRIORITY = 1) :后台辅助线程、低优先级巡检任务;
② 默认优先级(NORM_PRIORITY = 5) :主线程默认级别、普通业务线程;
③ 最高优先级(MAX_PRIORITY = 10):紧急任务、高优先级心跳、监控线程。
3、核心底层原理
Java优先级仅仅是给操作系统的建议值,属于**概率优先级**:优先级越高,获取CPU时间片概率越大;优先级低,抢到CPU概率低。
4、代码修改优先级示例
java
Thread thread = new Thread(()->{});
thread.setPriority(10); // 设置最高优先级
System.out.println(thread.getPriority()); // 获取优先级
5、高频面试坑点(必考)
① Java优先级不保证执行顺序 ,仅仅是建议,不能依赖优先级做业务顺序;
② 主线程默认优先级为5;
③ 优先级范围必须在1~10之间,超出范围抛出IllegalArgumentException;
④ 不同操作系统对优先级适配不同,Linux系统优先级权重极低。
6、一句话总结 :优先级一共十级、默认五级、仅影响CPU抢占概率、不保证执行顺序。
17.什么是上下文切换?(满分完整版+底层原理+生产优化)
1、官方定义
CPU在多线程执行过程中,暂停当前正在运行的线程,保存当前线程运行状态(上下文) ,加载下一条线程的状态并执行,这个过程叫做线程上下文切换。
2、通俗比喻
CPU就是一个写字的人,线程就是作业;写完A作业一半,把A作业合上(保存上下文),拿出B作业继续写,来回切换的动作就是上下文切换。
3、切换保存的内容
程序计数器、虚拟机栈、寄存器状态、程序运行指针、内存地址标识。
4、触发上下文切换的常见场景(面试必考)
① 线程时间片耗尽,CPU强制切换;
② 线程主动阻塞:sleep()、wait()、join()、锁阻塞;
③ 线程优先级调度,高优先级抢占CPU;
④ GC垃圾回收触发线程停顿。
5、上下文切换优缺点
① 优点:实现多线程并发,最大化利用CPU资源;
② 缺点:切换极其耗性能 ,保存+恢复状态消耗CPU算力,频繁切换会导致系统吞吐量下降、CPU飙升。
6、生产如何减少上下文切换(线上优化)
① 合理设置线程数量,不要大量创建无效线程;
② 减少锁竞争,缩小锁范围,避免大量阻塞;
③ 禁止频繁使用sleep、wait;
④ CPU密集型任务线程数 = CPU核心数,减少抢占。
7、一句话总结 :上下文切换就是线程换人执行、保存现场、恢复现场;频繁切换是性能杀手。
18.线程死循环如何优雅停止?
使用 volatile 布尔标记位控制退出。
19.为什么不推荐 stop () 销毁线程?
强制终止,资源无法释放,数据不一致。
20.线程组作用?
批量管理一组线程,日常开发极少使用。
二、锁机制 Synchronized(21-40 题)
1.synchronized 是什么?(满分完整版+特性+通俗讲解)
1、官方定义
synchronized 是 Java 原生内置悲观锁 ,无需手动导入类、无需手动释放锁;属于排他锁、独占锁、可重入锁、非公平锁 。用于解决多线程共享资源竞争问题,保证线程安全。
2、核心四大作用(面试必背)
① 保证原子性 :同步代码块内代码不可拆分,一次性执行完毕;
② 保证可见性 :加锁强制刷新主内存,线程之间数据实时可见;
③ 保证有序性 :禁止锁内部指令重排序;
④ 解决并发竞争 :同一时刻只允许一条线程获取锁,串行执行。
3、三种使用方式
① 修饰普通方法(对象锁) :锁住当前 this 对象,不同实例互不影响;
② 修饰静态方法(类锁) :锁住 Class 类对象,所有实例共用一把锁;
③修饰代码块(自定义锁) :锁住任意对象,锁范围最小、性能最优。
4、底层本质
底层依赖 对象头MarkWord + Monitor监视器锁 ,操作系统依赖mutex互斥量实现加锁。JDK1.6之后引入锁优化:偏向锁→轻量级锁→重量级锁,自动锁升级。
5、synchronized 核心特性
✅ 自动加锁、自动释放锁,无需人工干预;
✅ 异常自动释放锁,不会死锁卡死;
✅ 天然可重入,同一线程多次加锁不会阻塞;
❌ 不可中断、不能超时、不可手动灵活控制。
6、一句话总结 :synchronized是Java内置悲观独占锁,保证原子性、可见性、有序性;简单无脑、自动释放,底层依靠对象头完成锁升级。
2.synchronized 锁什么?(满分完整版+三类锁+代码区分+面试必考)
1、核心标准答案
synchronized 一共锁住三类对象:实例对象锁、类对象锁、自定义任意对象锁,对应三种使用方式,锁的对象不同,生效范围完全不同。
2、三类锁详细拆解(面试必背表格)
| 锁类型 | 锁住对象 | 生效范围 | 别称 |
|---|---|---|---|
| 普通实例方法 | 当前 this 实例对象 | 仅限制当前单个实例,不同实例互不阻塞 | 对象锁 |
| 静态方法 | 当前 Class 类对象 | 全局所有实例,所有对象共用一把锁 | 类锁 |
| 同步代码块 | 自定义任意对象 | 自定义控制锁范围,粒度最小、性能最优 | 自定义锁 |
3、三种锁代码直观演示
① 实例方法(锁this)
java
// 锁住当前实例对象this
public synchronized void test(){}
② 静态方法(锁Class)
java
// 锁住当前类的Class对象
public static synchronized void test(){}
③ 同步代码块(锁自定义对象)
java
// 可锁任意对象,推荐锁常量对象
synchronized (obj){
// 同步代码
}
4、高频面试重难点
① 对象锁和类锁互不互斥 :一个加实例锁、一个加类锁,多线程可以同时执行,不会阻塞;
② 尽量使用同步代码块:缩小锁范围,减少阻塞时间,提升并发性能;
③ 不要锁字符串常量、不要锁空对象,容易出现隐形死锁;
④ 类锁作用于所有对象,全局互斥,适合静态共享资源。
5、一句话总结:
普通方法锁实例、静态方法锁类、代码块锁任意对象;对象锁隔离实例,类锁全局隔离。
3.synchronized 底层原理?(满分完整版+三层底层+通俗易懂)
1、一句话极简背诵
底层依靠对象头MarkWord、Monitor监视器锁、操作系统Mutex互斥量 实现,JDK1.6优化锁升级机制。
2、三层底层结构(面试必背)
① 对象头 Mark Word(Java层)
每一个对象在内存中都有对象头,MarkWord占用内存大小:32位/64位。内部存放:锁标记位、偏向线程ID、GC分代年龄、哈希码。所有锁状态全部记录在MarkWord中,锁升级本质就是修改MarkWord二进制标识。
② Monitor 监视器锁(JVM层)
每个对象绑定一把Monitor,底层由C++实现。Monitor包含:拥有线程、等待池、锁池、计数器 。未抢到锁进入锁池阻塞,调用wait进入等待池。
③ Mutex 互斥量(操作系统层)
重量级锁依赖操作系统底层互斥量,线程需要从用户态切换为内核态,阻塞挂起,开销极大。
3、加锁执行流程
线程访问同步代码 → 判断对象头锁标记 → 无竞争偏向锁、轻微竞争轻量级锁、激烈竞争膨胀为重量级锁 → 未抢到锁进入锁池阻塞 → 释放锁唤醒下一个线程。
4、JDK1.6 锁优化机制
引入自动锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁 ,只能升级、不能降级,目的是在不同竞争场景下最大化提升性能。
5、字节码底层指令
同步代码块底层依赖:monitorenter、monitorexit 两条字节码指令;同步方法不需要指令,依靠方法标识位判定。
6、高频面试坑点
① synchronized锁的是对象 ,不是代码;
② 偏向锁、轻量级锁在用户态执行,无系统阻塞;
③ 只有重量级锁才会触发用户态切换内核态,性能最差;
④ 代码块会出现两次monitorexit,保证异常情况下自动释放锁。
7、终极总结:
MarkWord存锁状态、Monitor管控线程、Mutex实现阻塞;四层锁逐级升级,自适应优化性能。
4.Java 锁升级顺序?(满分完整版+升级流程+触发条件)
1、标准答案(必背)
JDK1.6 对 synchronized 进行锁优化,锁升级顺序:无锁 → 偏向锁 → 轻量级锁 → 重量级锁 。
2、核心硬性规则
① 锁只能升级,不能降级 ;
② 升级目的:适配不同竞争场景,最大化提升 synchronized 执行性能;
③ 所有锁状态信息全部存放在对象头 MarkWord 中。
3、四级锁逐一层级详解
① 无锁 :对象刚创建,无任何线程竞争,MarkWord无锁标记;
② 偏向锁(单线程场景) :只有一条线程反复加锁,消除不必要CAS,偏向当前线程;
③ 轻量级锁(轻微竞争) :多条线程交替抢锁、无阻塞,采用自旋CAS不停尝试抢锁;
④ 重量级锁(激烈竞争) :大量线程并发争抢,自旋失败膨胀为重量级锁,底层依赖Monitor,线程阻塞挂起。
4、锁升级触发条件
① 无锁→偏向锁:第一次线程加锁;
② 偏向锁→轻量级锁:出现第二条线程竞争、打破偏向;
③ 轻量级锁→重量级锁:自旋次数超过阈值、竞争激烈、自旋消耗CPU过大。
5、高频面试坑点
① JDK1.6之前只有重量级锁,没有锁升级;
② 偏向锁默认延迟开启,程序启动前几秒默认轻量级锁;
③ 一旦膨胀为重量级锁,永远不会降级;
④ 偏向锁适合单线程、轻量级适合轻微竞争、重量级适合高并发激烈竞争。
6、一句话总结:
无锁起步、单线程偏向、轻微竞争自旋、激烈竞争膨胀;只升不降,自适应优化性能。
5.偏向锁作用?
消除无竞争下锁 CAS 操作,提升单线程性能。
6.轻量级锁原理?
自旋 CAS 抢锁,用户态完成,不进入内核态。
7.自旋锁优缺点?
优点无阻塞;缺点占用 CPU,竞争激烈性能差。
8.重量级锁特点?
依赖内核态,线程阻塞挂起,开销大,竞争激烈稳定。
9.synchronized 可重入吗?
可重入,同一线程多次加锁不会死锁。
10synchronized 是否公平锁?
非公平锁,抢占式获取。
11.同步代码块和同步方法哪个效率高?(满分完整版+对比+开发规范)
1、标准答案 :同步代码块效率更高 。
2、核心根本原因
① 同步方法 :锁粒度极大,直接锁住整个方法、锁住当前this实例对象。哪怕方法内只有一小段代码需要同步,也会锁住全部代码,线程阻塞时间变长,并发性能差;
② 同步代码块 :可手动缩小锁范围,只包裹需要保证线程安全的共享资源代码,无关代码异步执行,减少线程阻塞抢占时间,吞吐量更高。
3、代码直观对比
java
// 1、同步方法:锁范围大,锁住整个方法,效率低
public synchronized void update(){
// 无关业务代码(不需要加锁)
log.info("日志打印");
// 仅这一行需要保证线程安全
count++;
}
// 2、同步代码块:精准加锁,只锁关键代码,效率高
public void update(){
log.info("日志打印"); // 无关代码无需加锁,并行执行
// 仅锁住共享资源代码,粒度最小
synchronized (this){
count++;
}
}
4、额外区别(面试加分)
① 同步方法:默认锁this(实例方法)、锁Class(静态方法),不能自定义锁对象 ;
② 同步代码块:支持自定义锁对象,灵活可控,还可以优化为专用锁常量,避免锁膨胀;
③ 同步方法可读性简单、写法粗暴;同步代码块灵活、性能最优。
5、生产开发规范
① 业务开发优先使用同步代码块 ,缩小锁范围是并发优化基础;
② 禁止无脑给所有方法加synchronized,会造成大量无意义阻塞;
③ 简单极简工具类、短逻辑代码可使用同步方法。
6、一句话总结:
锁范围越小性能越高,同步代码块粒度更小、灵活度更高,优于同步方法。
12.对象锁和类锁能否共存?(满分完整版+代码演示+面试高频坑点)
1、标准答案(必背)
可以共存、互不互斥、不会阻塞 。两者锁的对象不同,不是同一把锁,不存在锁竞争。
2、核心底层原因
① 对象锁 :锁住当前实例对象(this),锁的是堆中单个对象;
② 类锁 :锁住Class字节码对象,锁的是方法区唯一类对象;
两块不同的锁对象,互不干涉,所以一个线程获取对象锁、另一个线程获取类锁,可以同时执行 。
3、代码直观演示(证明共存不阻塞)
java
public class LockDemo {
// 类锁:静态同步方法
public static synchronized void staticMethod(){
while(true){
System.out.println("我是类锁(静态锁)");
}
}
// 对象锁:普通同步方法
public synchronized void instanceMethod(){
while(true){
System.out.println("我是对象锁(实例锁)");
}
}
public static void main(String[] args) {
LockDemo demo = new LockDemo();
// 开启线程1:执行类锁
new Thread(LockDemo::staticMethod).start();
// 开启线程2:执行对象锁
new Thread(demo::instanceMethod).start();
}
}
4、运行结果
两行代码交替同时打印 ,互相不阻塞,完美证明两把锁可以共存、无互斥关系。
5、面试超级易错坑点
① 很多求职者误以为synchronized全部互斥,只有锁同一个对象才会互斥 ;
② 同一实例下:对象锁+类锁并行执行、互不阻塞;
③ 不同实例下:对象锁互不阻塞,类锁全局互斥;
④ 开发中不要同时混用对象锁和类锁,逻辑混乱极易引发并发bug。
6、一句话总结:
锁对象不同就不互斥,对象锁锁实例、类锁锁Class,二者可以共存、并行执行。
13.synchronized 异常会释放锁吗?
会,线程抛出异常自动释放锁。
14.什么是锁消除?
JVM 编译优化,去除不可能存在竞争的锁。
15.什么是锁粗化?
连续多次加锁合并成一次,减少加锁次数。
16.偏向锁关闭场景?
多线程竞争激烈环境关闭偏向锁提升性能。
17.wait()、notify()、notifyAll() 区别?(满分完整版+表格+代码+面试必考)
1、核心共性
三者都是 Object 类的原生方法 ,必须在 synchronized 同步代码块/方法中使用,缺一报错非法监视器异常。作用于线程之间的等待与唤醒通信。
2、三者详细区别(面试必背表格)
方法名称作用功能锁行为线程去向 wait()让当前线程无限等待主动释放锁 进入对象等待池notify()随机唤醒一条等待线程执行完代码才释放锁从等待池移入锁池notifyAll()唤醒全部等待线程执行完代码才释放锁全部移入锁池竞争锁
3、逐方法深度讲解
① wait() :主动放弃当前持有的锁,进入无限等待状态,释放CPU资源,等待被其他线程唤醒;分为无参(无限等待)、有参(限时等待)。
② notify() :随机挑选一条处于等待池的线程唤醒,唤醒后不会立刻释放锁,必须执行完当前同步代码才释放。
③ notifyAll() :唤醒当前对象等待池中所有 等待线程,全部转移到锁池,重新竞争一把锁。
4、标准使用代码(生产者消费者极简示例)
java
// 等待线程
synchronized (lock){
while(条件不满足){
lock.wait(); // 释放锁、进入等待
}
}
// 唤醒线程
synchronized (lock){
lock.notify(); // 随机唤醒一条
// lock.notifyAll(); // 唤醒全部线程
}
5、高频面试坑点(必考)
① 三者必须在同步代码块执行,不加锁直接抛 IllegalMonitorStateException ;
② wait 立刻释放锁,notify/notifyAll 不会立刻释放锁,要等代码执行完毕;
③ notify 是随机唤醒,JVM不保证唤醒顺序;
④ 生产中尽量使用 notifyAll ,防止部分线程永久休眠(线程饿死)。
6、一句话总结:
wait放手等待、notify随机唤醒、notifyAll全部唤醒;全部必须搭配synchronized使用。
18.notify 唤醒线程顺序?
无序,由 JVM 调度决定。
19.为什么 wait 必须放在循环里?
防止虚假唤醒。
20.synchronized 优缺点?(满分完整版+表格+生产对比)
1、一句话极简背诵
优点:简单无脑、自动加解锁、安全稳定;缺点:功能死板、竞争激烈阻塞严重、灵活性差。
2、详细优点(面试必背)
① 使用简单、底层原生 :Java内置关键字,无需手动创建对象,编码简洁;
② 自动释放锁 :代码执行完毕、程序异常,JVM都会自动释放锁,不会死锁;
③ 天然锁特性齐全 :自带可重入、排他、原子性、可见性、有序性;
④ JVM持续优化 :JDK1.6引入偏向/轻量级/重量级锁,无竞争场景性能极高;
⑤ 底层稳定无BUG :由JVM底层保障,无需人为维护,生产故障率极低。
3、详细缺点(面试必背)
① 灵活性极差 :不可中断、不能超时、无法手动释放锁;
② 高并发性能差 :线程竞争激烈直接膨胀为重量级锁,内核态阻塞,开销大;
③ 非公平锁 :默认抢占机制,线程可能长期抢不到锁产生饥饿;
④ 锁粒度死板 :方法锁只能锁this/class,无法精细化控制;
⑤ 无法精准唤醒 :只能配合notify随机唤醒,不能指定唤醒某一条线程。
4、优缺点汇总表格
| 优点 | 缺点 |
|---|---|
| Java 原生内置关键字,使用简单、编码便捷,无需手动导入额外类 | 灵活性极低,不可中断、不能设置超时,无法手动灵活控制锁的释放时机 |
| 自动加锁、自动释放锁,代码执行完毕或抛出异常都会自动释放锁,不会造成锁泄漏死锁 | 高并发竞争激烈场景下,会膨胀为重量级锁,线程内核态阻塞挂起,性能大幅下降 |
| JVM 持续优化,低竞争、无并发场景下,偏向锁 / 轻量级锁性能表现优秀 | 默认非公平锁,无法修改为公平锁,存在线程长期抢不到锁的饥饿风险 |
| 底层由 JVM 保障,安全稳定,无内存泄漏风险,生产环境故障率极低 | 唤醒机制粗糙,仅支持 notify 随机唤醒、notifyAll 全部唤醒,无法精准唤醒指定线程 |
5、生产使用场景(加分考点)
✅ 适合:低并发、短代码、简单同步、少量竞争场景;
❌ 不适合:高并发、大量阻塞、需要超时、精准唤醒场景(改用ReentrantLock)。
6、一句话总结:
低竞争无脑用synchronized,简单稳定零BUG;高并发必须换Lock,灵活可控性能更强。
三、JUC 显式锁 Lock(41-60 题)
1.Lock 和 synchronized 区别?(满分完整版+表格+代码+生产选型)
1、一句话极简背诵
synchronized自动加解锁、简单粗暴;Lock手动加解锁、灵活强悍。低并发用synchronized,高并发、复杂锁场景必须用Lock。
2、七大核心区别(面试必背表格)
| 对比维度 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 底层归属 | Java 原生关键字,JVM 底层实现 | JDK 代码实现接口,属于 Java 代码层级 |
| 加锁方式 | 隐式锁,自动加锁、自动释放锁 | 显式锁,手动 lock () 加锁、unlock () 解锁 |
| 锁中断 | 不可中断,线程阻塞后会一直死等 | 可中断,lockInterruptibly () 可响应中断信号 |
| 超时机制 | 无超时机制,抢不到锁会永久阻塞 | 支持超时抢锁,tryLock (time) 可避免死锁 |
| 锁公平性 | 默认非公平锁,无法修改为公平锁 | 默认非公平锁,可手动传入 true 开启公平锁 |
| 唤醒机制 | 仅支持 notify 随机唤醒、notifyAll 全部唤醒,无法精准控制 | 搭配 Condition 可实现精准唤醒指定线程,分组管控 |
| 性能能力 | 低竞争场景性能优秀,高竞争激烈场景会膨胀为重量级锁,线程阻塞严重 | 高并发场景性能强悍,吞吐量更高,全程无内核态重量级阻塞 |
3、代码直观对比写法
① synchronized 写法(简洁自动)
java
// 无需手动解锁,异常自动释放锁
synchronized (obj){
// 同步业务代码
}
② Lock 写法(手动严谨)
java
// 必须手动解锁,必须搭配finally防止死锁
Lock lock = new ReentrantLock();
lock.lock(); // 手动加锁
try {
// 同步业务代码
}finally {
lock.unlock(); // 强制手动释放锁
}
4、Lock 独有四大强悍功能(面试加分)
① 可中断加锁 :线程阻塞时可被中断,避免永久阻塞;
② 超时加锁 :tryLock(time)指定时间抢不到锁直接放弃,防止死锁;
③ 公平锁实现 :new ReentrantLock(true)实现先来先排队;
④ 精准唤醒 :多个Condition精准唤醒某一组线程,而不是随机唤醒。
5、底层原理区别
① synchronized:依赖对象头MarkWord+Monitor,JVM管控,自动锁升级;
② Lock:底层基于AQS抽象队列同步器 ,CAS+双向队列,代码层面自旋抢锁。
6、生产开发选型规范(必考)
✅ 选用synchronized:代码简单、低并发、短同步代码、无复杂逻辑;
✅ 选用Lock:高并发、大量阻塞、需要超时、公平锁、精准唤醒、读写分离。
7、高频面试坑点
① Lock必须在finally中解锁,否则异常卡死造成死锁;
② synchronized锁升级后无法降级,Lock全程自旋无重量级阻塞;
③ JDK1.8之后synchronized性能已经无限接近Lock,日常简单业务优先原生锁。
8、一句话总结:
简单同步用synchronized省心,高并发复杂锁用Lock灵活;自动锁简单、手动锁强悍。
2.ReentrantLock 可重入原理?(满分完整版+源码+流程+通俗讲解)
1、极简定义
可重入指:同一线程可多次重复获取同一把锁,不会发生自我阻塞死锁。ReentrantLock 底层依靠 AQS 实现可重入机制。
2、底层核心三大要素(面试必背)
① state 状态值 :int 类型,记录锁持有次数;无锁为0,每加锁一次+1,解锁一次-1;
② exclusiveOwnerThread 独占线程 :记录当前持有锁的线程;
③ 计数器机制:同一线程反复加锁,不断累加次数,解锁必须逐次清零,彻底归零才算真正释放锁。
3、完整执行流程
① 第一次加锁:state=0,当前线程抢占成功,state 修改为 1,标记持有线程;
② 同一线程再次加锁:判断当前线程等于持有线程,不阻塞、直接放行 ,state+1;
③ 其他线程加锁:线程不一致,进入双向队列阻塞排队;
④ 解锁流程:每解锁一次 state-1,必须 state=0 才彻底释放锁 ,清空持有线程。
4、关键源码片段(AQS内部判断) // 判断是否为当前持有线程
java
if (current == exclusiveOwnerThread) {
// 重入次数累加
int nextc = c + acquires;
setState(nextc);
return true;
}
5、代码演示可重入特性 ReentrantLock lock = new ReentrantLock();
java
lock.lock(); // 第一次加锁 state=1
lock.lock(); // 同一线程二次加锁 state=2,不会阻塞
lock.unlock();// 解锁一次 state=1
lock.unlock();// 彻底解锁 state=0
6、高频面试坑点
① 加锁次数必须和解锁次数一致 ,否则锁永远无法释放,造成死锁;
② synchronized 底层也有可重入,依靠 Monitor 计数器;
③ ReentrantLock 重入次数理论无上限,受 int 最大值限制;
④ 不可跨线程重入,只有当前持有锁线程能重复加锁。
7、一句话总结:
依靠AQS的state计数器+持有线程标记,同线程累加次数实现重入;必须成对解锁清零,锁才算真正释放。
3.ReentrantLock 公平锁与非公平锁?(满分完整版+源码+对比+面试必考)
1、极简背诵定义
① 公平锁 :线程严格按照请求先后顺序排队,先来先获取锁,不允许插队,保证所有线程公平竞争;
② 非公平锁 :线程允许直接插队抢占锁,不遵循排队顺序,竞争激烈时可能出现线程饥饿,JUC默认非公平锁。
2、创建方式(源码构造方法)
java
// 1、非公平锁(默认):无参构造,生产默认推荐
ReentrantLock lock = new ReentrantLock();
// 2、公平锁:传入布尔值true,手动开启公平机制
ReentrantLock fairLock = new ReentrantLock(true);
3、底层AQS实现原理
① 公平锁底层 :线程抢锁前,优先判断AQS双向阻塞队列是否存在排队线程;队列不为空则当前线程直接入队排队,禁止插队,严格保证先来后到;
② 非公平锁底层 :线程上来直接通过CAS暴力抢占锁,不判断排队队列;抢占失败后,再进入阻塞队列排队,允许新线程插队。
4、核心区别对比表格(面试必背)
| 对比维度 | 公平锁 | 非公平锁(默认) |
|---|---|---|
| 抢占规则 | 严格排队、禁止插队、先来先服务 | 允许插队、CAS 抢占、暴力抢锁 |
| 线程饥饿 | 无线程饥饿,所有线程轮流获取锁 | 存在饥饿风险,老线程长期抢不过新线程 |
| CPU 开销 | 开销大,频繁触发线程上下文切换 | 开销小,减少线程切换次数 |
| 吞吐量 | 吞吐量偏低,执行速度慢 | 吞吐量高,并发性能更强 |
| 底层逻辑 | 需判断队列前驱节点,逻辑繁琐 | 直接 CAS 抢锁,逻辑简单高效 |
5、生产开发适用场景
✅ 选用公平锁 :任务执行耗时均匀、对执行顺序有要求、不允许线程饥饿、业务优先级一致;
✅ 选用非公平锁(主流) :绝大多数业务场景、追求高吞吐量、追求高性能、无严格执行顺序要求。
6、高频面试坑点(必考)
① ReentrantLock 默认非公平锁 ,也是生产环境推荐使用;
② synchronized 底层永久是非公平锁,没有任何方式修改为公平锁;
③ 非公平锁性能更高的本质:减少线程阻塞唤醒、降低上下文切换开销;
④ 公平锁会遍历AQS队列判断前驱节点,逻辑复杂,资源消耗更高。
7、一句话总结 :公平锁排队不插队、无饥饿性能低;非公平锁抢占可插队、高性能默认首选。
4.tryLock () 作用?
尝试获取锁,失败不阻塞,可设置超时时间。
5.lockInterruptibly () 作用?
可响应中断的加锁方式。
6.读写锁 ReentrantReadWriteLock 特点?(满分完整版+规则+代码+面试坑点)
1、极简背诵定义
ReentrantReadWriteLock 是可重入、读写分离锁 ,分为**读锁(共享锁)**和**写锁(排他锁)**,专门适配**读多写少**业务场景,提高并发吞吐量。
2、四大互斥规则(面试必考核心)
✅ 读读共享 :多个线程同时加读锁,不互斥、不阻塞,并发最高;
✅ 读写互斥 :一个读、一个写,互相阻塞,防止数据脏读;
✅ 写写互斥 :多个线程写操作,串行执行,保证数据安全;
✅ 读锁共享、写锁独占 :读无锁开销、写保证安全。
3、底层原理
① 基于AQS实现,内部维护两把锁:ReadLock读锁、WriteLock写锁;
② 使用一个int类型state高位记录读锁数量、低位记录写锁重入次数;
③ 支持公平/非公平锁、支持可重入、支持锁降级。
4、标准代码模板(生产常用)
java
// 创建读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获取读锁
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
// 获取写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
// 读操作:共享无阻塞
readLock.lock();
try{
// 查询、读取数据
}finally {
readLock.unlock();
}
// 写操作:独占互斥
writeLock.lock();
try{
// 修改、新增、删除数据
}finally {
writeLock.unlock();
}
5、优缺点详解
① 优点 :读多写少场景极大提升并发性能、避免独占锁全程串行、锁粒度灵活;
② 缺点 :存在读锁饥饿 ,大量读线程霸占锁,写线程长期等待;不适合写多场景。
6、高频面试坑点
① 只支持锁降级,不支持锁升级 :写锁可以转读锁,读锁不能转写锁;
② 读锁是共享锁、写锁是排他锁;
③ 写锁完全互斥,串行执行,保证数据一致性;
④ 生产大量缓存框架底层都是读写锁(本地缓存、多级缓存)。
7、一句话总结 :读读共享、读写互斥、写写互斥;读多写少专用锁,读快写慢,支持锁降级。
7.什么是锁降级?
写锁降级为读锁,JUC 支持,不支持锁升级。
8.StampedLock 是什么?
乐观读写锁,比读写锁并发更高。
9.Condition 作用?(满分完整版+精准唤醒+完整代码+面试对比)
1、极简背诵定义
Condition 是 Lock 锁的线程等待唤醒工具类,替代 synchronized 的 wait/notify;
实现精准唤醒、分组唤醒线程,解决原生notify随机唤醒的弊端。
2、核心底层作用(面试必背)
① 精准唤醒 :可以指定唤醒某一组线程,不再随机唤醒;
② 分组阻塞 :一把锁创建多个Condition,不同线程绑定不同等待队列,互不干扰;
③ 丰富功能 :支持超时等待、可中断等待、无参等待,比wait更强;
④ 依赖Lock :必须配合ReentrantLock使用,不能单独创建。
3、核心常用方法
java
// 线程等待,释放锁,进入等待队列
condition.await();
// 唤醒当前队列一条线程
condition.signal();
// 唤醒当前队列全部线程
condition.signalAll();
4、标准实操代码(精准唤醒演示)
java
public class ConditionDemo {
// 创建显式锁
private static final ReentrantLock lock = new ReentrantLock();
// 创建两组条件队列(分组)
private static final Condition conditionA = lock.newCondition();
private static final Condition conditionB = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
// 线程A:阻塞在conditionA
new Thread(() -> {
lock.lock();
try {
System.out.println("线程A等待...");
conditionA.await(); // A专属等待队列
System.out.println("线程A被精准唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
// 线程B:阻塞在conditionB
new Thread(() -> {
lock.lock();
try {
System.out.println("线程B等待...");
conditionB.await(); // B专属等待队列
System.out.println("线程B被精准唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
Thread.sleep(1000);
// 只精准唤醒线程A,不会影响线程B
lock.lock();
conditionA.signal();
lock.unlock();
}
}
5、Condition VS wait/notify(面试必考对比)
| 对比维度 | wait/notify | Condition |
|---|---|---|
| 所属锁体系 | synchronized 隐式锁 | Lock 显式锁 |
| 唤醒方式 | 随机唤醒,无法指定唤醒目标线程 | 精准分组唤醒,可指定唤醒对应等待队列的线程 |
| 等待队列 | 单个全局等待队列,所有等待线程共用 | 可创建多个独立等待队列,不同业务场景线程分组隔离 |
| 功能强度 | 功能简单,仅支持基础等待 / 唤醒,无超时可控能力 | 功能丰富,支持超时等待、可中断等待、多种等待模式,灵活度更高 |
6、高频面试坑点
① Condition 必须配合 Lock 使用,不加锁直接报错;
② await() 会主动释放锁,和wait一致;
③ signal() 不会立刻释放锁,执行完代码才释放;
④ 生产中生产者消费者模型优先使用 Condition,精准控制线程。
7、一句话总结:
Condition是Lock版的等待唤醒工具,多队列分组、精准唤醒,吊打原生notify随机唤醒。
10.Lock 如何避免死锁?(满分完整版+五种方案+代码实操+面试必考)
1、极简背诵定义
Lock 相比于synchronized自带多种**防死锁机制**,依靠手动可控加锁逻辑,从根源规避死锁;synchronized无法干预、只能阻塞死等,Lock可主动终止抢锁,避免线程永久卡死。
2、五大避免死锁核心手段(面试必背)
① tryLock() 超时加锁(最核心) :指定时间抢不到锁直接放弃,不会永久阻塞,打破死锁请求保持条件;
② lockInterruptibly() 可中断加锁 :阻塞中的线程可以被中断,主动跳出锁等待,破除死锁循环;
③ 统一加锁顺序 :多把锁严格固定获取顺序,杜绝循环等待,最基础防死锁手段;
④ 减少锁嵌套 :尽量不嵌套加锁,必须嵌套时控制层级,降低死锁概率;
⑤ finally强制解锁 :Lock必须在finally执行unlock,异常也不会造成锁不释放。
3、重点方法代码演示(防死锁核心代码)
① tryLock超时避免永久阻塞
java
// 5秒内抢不到锁直接放弃,不会死等
if (lock.tryLock(5, TimeUnit.SECONDS)){
try{
// 执行业务逻辑
}finally {
lock.unlock();
}
}else{
// 抢锁失败,直接退出,规避死锁
System.out.println("抢锁失败,放弃执行");
}
② lockInterruptibly可中断阻塞线程
java
try {
// 阻塞过程可被中断,跳出死锁等待
lock.lockInterruptibly();
} catch (InterruptedException e) {
// 收到中断信号,主动终止抢锁
System.out.println("线程中断,终止抢锁");
}
4、Lock vs synchronized 死锁对比
| 对比维度 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 阻塞特性 | 不可中断,永久死等 | 可中断、可超时、主动放弃 |
| 死锁规避 | 无任何规避手段 | 超时 + 中断双重防死锁 |
| 锁释放 | 自动释放,无法干预 | 手动释放,可控性强 |
5、高频面试坑点
① 死锁四大必要条件:互斥、请求保持、不可剥夺、循环等待 ;Lock主要破坏【请求保持、不可剥夺】两个条件;
② tryLock是生产最常用防死锁手段,适配多锁嵌套场景;
③ Lock必须手动unlock,若漏写解锁,依然会造成人为死锁;
④ synchronized只能靠代码顺序优化防死锁,无底层补救方法。
6、一句话总结:
Lock依靠超时抢锁、中断抢锁、手动解锁破坏死锁条件;synchronized只能死等,无补救措施。
11.AQS 是什么?(满分完整版+架构+通俗讲解+面试必考)
1、极简背诵定义
AQS 全称 AbstractQueuedSynchronizer 抽象队列同步器 ,是 **JUC 并发包底层核心基石框架**。Java中所有显式锁、并发工具类底层全部依赖AQS,它封装了抢锁、排队、阻塞、唤醒通用逻辑,是锁的底层模板。
2、核心定位
AQS 是一套**通用锁模板**,Doug Lea 编写,把加锁、释放锁、线程排队全部封装,子类只需要简单重写少量方法,就能快速实现自定义锁。
3、AQS 三大核心组成(必背)
① state 状态变量(核心灵魂) :int 类型,volatile修饰;
-
0:无锁状态;
-
大于0:线程持有锁,数值代表重入次数;
-
不同工具类含义不同:ReentrantLock代表重入次数、CountDownLatch代表计数器、Semaphore代表许可数量。
② 双向FIFO阻塞队列 :存放抢锁失败的线程;
采用双向链表,包含head头节点、tail尾节点;未抢到锁的线程封装成Node节点入队休眠,节约CPU资源。
③ 独占线程标记 :exclusiveOwnerThread,记录当前正在持有锁的线程,用于实现可重入判断。
4、两大锁模式(面试必考)
① 独占模式(排他锁) :同一时刻只能一个线程持有锁;代表类:ReentrantLock。
② 共享模式(共享锁) :多个线程可同时获取锁;代表类:CountDownLatch、Semaphore、读写锁。
5、底层完整执行流程
① 线程尝试通过CAS修改state抢锁;
② 抢锁成功:标记当前独占线程,执行业务;
③ 抢锁失败:封装成Node节点,加入双向阻塞队列;
④ 队列中线程挂起休眠,避免空转消耗CPU;
⑤ 持有锁线程释放锁,唤醒队列中下一个节点线程继续抢锁。
6、AQS 核心底层方法
java
// 独占模式
acquire() // 获取锁
release() // 释放锁
// 共享模式
acquireShared() // 获取共享锁
releaseShared() // 释放共享锁
7、高频面试坑点
① AQS 采用**模板方法设计模式**,通用逻辑父类实现,差异化逻辑子类重写;
② 阻塞队列中的线程是**挂起状态**,不消耗CPU,区别于自旋锁;
③ state必须用volatile修饰,保证多线程可见性;
④ synchronized底层没有使用AQS,只有JUC显式锁依赖AQS。
8、一句话总结:
AQS是JUC锁底层通用模板,依靠state状态值+双向阻塞队列+独占线程,统一实现加锁、排队、唤醒逻辑,是所有显式锁的底层祖宗。
12.AQS 核心原理?(满分完整版+通俗拆解+执行流程+面试必考)
1、极简背诵定义
AQS核心原理:依靠state状态变量 + 双向FIFO阻塞队列 + 线程独占标记 ,配合CAS自旋,统一实现线程**加锁、排队、阻塞、唤醒**全套并发逻辑。
2、三大核心底层组件(必背)
① State 状态变量(核心) :volatile int 修饰,同步器核心计数器;通过CAS修改值判断加锁/释放锁,不同锁含义不同:ReentrantLock为重入次数、读写锁拆分高低位、计数器为剩余次数。
② 双向阻塞队列 :抢锁失败的线程封装为Node节点,存入队列;双向链表结构、头尾指针,保证FIFO先进先出,线程入队后park挂起,不消耗CPU。
③ 独占线程标记 :exclusiveOwnerThread,记录当前持有锁的线程,判定同线程重入、线程归属。
3、完整执行底层流程
① 线程CAS尝试修改state抢锁;
② 抢锁成功:修改state、标记独占线程,执行业务;
③ 抢锁失败:封装Node节点,尾插法进入双向队列;
④ 线程park休眠阻塞,让出CPU;
⑤ 锁释放:修改state,唤醒下一个有效后继节点;
⑥ 被唤醒线程再次竞争锁,循环往复。
4、两种工作模式
① 独占模式 :单线程持有锁,排他执行,如ReentrantLock;
② 共享模式 :多线程同时持有锁,如CountDownLatch、读写锁。
5、底层关键技术
① CAS无锁自旋 :无竞争时不阻塞,轻量修改state;
② LockSupport.park() :阻塞队列线程,休眠节省CPU;
③ 模板方法模式 :父类通用排队逻辑,子类重写tryAcquire自定义抢锁。
6、高频面试坑点
① AQS队列内线程是挂起状态 ,不是自旋,不耗CPU;
② 队列节点采用尾插法 ,保证排队顺序;
③ 虚假唤醒:队列唤醒后仍需判断state状态,防止异常唤醒;
④ 只有JUC显式锁依赖AQS,synchronized底层无AQS。
7、一句话总结:
一变量、一队列、一标记;CAS抢锁、队列排队、park休眠、有序唤醒,这就是AQS全部核心原理。
13.AQS 独占与共享模式?(满分完整版+通俗易懂+对比+面试必考)
1、极简背诵定义
AQS把所有锁分为两大类:独占模式(排他锁) 、共享模式(共享锁)。
两种模式底层都是依靠state+双向队列,区别在于:同一时刻允许几条线程同时持有锁。
2、独占模式(Exclusive)详解
① 核心特点 :同一时刻只能有一条线程 持有锁,排他执行、互斥阻塞;
② 底层逻辑 :CAS修改state,state≠0代表锁被占用,其他线程全部入队阻塞;
③ 代表类 :ReentrantLock、Synchronized底层、WriteLock写锁;
④ 适用场景 :修改数据、增删改、强一致性、防止并发篡改。
3、共享模式(Shared)详解
① 核心特点 :同一时刻多条线程可以同时持有锁 ,并发执行、互不阻塞;
② 底层逻辑 :state代表许可数量/资源阈值,只要state>0,线程就能获取共享锁;
③代表类 :CountDownLatch、Semaphore、ReadLock读锁;
④ 适用场景 :查询数据、限流、批量等待、读多写少场景。
4、核心方法对照表(面试必背)
| 对比维度 | 独占模式(排他锁) | 共享模式(共享锁) |
|---|---|---|
| 持有线程数 | 同一时刻仅 1 条线程可持有锁,排他执行 | 允许多条线程同时持有锁,并发执行 |
| 核心方法 | acquire() / release() |
acquireShared() / releaseShared() |
| state 变量含义 | 记录锁重入次数,0 = 无锁状态 | 记录剩余许可数量 / 计数器阈值 |
| 线程唤醒机制 | 仅唤醒队列中下一个有效后继节点线程,串行接力抢锁 | 向后传播唤醒,连续唤醒后续一批共享节点线程,批量放行 |
| 典型实现类 | ReentrantLock、ReentrantReadWriteLock 写锁 | ReentrantReadWriteLock 读锁、CountDownLatch、Semaphore |
5、底层关键区别(面试加分)
① 独占模式 :唤醒线程时,只唤醒队列中下一个 有效线程,串行接力抢锁;
② 共享模式 :唤醒线程时,会向后传播唤醒 ,连续唤醒后面所有共享节点,实现批量放行;
③ 读写锁同时包含两种模式:写锁独占、读锁共享,这也是AQS高级混合用法。
6、高频面试坑点
① AQS本身不实现锁,只定义两种模式模板,子类按需实现;
② 独占锁保证数据强一致性,共享锁保证高并发吞吐量;
③ 共享模式下存在**无限向后唤醒**,提高并发放行效率;
④ 没有任何类同时大量混用两种模式,仅有读写锁特殊混合使用。
7、一句话总结:
独占单线程排他执行、保证数据安全;共享多线程并行执行、提高并发吞吐量,二者构成AQS全部锁模型。
14.AQS 阻塞队列原理?(满分完整版+通俗易懂+流程拆解+面试必考)
1、极简背诵定义
AQS阻塞队列本质是双向Node节点链表(FIFO) ,专门存放抢锁失败的线程;通过尾插法入队、前驱唤醒后继,配合park休眠,实现线程有序排队、无CPU空转,是AQS管控阻塞线程的核心容器。
2、队列核心组成(必背)
① Node节点 :每一个抢锁失败的线程被封装为Node,存储线程引用、等待状态、前后指针;
② 头尾指针 :head头节点、tail尾节点,默认初始化空哨兵节点,避免链表判空;
③ 等待状态waitStatus :标记节点状态(等待、取消、唤醒),控制线程流转;
④ 双向指针 :prev前驱、next后继,形成双向链表,方便回溯遍历、精准唤醒。
3、四大节点状态(面试冷门加分)
① SIGNAL(-1) :后继节点需要唤醒,前驱节点释放锁后唤醒下一个;
② CANCELLED(1) :线程超时/中断,节点取消排队,永久失效;
③ CONDITION(-2) :线程在Condition条件队列等待;
④ 0 :节点初始默认状态。
4、完整入队执行流程(抢锁失败)
① 线程CAS抢锁失败,不反复无效自旋;
② 封装为Node节点,通过CAS尾插法 插入链表尾部;
③ 修改前驱节点状态为SIGNAL,标记需要唤醒;
④ 调用LockSupport.park(),线程挂起休眠、释放CPU;
⑤ 进入阻塞队列安静排队,不消耗系统资源。
5、完整出队执行流程(锁释放)
① 持有锁的线程修改state、释放锁;
② head头节点向后寻找第一个有效后继节点;
③ 调用unpark()唤醒该节点线程;
④ 被唤醒线程重新CAS抢占锁;
⑤ 抢锁成功后清空节点、移动头指针,废弃无效节点。
6、关键底层规则(面试必考)
① 哨兵节点机制 :队列初始化就创建空head节点,简化头尾判断逻辑;
② 前驱唤醒后继 :永远由前一个节点唤醒下一个节点,不会跳跃唤醒;
③ 剔除取消节点 :唤醒过程中自动清理超时、中断的无效节点;
④ 非公平锁插队 :新线程可直接CAS抢锁,不用入队排队。
7、高频面试坑点
① 队列线程是park挂起状态 ,不是自旋,零CPU消耗;
② 必须双向链表:方便删除中间取消的节点,单向链表无法回溯;
③ 头节点永远是无效哨兵,真正排队线程从第二个节点开始;
④ 共享模式会链式唤醒,独占模式只唤醒单个后继节点。
8、一句话总结:
双向链表存线程、尾插排队、前驱唤醒后继、park休眠省CPU;哨兵节点简化逻辑,自动剔除无效节点,这就是AQS阻塞队列全部原理。
15.volatile 关键字作用?(满分完整版+三大特性+底层原理+代码+面试必考)
1、极简背诵定义
volatile 是 Java 轻量级同步关键字,无锁、不阻塞 ;专门解决多线程内存不可见、指令重排问题,不保证原子性,是轻量级并发安全手段,底层依托内存屏障实现。
2、三大核心作用(面试必背、必考)
① 保证可见性 :修饰变量,一条线程修改后,强制刷新主内存,其他线程立刻感知最新值,消除工作内存缓存差异;
② 禁止指令重排序 :底层添加内存屏障,禁止编译器、CPU对代码乱序优化,保证代码执行有序;
③ 不保证原子性:只能保证单次读写可见,复合操作(i++、自增累加)无法保证线程安全。
3、底层实现原理
① 内存可见性 :volatile变量修改后,触发CPU缓存失效,强制写入主内存,其他线程缓存失效重新读取;
② 内存屏障 :插入LoadLoad、StoreStore屏障,禁止上下代码指令交换位置,杜绝重排序;
③ 无锁机制 :不占用锁资源、不阻塞线程,纯硬件指令优化,性能远超synchronized。
4、代码演示(可见性经典案例)
java
// 不加volatile:主线程永远死循环,看不到修改后flag
// 加volatile:主线程立刻感知,跳出循环
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag){
// 空循环
}
System.out.println("线程感知变量变化,退出循环");
}).start();
Thread.sleep(2000);
flag = false; // 修改变量
}
5、为什么不保证原子性?(高频追问)
原子性需要:读取→修改→写入三步操作;
volatile只能保证单次读写可见,中间修改步骤无法加锁,多线程并发修改会出现数据覆盖。典型例子:volatile修饰int,i++依旧线程不安全。
6、volatile 与 synchronized 区别(必背表格)
| 对比维度 | volatile | synchronized |
|---|---|---|
| 锁级别 | 轻量级无锁 | 重量级独占锁 |
| 阻塞特性 | 不阻塞、无上下文切换 | 阻塞线程、开销大 |
| 保障能力 | 可见性、有序性,无原子性 | 原子性、可见性、有序性 |
| 适用场景 | 状态标记、简单变量同步 | 同步代码块、复合操作 |
| 底层原理 | 内存屏障 + 缓存失效 | Monitor 锁 + 对象头 |
7、高频面试坑点
① volatile 绝对不能替代锁 ,无法解决自增、复合运算线程安全;
② JDK1.5之后volatile才真正生效,修复早期内存模型漏洞;
③ 不修饰方法、不修饰常量,只修饰成员变量 ;
④ 禁止重排序可以解决DCL单例模式空指针问题。
8、一句话总结:
volatile保证可见性、禁止重排、不保证原子;轻量无锁性能高,只做状态标记,不做计算修改。
16.volatile 适用场景?(满分完整版+四大实战场景+代码+禁忌+面试必考)
1、极简背诵定义
volatile 适合简单轻量、无复合计算、仅读写标记 的场景;依靠可见性+禁止重排序特性,在不使用锁的前提下保证线程感知变量变化,**绝对不能用于多线程计算、数据累加**。
2、四大生产常用场景(面试必背)
① 状态标记位(最常用)
用于线程启停、开关控制,多线程只读、单线程修改;例如线程终止标记、服务启停开关、全局判断标识。
java
// 优雅终止线程标记位
private static volatile boolean running = true;
② 双重校验锁DCL单例模式
禁止指令重排序,解决对象初始化空指针问题,保证单例对象创建有序,是volatile经典高频面试场景。
③ 读写分离、简单共享变量
一写多读场景:一条线程修改、多条线程读取;保证读线程实时拿到最新值,无需加锁,性能极高。
④ 底层并发工具修饰
JUC底层大量使用:AQS的state、Atomic原子类value、线程池变量,依靠volatile保证内存可见。
3、经典实战代码(DCL单例)
java
public class Singleton {
// volatile禁止指令重排,防止半初始化对象溢出
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
4、绝对不适用场景(面试大坑)
① 不能用于复合运算 :i++、自增、加减乘除,无法保证原子性;
② 不能作为共享计数器 :多线程同时修改会数据覆盖;
③ 不能解决并发修改冲突 :仅保证可见,无互斥能力。
5、生产开发硬性规范
✅ 推荐:开关标记、状态位、配置热更新、一写多读;
❌ 禁止:数值计算、累加统计、多线程同时修改。
6、高频面试坑点
① volatile只能修饰成员变量,局部变量无效;
② 仅保证单次读写原子性,复合操作一律不安全;
③ 高并发修改场景必须改用synchronized/CAS原子类;
④ DCL单例必须加volatile,防止JVM指令重排造成空指针。
7、一句话总结:
只做标记不做计算、只做读取不做修改;一写多读专用,杜绝复合运算,这就是volatile使用边界。
17.CAS 原理?(满分完整版+通俗拆解+源码+流程+面试必考)
1、极简背诵定义
CAS 全称 Compare And Swap(比较并交换) ,是Java无锁乐观锁 ;不加锁、不阻塞、依靠硬件指令,自旋比较内存值,实现线程安全修改,底层依赖Unsafe类,是AQS、原子类的底层核心。
2、CAS三大核心参数(必背)
① 内存地址V :要修改的共享变量在内存中的真实地址;
② 预期旧值A :线程读取到的当前变量旧值;
③ 更新新值B :线程想要修改写入的目标新值。
3、底层执行流程(通俗易懂)
① 线程从主内存读取变量,记录为预期旧值A;
② CAS判断:内存地址V中的真实值 是否等于 预期旧值A;
③ 相等:无其他线程修改,直接交换,写入新值B,修改成功 ;
④ 不相等:被其他线程篡改,本次修改失败,重新自旋重试 ;
⑤ 循环反复判断,直到修改成功为止。
4、底层源码与硬件原理
① Java层面:调用 Unsafe 类native本地方法,无Java实现源码;
② JVM层面:封装汇编指令;
③ 硬件层面:依靠 CPU cmpxchg 原子指令,硬件级别保证操作不可拆分;
④ 单核/多核CPU均会加总线锁,杜绝并发篡改,保证原子性。
5、简易代码演示(逻辑模拟)
java
// CAS伪代码逻辑,通俗易懂
public boolean CAS(V内存地址, A预期旧值, B新值){
if(内存真实值 == A){
内存真实值 = B;
return true; // 修改成功
}
return false; // 被篡改,修改失败
}
6、CAS核心优点
① 无锁无阻塞 :不触发线程阻塞、无上下文切换;
② 性能极高 :用户态自旋,开销远小于synchronized重量级锁;
③ 乐观并发 :默认竞争不激烈,适合轻量并发场景。
7、高频面试坑点
① CAS只能保证单个变量原子性 ,无法保证多行代码复合原子性;
② CAS是乐观锁 ,synchronized是悲观锁;
③ 自旋空转消耗CPU,竞争极端激烈场景性能不如重量级锁;
④ 底层必须依赖Unsafe类,无法手动直接调用。
8、一句话总结:
拿旧值比对内存,相等就改、不等重试;硬件指令保证原子、无锁自旋高性能,这就是CAS全部原理。
18.CAS 三大问题?(满分完整版+通俗拆解+代码+解决方案+面试必考)
1、极简背诵定义
CAS虽然无锁高性能,但是存在ABA问题、循环自旋空转、只能单变量原子性 三大硬伤;生产必须针对性解决,否则出现并发BUG。
2、第一大问题:ABA问题(最经典面试题)
① 问题原理 :线程A读取旧值A,还未CAS修改;线程B先把A改成B、又改回A;线程A判定数值没变,修改成功,忽略中间篡改过程 ,产生数据隐患。
② 通俗比喻 :钱包100元→被人拿走变0→又补回100元;你只看到最后还是100,不知道中间被动过手脚。
③ 代码演示隐患 :普通AtomicInteger无法识别ABA篡改。
④ 解决方案 :添加版本号/时间戳 ;使用带标记的引用类:AtomicStampedReference。
3、第二大问题:循环自旋空转(CPU消耗)
① 问题原理 :竞争激烈时,CAS一直修改失败,无限循环重试;线程不断空转CAS,占用CPU资源,CPU飙升。
② 痛点场景 :大量线程同时修改同一个变量,自旋次数暴增,性能碾压重量级锁。
③ 解决方案 :
✅ 增加自旋次数上限,超限改用重量级锁;
✅ JDK自适应自旋,控制循环次数;
✅ 高并发竞争场景直接使用synchronized、Lock锁。
4、第三大问题:只能保证单个变量原子性
① 问题原理 :CAS硬件指令只能针对一个内存地址 做原子修改;无法同时操作多个变量、无法保证多行代码复合原子性。
② 业务痛点 :同时修改两个共享变量,CAS无法保证数据一致性。
③ 解决方案 :多行复合操作、多变量修改,必须使用悲观锁synchronized/Lock 。
5、三大问题+对应解决方案汇总表(必背)
| 存在问题 | 产生原因 | 解决方案 |
|---|---|---|
| ABA 问题 | 数值反复修改、无修改记录版本号 | 添加版本号 / 时间戳(使用 AtomicStampedReference) |
| 循环自旋空转 | 竞争激烈、修改失败无限重试 | 限制自旋次数上限,竞争激烈场景改用悲观锁 |
| 单变量原子性限制 | 硬件 CAS 指令仅能锁定单个内存地址 | 多变量 / 复合操作场景,改用 Lock/synchronized 悲观锁 |
6、高频面试坑点
① CAS不能杜绝ABA ,普通原子类默认存在ABA漏洞;
② 自旋空转是CPU密集消耗,高并发慎用CAS;
③ CAS只能保证行内原子,不能替代锁做复杂业务;
④ JUC中AQS、原子类均规避了CAS缺陷,做了优化封装。
7、一句话总结:
ABA要看版本、自旋怕高并发、只能改单变量;三大缺陷各有解法,复杂业务放弃CAS改用锁。
19.ABA 问题如何解决?(满分完整版+双代码演示+通俗详解+面试必考)
1、极简背诵定义
ABA核心解决思路:不比较数值,比较修改版本 ;给数据添加版本号/时间戳,记录每一次修改记录,哪怕数值变回原值,版本号不可逆递增,彻底规避ABA篡改漏洞。
2、原生缺陷:普通原子类存在ABA漏洞
AtomicInteger、AtomicLong 仅对比数值,无修改记录;数值A→B→A,线程无法识别中间篡改,判定无修改,存在安全隐患。
3、官方解决方案(面试必背)
① AtomicStampedReference(主流) :维护引用值+版本号 ,每次修改版本号自增,比对数值+版本号双重校验;
② AtomicMarkableReference :维护引用值+布尔标记 ,仅标记是否修改,不记录修改次数;
③ 自定义时间戳 :业务中给共享变量附加修改时间戳,替代版本号,逻辑一致。
4、底层解决原理
普通CAS:只对比【内存值、预期值】;
改良CAS:同时对比【内存值、预期值、内存版本、预期版本】;
只有数值+版本号全部一致 ,才判定无篡改,允许修改,完美杜绝ABA。
5、代码演示1:普通AtomicInteger产生ABA问题
java
// 普通原子类:存在ABA漏洞
AtomicInteger atomic = new AtomicInteger(100);
new Thread(()->{
// 1、读取初始值100
int old = atomic.get();
try {Thread.sleep(2000);} catch (Exception e) {}
// 3、时隔两秒,尝试修改,依旧修改成功(存在隐患)
atomic.compareAndSet(old, 200);
}).start();
new Thread(()->{
// 2、中间篡改:100→0→100,数值还原
atomic.compareAndSet(100, 0);
atomic.compareAndSet(0, 100);
}).start();
6、代码演示2:AtomicStampedReference彻底解决ABA
java
// 带版本号原子引用:杜绝ABA
// 参数:初始值、初始版本号
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 1);
new Thread(()->{
// 获取当前值+当前版本
int oldValue = ref.getReference();
int oldStamp = ref.getStamp();
try {Thread.sleep(2000);} catch (Exception e) {}
// 数值相同、版本不同,修改失败,拦截ABA篡改
boolean success = ref.compareAndSet(oldValue,200,oldStamp,oldStamp+1);
System.out.println("是否修改成功:"+success); // 输出false
}).start();
new Thread(()->{
// 两次修改,版本号持续递增
ref.compareAndSet(100,0,ref.getStamp(),ref.getStamp()+1);
ref.compareAndSet(0,100,ref.getStamp(),ref.getStamp()+1);
}).start();
7、两个工具类区别(面试冷门加分)
| 对比维度 | AtomicStampedReference | AtomicMarkableReference |
|---|---|---|
| 核心标记类型 | 整型版本号(stamp),每次修改版本号不可逆递增 | 布尔标记(mark),仅记录是否被修改过,无次数统计 |
| 修改记录能力 | 完整记录每一次修改的版本迭代,可精准统计修改次数、追溯修改轨迹 | 仅能标记是否发生过修改,无法记录修改次数、无法追溯多次修改的过程 |
| ABA 问题解决能力 | 彻底解决 ABA 问题,哪怕数值变回原值,版本号不同也能识别中间篡改过程 | 仅能解决单次 ABA 问题,无法应对多次来回修改的复杂 ABA 场景 |
| 适用场景 | 需要精准统计修改次数、追溯修改轨迹、严格校验数据完整性的场景,如库存扣减、订单状态流转 | 仅需判断数据是否被修改过、无需关注修改次数的简单场景,如开关标记、状态校验 |
| 底层实现复杂度 | 双参数(引用 + 版本号)CAS 操作,逻辑略复杂,需同时校验引用和版本号 | 单参数(引用 + 布尔标记)CAS 操作,逻辑更简单,仅需校验引用和标记位 |
| 性能开销 | 版本号递增 + 双值校验,自旋成功率略低,性能开销略高 | 布尔标记 + 单值校验,自旋成功率更高,性能开销更低 |
8、高频面试坑点
① JDK原生普通原子类全部无法规避ABA ,必须手动改用带版本原子引用;
② 版本号只能递增、不能回退,保证修改记录不可逆;
③ AtomicStampedReference自旋成功率更低,性能略低于普通原子类;
④ 业务简单场景可用时间戳替代版本号,逻辑一致。
9、一句话总结:
ABA问题本质是只看结果不看过程;加版本号双重校验,记录修改轨迹,是最简单通用的解决方案。
20.原子类原理?(满分完整版+分类+底层+代码+面试必考)
1、极简背诵定义
原子类是JUC封装的无锁线程安全工具类 ,底层核心依靠 CAS + volatile + Unsafe 实现;不加锁、不阻塞,保证多线程下变量操作原子性,性能远超synchronized同步锁,专门解决基础变量并发安全问题。
2、底层三大核心支柱(面试必背)
① CAS无锁自旋 :硬件级比较交换,失败循环重试,替代重量级锁实现原子修改;
② volatile 可见性 :修饰共享变量,保证多线程内存可见、禁止指令重排;
③ Unsafe 底层类 :提供native本地方法,直接操作内存地址,调用CPU原子指令。
3、原子类四大分类(面试思维导图)
① 基础数据原子类 :AtomicInteger、AtomicLong、AtomicBoolean;适配普通数值计算;
② 引用类型原子类 :AtomicReference、AtomicStampedReference;解决对象修改、ABA问题;
③ 数组原子类 :AtomicIntegerArray、AtomicLongArray;保证数组内元素线程安全;
④ 累加器增强类 :LongAdder、DoubleAdder;高并发计数,分段锁优化,性能碾压普通原子类。
4、底层执行流程(通俗易懂)
① 通过Unsafe获取变量内存偏移地址;
② volatile修饰变量,实时读取主内存最新值;
③ CAS比对内存旧值,一致则原子修改;
④ 修改失败无限自旋重试,直到修改成功;
⑤ 全程无锁、无阻塞、用户态执行。
5、基础原子类代码演示
java
// 普通int多线程不安全,AtomicInteger保证原子性
AtomicInteger count = new AtomicInteger(0);
// 自增方法,底层CAS自旋
count.incrementAndGet();
// 底层源码逻辑:CAS循环比对修改
6、普通原子类 VS LongAdder(高频面试对比)
| 对比维度 | 普通原子类(AtomicInteger/AtomicLong) | LongAdder |
|---|---|---|
| 底层原理 | 单 CAS 自旋修改,全程对单个内存地址自旋抢锁 | 分段累加、分散竞争,将压力拆分到多个单元格,降低锁冲突 |
| 并发性能 | 低,高并发场景下大量线程自旋空转,CPU 消耗高 | 极高,高并发下拆分竞争压力,吞吐量碾压普通原子类 |
| ABA 问题 | 存在 ABA 漏洞,数值来回修改无法识别中间篡改过程 | 无 ABA 问题,仅做数值累加,无版本号校验需求,天然规避 ABA 风险 |
| 适用场景 | 普通低并发计数、轻量数值修改场景 | 超高并发统计、接口流量统计、PV/UV 统计、大流量业务计数场景 |
7、高频面试坑点(必考)
① 原子类 只能保证单个变量原子性 ,无法解决多行代码复合原子性;
② 普通原子类存在ABA问题,必须使用带版本号引用类规避;
③ 高并发计数优先 LongAdder ,放弃AtomicInteger,减少CPU空转;
④ 所有原子类底层都不依赖锁,属于乐观无锁并发;
⑤ JDK1.8新增Adder累加器,专门优化高并发竞争痛点。
8、一句话总结:
原子类依托CAS自旋+volatile可见性+Unsafe内存操作;无锁安全、轻量高效,普通计数用Atomic,超高并发用LongAdder。
四、并发工具类(61-80 题)
1.CountDownLatch 作用?(满分完整版+底层+代码+对比+面试必考)
1、极简背诵定义
CountDownLatch 是一次性递减计数器,属于AQS共享模式实现;允许**一条主线程等待多条子线程全部执行完毕**,再继续向下执行,起到线程批量等待、汇总结果作用,执行完毕不可重置复用。
2、底层核心原理(面试必背)
① 基于AQS共享模式,state作为计数器 ,初始化设定计数次数;
② countDown():CAS递减state计数器,计数减一;
③ await():阻塞主线程,直到state递减为0,批量唤醒等待主线程;
④ 底层使用LockSupport.park()阻塞、unpark()唤醒,无锁阻塞、性能高。
3、标准实操代码(生产通用模板)
java
// 初始化计数器,设定3个子线程
CountDownLatch latch = new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
new Thread(()->{
try {
// 子线程执行业务
System.out.println(Thread.currentThread().getName()+"执行任务");
} finally {
// 计数器必须放在finally,保证一定递减
latch.countDown();
}
},"线程"+i).start();
}
// 主线程阻塞,等待全部子线程完成
latch.await();
System.out.println("所有子线程执行完毕,主线程汇总执行");
4、三大核心常用方法
① CountDownLatch(int count) :构造方法,初始化计数器次数;
② countDown() :计数器减一,线程执行完调用;
③ await() :阻塞当前线程,等待计数器归零;
④ await(time,unit) :限时等待,超时自动放行,防止永久阻塞。
5、生产真实使用场景(面试加分)
① 并行任务汇总 :多线程拆分查询数据,全部查询完统一组装返回;
② 服务启动预热 :项目启动加载多个资源,全部加载完再对外提供服务;
③ 压测并发模拟:统一放行大量线程,模拟高并发同时请求。
6、CountDownLatch VS CyclicBarrier(高频对比)
| 对比维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 复用性 | 一次性使用、不可重置 | 循环复用、可重复拦截 |
| 等待对象 | 主线程等待子线程全部执行完成 | 子线程之间互相等待,集齐指定数量线程 |
| 核心逻辑 | 计数器递减,计数归零自动放行 | 栅栏拦截,集齐线程后统一批量放行 |
| 适用场景 | 多任务拆分汇总、主线程等待子线程结果 | 多线程集合同步执行、分阶段并行任务、并发压测 |
7、高频面试坑点(必考)
① 计数器必须放到finally代码块 ,防止异常导致计数器无法递减、主线程永久阻塞;
② 只能一次性使用,计数归零后无法重置;
③ await()不会抛出中断以外异常,阻塞期间可响应中断;
④ 不适合无限任务,一定要搭配超时await防止卡死。
8、一句话总结:
减法计数器、一次性不可复用;子线程逐个递减、主线程阻塞等待,集齐全部任务再放行,做汇总收尾专用。
2.CyclicBarrier 作用?(满分完整版+原理+代码+对比+面试必考)
1、极简背诵定义
CyclicBarrier 是循环栅栏、可复用计数器 ,允许一组线程互相等待,全部到达栅栏屏障点后,再统一同时放行执行;执行完毕计数器自动重置,可循环重复使用。
2、底层核心原理
① 基于AQS共享模式实现,内部维护计数阈值、等待线程数;
② 每一条线程执行到await(),线程阻塞、计数器递减;
③ 所有线程全部抵达栅栏(计数归零),批量唤醒所有阻塞线程,统一并发执行;
④ 支持栅栏触发后执行优先任务,支持重置复用。
3、标准实操代码
java
// 设置栅栏阈值:3个线程集齐统一执行
CyclicBarrier barrier = new CyclicBarrier(3,()-> System.out.println("全部线程就位,统一发车执行"));
for (int i = 1; i <= 3; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"到达栅栏,等待集结");
// 线程阻塞,等待集齐所有线程
barrier.await();
System.out.println(Thread.currentThread().getName()+"统一并发执行任务");
} catch (Exception e) {
e.printStackTrace();
}
},"线程"+i).start();
}
4、核心常用方法
① CyclicBarrier(int parties) :设置栅栏拦截线程数量;
② await() :当前线程到达屏障点,阻塞等待集结;
③reset() :手动重置栅栏,恢复初始计数,可二次使用。
5、生产适用场景
① 多线程集结同步 :多人任务集齐后统一执行,模拟并发压测;
② 分阶段执行任务 :第一阶段全部执行完,再开启第二阶段,分段管控;
③ 批量并行计算 :多线程拆分计算,全部完成后统一进入下一流程。
6、CyclicBarrier 与 CountDownLatch 核心区别(面试必背)对比维度
| 对比维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 计数器类型 | 减法计数器,计数不可逆,只能从初始值递减到 0 | 加法计数器,可重置复用,计数达到阈值后自动重置 |
| 复用性 | 一次性使用,计数归零后无法重置,不可二次使用 | 循环复用,多次拦截线程,放行后自动重置初始计数,可重复使用 |
| 等待对象 | 主线程阻塞等待,等待所有子线程执行完成 | 子线程之间互相阻塞等待,所有线程互相等待集结 |
| 触发放行时机 | 计数器递减至 0 时,自动唤醒阻塞的主线程 | 集齐指定数量的线程后,统一批量唤醒所有阻塞线程 |
| 核心设计目标 | 主线程等待多线程任务全部完成,做汇总收尾工作 | 多线程之间同步集结,统一开始执行后续任务,适配分阶段并行执行 |
| 典型适用场景 | 多任务拆分汇总、服务启动预热、主线程等待子线程结果 | 多线程集合同步执行、分阶段并行任务、并发压测、数据分片计算 |
7、高频面试坑点
① 任意线程中断,栅栏直接破损,剩余线程抛出异常;
② 必须凑齐设定线程数才放行,否则永久阻塞;
③ 支持栅栏回调任务,集齐线程后优先执行回调逻辑;
④ 适合分段同步任务,不适合一次性汇总任务。
8、一句话总结:
CyclicBarrier是可循环栅栏,线程互相等待、集齐统一发车、重复复用;适合多线程集结同步、分段并行执行。
3.CyclicBarrier 作用?(满分完整版+企业实战代码+业务场景+坑点)
1、极简背诵定义
CyclicBarrier 是可循环复用的线程栅栏 ,底层基于AQS共享模式;让一组线程互相等待,全部抵达屏障点后统一批量放行,支持循环复用、栅栏回调任务,适合多线程分阶段同步执行。
2、核心关键特性(面试必背)
① 可循环复用 :计数归零后自动重置,反复使用;
② 线程互相等待 :子线程之间阻塞等待,集齐指定数量线程才放行;
③ 栅栏回调 :所有线程抵达屏障后,优先执行自定义回调任务;
④ 阻塞机制 :底层LockSupport.park()阻塞,无锁阻塞、性能高。
3、企业实战业务代码(生产可直接复用)
业务场景:批量拆分查询数据库,多线程并行查询,全部查询完成后统一汇总数据,分阶段执行。
java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* CyclicBarrier 企业实战:批量数据分片查询汇总
* 场景:千万级数据拆分多线程查询,集齐结果统一组装
*/
public class CyclicBarrierBusinessDemo {
// 定义分片查询线程数
private static final int THREAD_NUM = 4;
// 创建栅栏+集齐线程后的回调汇总任务
private static final CyclicBarrier BARRIER = new CyclicBarrier(THREAD_NUM, () -> {
// 所有分片查询完毕,执行汇总任务
System.out.println("✅ 所有分片查询完成,开始统一汇总组装数据");
});
// 存储分片查询结果
private static final List<String> RESULT_LIST = new ArrayList<>();
public static void main(String[] args) {
// 创建固定线程池
ExecutorService pool = Executors.newFixedThreadPool(THREAD_NUM);
// 模拟4个分片查询任务
for (int i = 1; i <= THREAD_NUM; i++) {
int shard = i;
pool.execute(() -> {
try {
// 1、模拟分片数据库查询
System.out.printf("线程%s:查询第%d分片数据...%n",Thread.currentThread().getName(),shard);
Thread.sleep(800); // 模拟IO查询耗时
RESULT_LIST.add("分片"+shard+"查询数据");
System.out.printf("线程%s:第%d分片查询完成,抵达栅栏等待集结%n",Thread.currentThread().getName(),shard);
// 2、抵达屏障点,阻塞等待所有线程完成
BARRIER.await();
// 3、全部集齐后,统一执行后续业务
System.out.printf("线程%s:开始执行后续同步业务%n",Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
});
}
// 关闭线程池
pool.shutdown();
}
}
4、核心常用方法 ① CyclicBarrier(int parties):初始化拦截线程数量;
② await():线程到达屏障,阻塞等待集结;
③ reset():手动重置栅栏,恢复初始状态;
④ getNumberWaiting():获取当前阻塞等待的线程数。
5、真实生产落地场景
① 数据分片计算 :大数据拆分多线程并行计算,全部完成后汇总;
② 接口并发压测 :所有请求线程集结完毕,统一同时发起请求;
③ 分阶段任务 :流程分段执行,上一阶段全部完成再进入下一阶段;
④ 多服务同步初始化:多个资源加载完毕,再启动对外服务。
6、高频面试坑点(必考)
① 任意线程中断,栅栏直接破损,其余线程抛出BrokenBarrierException;
② 必须凑齐设定线程数才放行,无超时会永久阻塞,生产建议加超时await(time);
③ 回调任务由最后抵达的线程 执行,并非单独线程;
④ 区别CountDownLatch:CyclicBarrier线程互相等、可复用;后者主线程等子线程、一次性。
7、一句话总结:
CyclicBarrier循环栅栏,多线程集结排队、统一发车、分段执行;大数据分片、并发压测首选,可循环复用是核心亮点。
4.Exchanger 作用?(满分完整版+企业实战代码+底层原理+面试坑点)
1、极简背诵定义
Exchanger 是 JUC 线程数据交换工具类,仅支持两个线程成对交换数据;线程两两配对、互相阻塞等待,双方抵达交换点后,互换持有数据,交换完成并行执行,属于点对点线程通信工具。
2、底层核心原理(面试必背)
① 底层基于AQS共享模式实现,内部维护交换槽位;
② 线程调用exchange()方法,若无配对线程,当前线程阻塞等待;
③ 第二个线程进入交换逻辑,两者匹配成功,互相交换数据;
④ 交换完成后,两个线程同时唤醒,继续执行业务;
⑤ 仅支持两两交换,不支持多线程批量交换,无存储容量。
3、企业开发实战代码(生产可直接复用)
业务场景:两个线程双向传输业务数据,线程A生成加密数据、线程B生成密钥,互相交换完成数据解密校验。
java
import java.util.concurrent.Exchanger;
/**
* Exchanger 企业实战:双线程数据双向交换
* 场景:加密数据与密钥配对交换,完成数据校验解密
*/
public class ExchangerBusinessDemo {
// 创建数据交换器,指定数据交换类型为字符串
private static final Exchanger<String> EXCHANGER = new Exchanger<>();
public static void main(String[] args) {
// 线程A:生成加密报文,等待交换密钥
new Thread(() -> {
try {
String encryptData = "ABC123_加密业务报文";
System.out.println("线程A:生成加密报文 = " + encryptData);
// 阻塞等待配对线程,交换数据:传入自身数据,接收对方数据
String secretKey = EXCHANGER.exchange(encryptData);
System.out.println("线程A:交换获取密钥 = " + secretKey);
// 拿到密钥解密业务数据
System.out.println("线程A:使用密钥解密报文,业务处理完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "加密报文线程").start();
// 线程B:生成密钥,等待交换加密报文
new Thread(() -> {
try {
String key = "MD5_666888秘钥";
System.out.println("线程B:生成加密密钥 = " + key);
// 阻塞配对,双向交换数据
String data = EXCHANGER.exchange(key);
System.out.println("线程B:交换获取加密报文 = " + data);
System.out.println("线程B:校验报文合法性,校验完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "密钥生成线程").start();
}
}
4、核心常用方法
① exchange(V x) :传入当前线程数据,阻塞等待配对线程,完成数据交换;
② exchange(V x, long timeout, TimeUnit unit) :限时交换,超时未配对直接抛出超时异常,避免永久阻塞。
5、真实生产落地场景
① 双线程数据对接 :报文与密钥双向传输、上下游线程数据校验;
② 缓冲区交换 :生产者、消费者双缓冲区互换,提升IO读写效率;
③ 简单点对点通信 :无需复杂队列,两个线程临时数据交互;
④ 数据镜像校验 :双线程同步数据,比对数据一致性。
6、高频面试坑点(必考)
① 严格限制两个线程 ,多线程场景下会随机两两配对,交换混乱;
② 单个线程调用exchange()会无限阻塞,必须等待配对线程;
③ 无数据存储容器,数据实时交换、不持久化;
④ 阻塞可中断,线程中断后直接退出交换阻塞状态;
⑤ 生产极少使用,仅适用于简单双线程点对点交互。
7、一句话总结:
Exchanger是双人数据交换工具,两两配对、互相阻塞、双向传值;仅限双线程通信,简单临时交互专用,生产使用频率极低。
5.BlockingQueue 阻塞队列特点?
满队列阻塞入队,空队列阻塞出队。
6.常见阻塞队列?(满分完整版+七大队列+特性+场景+面试必背)
1、标准答案(七大常用阻塞队列)
JUC 常见阻塞队列一共 7 种 :ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue、LinkedTransferQueue、LinkedBlockingDeque。
2、逐个详细拆解(面试必背)
①ArrayBlockingQueue(有界数组阻塞队列)
底层:数组实现、**有界固定容量**;默认非公平锁;
特点:长度固定、不扩容、生产消费速度均衡;
场景:普通限流、固定并发任务缓冲。
② LinkedBlockingQueue(无界/有界链表队列)
底层:单向链表、默认无界(最大Integer最大值);
特点:读写分离锁、吞吐量高于数组队列;
坑点:无界不指定容量容易堆积任务引发OOM;
场景:Executors.newFixedThreadPool 默认队列。
③ SynchronousQueue(同步无容量队列)
底层:无数组无链表、**无存储空间**;
特点:存必须取、一对一配对、不缓存任务;
场景:Executors.newCachedThreadPool 默认队列、瞬时高并发任务。
④ PriorityBlockingQueue(优先级阻塞队列)
底层:最小堆二叉树、无界队列;
特点:自带排序、自定义优先级、永远优先取出权重最高任务;
场景:优先级任务、订单分级处理、紧急任务插队。
⑤DelayQueue(延迟阻塞队列)
底层:优先级队列+时间排序、无界;
特点:元素实现延时接口,**到期才能取出**,未到期永久阻塞;
场景:订单超时关闭、延时任务、定时重试、会员过期。
⑥ LinkedTransferQueue(高效传输队列)
底层:链表、无界;
特点:结合SynchronousQueue+LinkedBlockingQueue优点,可直接传输、可缓存任务,并发性能天花板;
场景:超高并发、海量异步任务传输。
⑦ LinkedBlockingDeque(双向阻塞队列)
底层:双向链表、可设置有界;
特点:头尾两端增删、支持双向阻塞;
场景:工作窃取、双端任务读写、定时任务队列。
3、高频面试坑点
① 有界队列:ArrayBlockingQueue、SynchronousQueue、LinkedBlockingDeque(可设置);
② 无界队列:LinkedBlockingQueue(默认)、Priority、Delay、Transfer;
③ 生产禁止使用无界队列,防止任务无限堆积OOM;
④ 所有阻塞队列底层都是 **ReentrantLock+Condition** 实现阻塞唤醒。
4、一句话总结:
数组固定、链表无界、同步无容量、优先级排序、延迟到期、双向双端、传输高性能;按需选用、拒绝无界、防止OOM。
7.SynchronousQueue 特点?
无容量,存入必须立即取出。
8.延迟队列 DelayQueue 使用场景?(满分完整版+生产真实案例)
1、核心特性 :无界阻塞队列,底层基于优先级最小堆,元素必须实现Delayed延时接口,**只有到期元素才能被取出**,未到期永久阻塞,无需手动轮询,性能极高。
2、生产高频落地场景(面试必背)
① 订单超时自动关闭(最经典) :电商下单未支付,15分钟后自动关闭订单、释放库存、回滚优惠券;
② 会员/权限过期管控 :VIP会员到期自动降级、临时权限到期回收;
③ 延时重试任务 :接口调用失败、消息发送失败,延迟指定时间二次重试;
④ 活动定时启停 :营销活动定时上线、定时下架,无需定时轮询扫描;
⑤ 任务延时调度 :预约任务、定时提醒、消息延后推送(短信、站内信);
⑥ 闲置资源回收 :连接池闲置连接超时销毁、临时缓存过期清理。
3、底层优点 :无需while死循环轮询、节约CPU资源、精准延时、无额外定时任务开销。
4、生产痛点坑点 :
① 无持久化,服务重启内存数据丢失,重要延时任务需搭配数据库持久化;
② 堆结构插入排序,大量任务写入性能一般;
③ 不支持精准时间排序外的复杂延时规则。
5、一句话总结:
到期自动取出、未到期阻塞等待;主打订单超时、延时重试、过期回收,轻量无轮询,适合简单延时任务。
9.ConcurrentHashMap1.7 与 1.8 区别?(满分完整版+表格+底层原理+面试必考)
1、一句话极简背诵
JDK1.7分段锁+数组链表、ReentrantLock加锁;JDK1.8数组+链表+红黑树、CAS+synchronized,优化哈希扰动、并发更高、查询更快。
2、七大核心区别(面试必背表格)
| 对比维度 | JDK1.7 | JDK1.8 |
|---|---|---|
| 底层数据结构 | 数组 + 分段链表(Segment 分段锁),双层数组分段存储 | 数组 + 链表 + 红黑树,取消 Segment 分段,一维数组直接存储 |
| 加锁实现方式 | ReentrantLock 显式锁,锁住整个 Segment 分段 | CAS 无锁插入 + synchronized 内置锁,仅锁住冲突桶位头节点 |
| 锁粒度 | 锁住整个 Segment 分段,一个分段内所有操作互斥 | 仅锁住当前数组桶位,不同桶位操作互不阻塞,锁粒度极细 |
| 哈希扰动算法 | 4 次位运算扰动,降低哈希碰撞概率 | 1 次扰动 + 高位参与运算,逻辑更简洁,散列均匀性更优 |
| 扩容机制 | 各 Segment 分段独立扩容,互不影响 | 全局统一扩容,新增迁移标记节点,支持多线程协助迁移数据 |
| 红黑树结构 | 无红黑树结构,哈希冲突仅靠链表挂载 | 链表长度 > 8 且数组长度 > 64 转为红黑树;节点数≤6 退化为链表 |
| 查询效率 | 链表遍历查询,大数据量下查询效率低,时间复杂度 O (n) | 链表 + 红黑树双结构,长链表查询优化为 O (logN),查询效率大幅提升 |
| 并发能力 | 并发度低,最大仅支持 16 个 Segment 分段并发 | 并发度极高,桶位独立加锁互不阻塞,理论并发数等于数组长度 |
3、逐点底层详解
① 数据结构 :1.7采用Segment数组+HashEntry数组+链表,双层数组分段存储;1.8取消Segment分段,直接采用一维数组,链表过长转化红黑树,平衡查询性能。
② 加锁方式 :1.7基于ReentrantLock锁住Segment分段,一个分段内数据互斥;1.8摒弃分段锁,采用CAS无锁插入,冲突后加synchronized锁,仅锁定当前桶位节点,锁粒度大幅缩小。
③ 哈希算法优化 :1.7做4次哈希扰动,降低碰撞;1.8简化扰动算法,将哈希值高位挪至低位运算,兼顾性能与散列均匀性。
④ 树化规则 :1.7无红黑树,链表无限变长;1.8规定:链表长度超过8、且数组容量≥64,树化为红黑树;扩容时节点数≤6,红黑树退化为链表,节省空间。
⑤ 扩容机制 :1.7各Segment独立扩容,互不干扰;1.8全局数组扩容,新增辅助迁移节点,多线程协助迁移数据,提升扩容效率。
⑥ 锁性能差异 :1.7分段锁最多同时16个线程并发;1.8桶位独立加锁,理论并发数等于数组长度,并发能力大幅提升。
4、高频面试坑点
① JDK1.8放弃ReentrantLock改用synchronized,原因:JDK1.6后锁优化,synchronized性能反超显式锁,更简洁;
② 红黑树转换双重条件:链表长度>8 + 数组长度≥64 ,缺一不树化,防止小容量无效树化;
③ 1.8扩容会保留原数组,新旧数组并存,迁移完成后废弃旧数组;
④ 二者均不允许key/value为null,区别于HashMap。
5、一句话总结:
1.7分段锁、链表慢、重入锁;1.8细粒度桶锁、CAS+sync、树化优化、并发性能全面碾压旧版本。
10.ConcurrentHashMap 的工作原理?(满分完整版+底层流程+面试必背)
1、一句话极简背诵
JDK1.8采用数组+链表+红黑树数据结构,依托CAS+synchronized细粒度桶位锁,结合哈希扰动、树化机制、多线程扩容,实现线程安全的高效键值存储,全程无分段锁,并发性能极强。
2、底层核心数据结构
① 底层主体 :一维哈希数组(Node[] table),数组下标为哈希桶位;
② 链表结构 :桶位哈希冲突时,尾插法挂载链表节点,存储冲突元素;
③ 红黑树:链表长度>8且数组容量≥64,链表转为红黑树,提升查询效率;节点数≤6,树退化为链表,节约内存。
3、加锁核心原理(重中之重)
① 无冲突:采用CAS无锁自旋 插入节点,不加锁、性能高;
② 发生哈希冲突:对当前桶位头节点加synchronized锁 ,仅锁住单个桶位,不影响其他桶位并发,锁粒度极细;
③ 摒弃JDK1.7分段锁,取消Segment层,简化结构、提升并发度。
4、完整写入执行流程
① 计算key哈希值,通过扰动算法优化哈希,确定数组桶位下标;
② 判断当前桶位是否为空,为空则CAS无锁直接插入节点;
③ 桶位不为空且哈希冲突,锁住桶位头节点,遍历链表/红黑树;
④ key重复则覆盖旧值,无重复则尾插新增节点;
⑤ 插入后判断链表长度,满足树化条件自动转换红黑树。
5、扩容迁移原理
① 数组元素达到扩容阈值(容量*0.75),触发全局扩容,容量翻倍;
② 新增迁移标记节点,支持多线程协助扩容 ,分散迁移压力;
③ 扩容期间新旧数组共存,迁移完成后废弃旧数组,避免数据丢失。
6、关键特性与约束
① 线程安全:CAS+synchronized保证并发读写安全;
② 去重规则:key唯一,重复key覆盖value;
③ 空值约束:key、value均不允许为null ,区别于HashMap;
④ 迭代机制:弱一致性迭代,遍历期间修改不会抛出并发修改异常。
7、高频面试坑点
① synchronized仅锁桶位头节点,不锁数组,并发粒度远超分段锁;
② 树化必须同时满足两个条件:链表长度>8、数组容量≥64;
③ JDK1.8舍弃ReentrantLock,因锁优化后synchronized性能更优;
④ 高并发写入无全局锁,多桶位可同时并行写入。
8、终极总结:
数组打底、冲突链表、过长树化;CAS无锁插空、sync锁冲突桶位;多线程扩容、细粒度锁,实现高并发线程安全哈希表。
11.HashMap 的工作原理?线程不安全表现?(满分完整版+底层流程+不安全原因+面试必考)
1、极简背诵定义
HashMap 是 JDK 非线程安全哈希表,JDK1.8 采用数组+链表+红黑树 数据结构,基于哈希算法存储键值对,依靠哈希表快速寻址,不保证线程安全,并发场景会出现数据异常。
2、底层工作原理(JDK1.8 主流版本)
① 底层结构 :一维哈希数组(默认容量16)+单向链表+红黑树;数组下标为哈希桶位,冲突元素挂载链表,长链表转为红黑树优化查询。
② 哈希寻址 :通过hash()扰动算法优化key哈希值,高位参与运算,减少哈希碰撞,计算数组下标确定存储桶位。
③ 插入规则 :桶位为空直接插入;桶位冲突采用尾插法 挂载链表;链表长度>8且数组容量≥64,树化为红黑树;节点数≤6退化为链表。
④ 扩容机制 :负载因子默认0.75,元素达到阈值触发扩容,容量翻倍;扩容后重新哈希迁移元素,优化哈希分布。
⑤ 查询逻辑 :计算哈希定位桶位,桶位首节点直接比对key;链表/红黑树遍历匹配key,返回对应value。
3、HashMap 核心参数
① 默认初始容量:16;② 默认负载因子:0.75;③ 树化阈值:链表长度>8;④ 退化阈值:节点数≤6。
4、多线程下线程不安全四大表现(面试必背)
①数据覆盖丢失 :多线程同时put元素,哈希碰撞时未加锁,后插入线程直接覆盖前一个线程数据,造成数据丢失。
② 扩容死循环(JDK1.7专属) :1.7采用头插法,多线程同时扩容迁移链表,节点引用循环指向,形成环形链表,get查询时CPU死循环飙升。
③ 元素错乱、链表断裂 :JDK1.8尾插法消除死循环,但并发扩容会出现节点丢失、链表断裂、元素顺序错乱问题。
④空指针异常、并发修改异常 :并发遍历+修改,底层节点结构突变,抛出异常或读取到null脏数据。
5、不安全底层根本原因
HashMap 无任何加锁机制,并发读写无互斥;扩容、插入、修改节点时,线程互相抢占修改节点引用,导致哈希表结构错乱。
6、面试高频坑点
① JDK1.7头插法有扩容死循环,1.8尾插法解决死循环,但依旧线程不安全 ;
② 负载因子0.75是时间与空间平衡最优值,不建议随意修改;
③ HashMap 允许key/value为null,key仅允许一个null,区别于ConcurrentHashMap;
④ 并发业务禁止使用HashMap,优先选用ConcurrentHashMap。
7、一句话总结:
数组打底、冲突链表、过长树化;无锁并发不安全,1.7死循环、1.8数据覆盖,高并发必须换ConcurrentHashMap。
12.ConcurrentHashMap 和 Hashtable 的区别?(满分完整版+表格+底层+面试必考)
1、一句话极简背诵
Hashtable 全局粗暴加锁、并发极差;ConcurrentHashMap 细粒度锁、并发高性能,两者都是线程安全哈希表,生产坚决弃用Hashtable,优先ConcurrentHashMap 。
2、六大核心区别(面试必背表格)
| 对比维度 | Hashtable | ConcurrentHashMap(JDK1.8) |
|---|---|---|
| 加锁方式 | 方法上加 synchronized,全局锁,锁整个数组 | CAS+synchronized 细粒度桶位锁,仅锁定冲突节点 |
| 锁粒度 | 锁粒度极大,同一时刻仅能一个线程执行读写操作 | 锁粒度极小,多线程可并行操作不同桶位,互不阻塞 |
| 并发性能 | 并发能力极低,属于老旧淘汰类 | 并发能力极强,是高并发场景首选方案 |
| 空值约束 | key、value均不允许为 null | key、value均不允许为 null |
| 数据结构 | 底层仅采用数组 + 单向链表,无红黑树优化 | 底层采用数组 + 链表 + 红黑树,长链表自动树化优化查询性能 |
| 迭代器特性 | 普通强一致性迭代器,遍历过程中修改元素会直接抛出并发修改异常 | 弱一致性迭代器,基于快照遍历,遍历期间允许修改元素,不抛异常 |
3、逐点底层深度拆解
① 加锁机制不同(核心区别)
Hashtable:在put、get、remove等所有公有方法上加synchronized,锁住整个哈希数组,无论是否冲突,所有线程互斥排队,并发吞吐量极低;
ConcurrentHashMap:无全局锁,空桶位CAS无锁插入,冲突仅锁住当前桶位头节点,不同桶位线程互不阻塞,并发能力拉满。
② 数据结构不同
Hashtable:底层仅采用数组+单向链表,无红黑树,哈希冲突严重时链表无限变长,查询效率O(n);
ConcurrentHashMap:数组+链表+红黑树,链表过长自动树化,查询效率优化为O(logN)。
③ 迭代机制不同
Hashtable:普通强一致性迭代器,遍历过程中修改元素,直接抛出ConcurrentModificationException并发修改异常;
ConcurrentHashMap:弱一致性迭代器,基于快照遍历,遍历期间允许修改,不抛异常,适合并发读写场景。
④ 初始化与扩容差异
Hashtable:默认初始容量11,扩容方式:原容量*2+1,素数容量减少哈希碰撞;
ConcurrentHashMap:默认初始容量16,扩容容量翻倍,2的幂次,哈希取模运算更高效。
4、高频面试坑点(必考)
① 两者都不允许key/value为null ,区别于HashMap;
② Hashtable是遗留老旧类,底层锁设计笨重,生产环境禁止使用 ;
③ Hashtable全局锁,锁竞争激烈极易线程阻塞;ConcurrentHashMap细粒度锁,适配高并发;
④ JDK1.8之后ConcurrentHashMap优化拉满,全面替代Hashtable成为并发哈希首选。
5、一句话总结:
Hashtable全局粗锁、性能拉胯老旧淘汰;ConcurrentHashMap细粒度桶锁、树化优化、高并发高性能,并发场景唯一首选。
13.ConcurrentHashMap 如何进行扩容?(满分完整版+流程+多线程迁移+面试必考)
1、极简背诵定义
JDK1.8 ConcurrentHashMap 满足扩容阈值触发全局翻倍扩容,采用新旧数组共存+多线程协助迁移机制,CAS+synchronized保障迁移安全,扩容期间不阻塞读写,大幅提升扩容效率,无分段独立扩容。
2、扩容触发条件(面试必背)
① 底层哈希数组元素数量 ≥ 容量 * 负载因子(0.75) ,触发正常扩容;
② 链表树化触发扩容:链表长度>8,但数组容量<64,不满足树化条件,优先扩容;
③ 初始容量最小为16,扩容容量严格遵循2倍翻倍 ,始终保持2的幂次。
3、核心扩容底层机制
① 容量翻倍 :原数组容量左移一位(*2),新建空的扩容数组;
② 迁移标记位 :定义特殊ForwardingNode迁移节点,标记当前桶位正在迁移,防止重复迁移、并发修改;
③ 多线程协助迁移 :主线程迁移过程中,其他写入/查询线程发现扩容标记,主动协助搬运数据,分散迁移压力;
④ 高低位拆分迁移 :原桶位数据无需重新哈希计算,依据哈希值高位划分:高位为0留原下标,高位为1迁移至【原下标+旧容量】新下标,迁移算法极简高效。
4、完整扩容迁移流程
① 判断元素数量达到扩容阈值,初始化二倍容量的新数组;
② 遍历原数组桶位,为空桶位直接标记跳过,无需迁移;
③ 有数据桶位加synchronized锁,拆分链表/红黑树为高低位两条链表;
④ 分别迁移至新数组对应下标,原桶位标记为迁移节点;
⑤ 全部桶位迁移完成,废弃旧数组,新数组替代原数组,扩容结束。
5、扩容线程安全保障
① 单个桶位加锁,不同桶位可并行迁移;
② ForwardingNode标记防止重复迁移,规避并发异常;
③ 迁移期间读写线程互不阻塞,读操作优先读取新旧数组有效数据。
6、高频面试坑点(必考)
① JDK1.7分段独立扩容,1.8改为全局统一扩容 ;
② 扩容无需重算哈希,依靠高位拆分,节省算力;
③ 多线程协助扩容是核心优化,高并发下大幅缩短扩容耗时;
④ 扩容期间新旧数组共存,不会丢失数据,保证数据一致性。
7、一句话总结:
达到阈值二倍扩容、高位拆分免重哈希、多线程协助搬运、桶位加锁保安全;迁移标记防重复,扩容高效且线程安全。
14.为什么 ConcurrentHashMap 不允许使用 null 作为键或值?(满分完整版+底层原因+面试必考)
1、一句话极简背诵
核心原因:并发环境下无法区分 null 是键不存在,还是存入的 null 值 ,为避免歧义、防止空指针隐患、适配并发查询,直接禁止null键和null值;而HashMap是非并发容器,无此顾虑。
2、两大核心底层原因(面试必背)
① 并发查询歧义问题(最根本原因)
ConcurrentHashMap 支持并发读写,多线程环境下,调用get(key)返回null时,无法判定两种情况:
✅ 情况1:该key本身不存在;
✅ 情况2:该key真实存在,存入的value就是null。
非并发的HashMap可以通过containsKey(key)二次判断区分;但并发场景下,两次查询之间数据可能被其他线程修改 ,判定结果失效,存在严重逻辑歧义。
② 空指针安全约束+设计规范
JUC并发容器设计初衷就是规避空指针异常、减少线上隐患;null代表空占位,无实际业务含义,并发存取null极易引发空指针、逻辑判断异常;因此JUC所有并发Map容器,统一禁止key、value为null。
3、HashMap 允许null的对比区别
① HashMap:单线程环境,可通过containsKey二次校验,无并发数据篡改风险,允许一个null键、多个null值;
② ConcurrentHashMap:并发多线程,二次校验失效,无法区分null语义,直接全部禁止。
4、高频面试坑点
① 不要只记空指针,核心考点是并发歧义无法判定 ;
② Hashtable同样禁止null键值,底层逻辑和ConcurrentHashMap一致;
③ 业务中存入空数据,建议自定义空标识常量,禁止用null占位。
5、一句话总结:
单线程HashMap可校验区分null,并发ConcurrentHashMap无法区分null语义;为杜绝并发歧义、规避空指针,直接禁止null键和null值。
15.CopyOnWriteArrayList 原理?(满分完整版+底层+代码+面试必考)
1、一句话极简背诵
核心原理:写时复制、读写分离、读无锁、写加锁 ;修改元素时拷贝出新数组,在新数组完成写入,修改完毕替换原数组,读操作全程无锁,极致适配读多写少并发场景。
2、底层核心原理(面试必背)
① 底层数据结构 :内部维护volatile 修饰的Object数组 ,volatile保证数组引用实时可见,禁止指令重排;
② 写时复制机制 :查询、遍历等读操作直接读取原数组,不加任何锁;新增、删除、修改等写操作,先加ReentrantLock显式锁,防止并发写入冲突,再拷贝原数组生成长度+1/同等长度的新数组,在新数组完成数据修改,最后用新数组覆盖原数组引用;
③ 读写分离 :读旧数组、写新数组,读写互不阻塞,读操作性能极高;
④ 迭代器弱一致性 :迭代时基于快照遍历,遍历期间修改数组不会抛出并发修改异常。
3、底层执行流程(以add新增为例)
① 写入前加ReentrantLock锁,保证单线程写入;
② 获取原数组,复制原数组元素,生成新数组;
③ 在新数组末尾添加元素;
④ 将volatile数组引用指向新数组;
⑤ 释放锁,写入完成。
4、极简源码核心片段 // 底层volatile数组,保证引用可见性
java
private transient volatile Object[] array;
// add方法核心逻辑
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 写操作加锁
try {
Object[] elements = getArray();
int len = elements.length;
// 拷贝生成新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 替换原数组引用
setArray(newElements);
return true;
} finally {
lock.unlock(); // 释放锁
}
}
5、核心优缺点(衔接下一题,逻辑连贯)
① 优点:读无锁、查询极速;并发读不阻塞、读写不阻塞;迭代不抛并发修改异常;
② 缺点:写入频繁会频繁拷贝数组,占用大量内存、产生GC;数据存在弱一致性、实时性差 ,读取不到最新写入数据;不适合大批量写操作。
6、高频面试坑点
① 加锁方式:写操作使用ReentrantLock显式锁 ,读操作无锁;
② 数组是volatile修饰,仅保证引用可见,不保证数组内部元素可见;
③ 每次写入必拷贝数组,数组越大拷贝开销越高;
④ 允许元素为null,支持重复元素,有序可重复。
7、生产适用场景
专门适配读多写少 场景:白名单、配置列表、权限菜单、静态常量集合、极少修改的缓存列表;禁止用于高频写入业务。
8、一句话总结:
写加锁、读无锁,修改先拷贝、改完换引用;读多写少专用,牺牲内存换并发读性能,存在数据弱一致性。
16.CopyOnWriteArrayList 缺点?
占用内存、数据实时性弱。
17.ThreadLocal 的工作原理?作用?(满分完整版+底层+代码+面试必考)
1、一句话极简背诵
ThreadLocal 是线程本地存储工具 ,为每个线程单独开辟私有存储空间,线程之间数据严格隔离、互不共享;底层线程自带Map存储私有数据,无锁实现线程安全,专门解决线程变量隔离问题。
2、核心作用(面试必背)
① 线程数据隔离 :每个线程拥有独立变量副本,线程之间互不干扰、互不覆盖;
② 无锁并发安全 :不依赖任何锁,规避并发竞争,性能极高;
③ 上下文数据传递 :同一线程内跨方法、跨层级透明传参,简化代码;
④ 保存线程私有上下文 :存储用户登录信息、请求链路ID、数据库连接等线程专属数据。
3、底层工作原理(通俗易懂)
① 存储结构 :每个 Thread 线程内部持有 ThreadLocalMap 集合;
② 存储关系 :以 ThreadLocal 对象为 key,存储的业务数据为 value;
③ 存取逻辑 :调用set()时,获取当前线程的ThreadLocalMap,存入键值对;调用get()时,从当前线程Map中取值;
④ 隔离本质 :数据存在线程自身内部,而非ThreadLocal,天生线程隔离,无需加锁。
4、标准实操代码(业务通用模板)
java
// 定义线程本地变量,存储用户信息
private static final ThreadLocal<UserInfo> USER_LOCAL = new ThreadLocal<>();
// 存入当前线程上下文(拦截器解析登录用户)
USER_LOCAL.set(userInfo);
// 同一线程任意位置获取用户信息
UserInfo info = USER_LOCAL.get();
// 使用完毕必须移除,防止内存泄漏
USER_LOCAL.remove();
5、底层核心特性
① 数据绑定当前线程,生命周期跟随线程;
② 底层Entry的key为弱引用 ,防止ThreadLocal对象内存常驻;
③ 不支持父子线程数据传递,子线程无法获取父线程本地数据;
④ 单线程内可存储多个不同类型本地变量。
6、生产高频使用场景
① Web请求上下文:存储登录用户、Token、请求IP,全局透明获取;
② 数据库会话:单线程绑定同一个数据库连接,避免频繁创建销毁;
③ 日志链路追踪:存放traceId,串联同一线程全部日志;
④ 简单工具隔离:非线程安全工具类(SimpleDateFormat),线程私有实例隔离。
7、高频面试坑点(必考)
① ThreadLocal 不共享数据 ,仅作用于当前线程;
② 线程池环境必须手动remove,线程复用会产生脏数据;
③ 弱引用key回收后,value强引用残留,引发内存泄漏;
④ 禁止存储大对象,长期常驻线程容易堆积内存。
8、一句话总结:
线程自带本地Map、key弱引用;实现线程数据隔离、无锁安全、上下文传参;用完务必remove,严防内存泄漏与线程复用脏数据。
18.ThreadLocal 底层结构?(满分完整版+源码+哈希规则+面试必考)
1、整体层级结构(面试必背)
层级链路:Thread 线程 → ThreadLocalMap 内部集合 → Entry 存储节点 ;数据不存在ThreadLocal中,而是存放在当前线程内部的ThreadLocalMap里,线程私有、天然隔离。
2、三大核心组成部分
① Thread(线程) :每个Thread内部维护一个ThreadLocalMap成员变量,初始为null,首次set才创建;
② ThreadLocalMap(内部哈希表) :ThreadLocal静态内部类,自定义简易哈希表,无链表、无红黑树;底层数组存储Entry,默认容量16,负载因子2/3;
③ Entry(存储节点) :ThreadLocalMap静态内部类,专门存储键值对;key为ThreadLocal弱引用、value为业务强引用 。
3、Entry底层源码结构 // 底层Entry核心源码
java
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// key弱引用、value强引用
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
4、哈希寻址规则(区别HashMap)
① 哈希算法:ThreadLocal采用固定魔数0x61c88647 黄金哈希值,均匀散列下标,减少哈希冲突;
② 冲突解决:无链表、无树化 ,采用线性探测法,哈希冲突向后顺延找空槽位;
③ 扩容机制:数组元素数量达到容量2/3触发扩容,容量翻倍,重新哈希迁移元素。
5、底层核心特点
① 专属私有:一个线程仅一个ThreadLocalMap,多线程互不干扰;
② 弱引用key:防止ThreadLocal对象被回收后,key常驻内存;
③ 自动清理:遍历数组时,发现key过期的无效Entry,自动清空value、释放内存;
④ 结构极简:舍弃复杂链表红黑树,线性探测适配少量线程本地变量存储。
6、高频面试坑点
① 不要混淆存储位置:数据存在Thread里,不是ThreadLocal ;
② 线性探测冲突效率低,不适合存储海量本地变量;
③ key弱引用+value强引用组合,是内存泄漏的根本结构原因;
④ Map延迟创建,线程无本地变量时不占用内存。
7、一句话总结:
线程内置Map、数组+Entry节点、key弱引用;黄金哈希散列、线性探测解决冲突,结构轻量专为线程私有数据设计。
19.ThreadLocal 内存泄漏原因?(满分完整版+底层结构+泄漏流程+面试必考)
1、一句话极简背诵
底层key弱引用、value强引用 ;ThreadLocal被回收后key清空,value无法自动释放,长期驻留线程造成内存泄漏,线程池复用场景泄漏最严重。
2、底层根本原因(核心必考)
① Entry存储结构缺陷 :ThreadLocalMap的Entry节点,key(ThreadLocal)为弱引用 ,value(业务数据)为强引用 ;
② GC回收机制差异 :发生GC时,无强引用的key会被弱引用机制自动回收置为null;但value是强引用,当前线程只要不销毁,value就一直被线程引用,无法被GC回收;
③ 数据残留堆积:key为null、value存活,形成无效脏数据,长期堆积在ThreadLocalMap中,造成内存泄漏。
3、完整泄漏执行流程
① 业务代码中ThreadLocal引用断开(局部变量结束生命周期);
② GC触发,弱引用key被回收,Entry中key变为null;
③ 线程持续存活(线程池/常驻核心线程),线程内部ThreadLocalMap不会销毁;
④ 强引用value一直绑定线程,无法被垃圾回收,永久残留内存,产生内存泄漏。
4、两大高发泄漏场景
① 线程池环境(最严重) :线程复用不会销毁,旧线程残留的null-key+value脏数据永久堆积;
② 常驻后台线程 :JVM守护线程、定时任务线程长期存活,无法自动清空残留数据。
5、补充面试坑点
① 若线程直接销毁,线程内部Map全部回收,不会产生内存泄漏 ;
② 弱引用不是漏洞,是设计者为防止ThreadLocal对象内存常驻;泄漏根源是强弱引用搭配缺陷+不手动清理 ;
③ 存储大对象、集合类数据,泄漏造成的内存占用会急剧放大。
6、一句话总结:
key弱引用被GC回收、value强引用滞留线程;线程不销毁、数据不释放,强弱引用结构缺陷引发内存泄漏。
20.如何解决 ThreadLocal 内存泄漏?(满分完整版+规范代码+生产强制规范)
1、核心最优解决方案(面试必背)
使用完毕,必须在finally代码块手动调用remove()清除数据 ,主动断开强引用value,彻底释放内存,从根源杜绝内存泄漏。
2、四大落地解决手段(生产强制执行)
① 手动remove清除(最根本、必做)
业务逻辑执行完毕,无论是否异常,在finally中强制删除当前线程本地数据,清空Entry的key和value,断开强引用链路,防止数据残留。
② 尽量使用try-finally标准模板
严格规范编码格式,set存入数据后,finally执行remove,保证任何情况下都能清除,杜绝遗漏。
③ 线程池环境重点管控
线程池线程长期复用、不会销毁,用完必须remove;否则复用线程会读取上一次残留脏数据,同时堆积内存泄漏。
④ 避免存储大对象、长生命周期数据
ThreadLocal适合存储轻量上下文数据,禁止存放集合、大实体、长缓存对象,减少泄漏内存占用体量。
3、生产标准安全代码模板(直接复用)
java
// 定义线程本地变量
private static final ThreadLocal<UserInfo> USER_LOCAL = new ThreadLocal<>();
public void bizMethod(){
try {
// 存入线程上下文
USER_LOCAL.set(new UserInfo());
// 执行业务逻辑
doBusiness();
} finally {
// 【强制】用完手动移除,根治内存泄漏
USER_LOCAL.remove();
}
}
4、辅助底层优化(JDK自带)
ThreadLocalMap底层自带**被动清理机制**:扩容、重新哈希、遍历查询时,自动扫描key为null的无效Entry,清空value释放内存;但该机制触发不可控、被动滞后,不能作为依赖,只能辅助兜底。
5、高频面试坑点
① 仅依靠JDK自动清理无法根治泄漏 ,触发时机随机,生产必须手动remove;
② 线程池、定时任务常驻线程,是泄漏重灾区,编码零容忍遗漏;
③ 禁止将ThreadLocal定义为静态常量后不清理,长期堆积脏数据;
④ 弱引用是兜底保护,不是解决方案,切勿混淆概念。
6、一句话终极总结:
finally手动remove是唯一根治手段,try-finally规范编码、线程池严格清理、杜绝大对象存储;被动清理仅兜底,手动清除才是王道。
21.InheritableThreadLocal 作用?(满分完整版+底层原理+坑点)
1、一句话极简背诵
InheritableThreadLocal 是可继承的线程本地变量 ,支持**父线程向子线程自动传递数据**,创建子线程时拷贝父线程本地数据,实现父子线程数据共享。
2、核心底层原理
① 继承ThreadLocal,重写childValue()拷贝方法;
② 创建子线程时,JVM主动检测父线程的InheritableThreadLocalMap;
③ 浅拷贝父线程数据至子线程独立Map,拷贝后父子线程数据相互隔离,互不影响;
④ 普通ThreadLocal子线程无法获取父线程数据,该类专门解决父子线程传参问题。
3、适用业务场景
① 主线程开启子线程,同步传递登录用户、请求上下文;
② 简单父子线程链路数据透传,无需手动赋值。
4、高频致命坑点(面试必考)
① 不支持线程池 :线程池线程复用,只会在线程创建时拷贝一次数据,复用不会刷新,产生脏数据;
② 数据为浅拷贝,引用类型数据修改会互相影响;
③ 子线程创建开销大,大量创建子线程性能损耗高。
5、一句话总结:
原生支持父子线程数据传递、创建子线程浅拷贝数据;简单异步子线程适用,线程池环境直接失效。
22.TransmittableThreadLocal 作用?(满分完整版+底层+线程池痛点+生产场景)
1、一句话极简背诵
阿里开源工具类,专门解决线程池环境下父子线程上下文传递失效 问题;原生InheritableThreadLocal线程复用脏数据,TransmittableThreadLocal实现线程池完美上下文透传。
2、核心底层作用
① 继承InheritableThreadLocal,保留父子线程数据拷贝能力;
② 修饰线程池、捕获任务提交时刻上下文快照,每次提交任务都刷新上下文 ;
③ 任务执行完毕自动清除上下文,杜绝线程复用产生脏数据;
④ 无侵入式透传,适配Spring、Dubbo、MDC日志链路追踪。
3、解决行业痛点(必考)
原生缺陷:InheritableThreadLocal 仅线程创建时拷贝一次数据 ,线程池线程长期复用,旧上下文残留,出现登录用户错乱、traceId混乱;
优化方案:TransmittableThreadLocal 在任务提交时复制当前主线程上下文,任务执行前赋值给工作线程,执行结束清空,完美适配线程池复用场景。
4、生产落地场景
① Web异步线程、线程池异步任务透传登录用户信息;
② MDC日志链路追踪,异步线程保持同一个traceId;
③ Dubbo异步调用、网关链路上下文透传;
④ 分布式服务之间请求头、Token透传。
5、高频面试坑点
① JDK原生无此类,依赖阿里ttl依赖包 ;
② 必须搭配修饰线程池使用,否则无法生效;
③ 核心原理:任务提交快照+执行赋值+结束清空。
6、一句话总结:
专治线程池上下文传递,弥补InheritableThreadLocal复用脏数据缺陷;异步线程、日志追踪、用户透传生产必备。
23.ScopedValue 作用?(JDK21新特性+满分完整版+对比ThreadLocal+面试必背)
1、一句话极简背诵
ScopedValue 是 JDK21 推出的作用域线程本地变量 ,专为替代 ThreadLocal 而生;作用域可控、自动销毁、无内存泄漏、不可修改,极简实现线程上下文透传,是虚拟线程配套核心工具。
2、核心官方定义
ScopedValue 属于不可变、作用域受限的线程绑定变量,一旦绑定赋值,生命周期固定在指定代码作用域内,代码执行完毕自动释放资源,无需手动remove,从底层根治ThreadLocal内存泄漏痛点。
3、四大核心特性(面试必背)
① 作用域可控 :绑定代码块作用域,出作用域立即失效,生命周期清晰;
② 天然无泄漏 :无需手动移除,JVM自动回收资源,彻底解决ThreadLocal内存残留问题;
③ 不可变安全 :赋值后不可修改,杜绝并发篡改、脏数据问题;
④ 适配虚拟线程 :专为JDK21虚拟线程优化,轻量化、低开销,适配海量IO密集型异步任务。
4、和 ThreadLocal 核心区别(面试加分)
| 对比维度 | ThreadLocal | ScopedValue |
|---|---|---|
| 可变特性 | 支持二次 set 修改 | 赋值后不可修改、天然安全 |
| 生命周期 | 跟随线程存活 | 绑定代码作用域,出域自动销毁 |
| 泄漏风险 | 弱引用结构缺陷,极易引发内存泄漏 | JVM 自动回收资源,零内存泄漏风险 |
| 适配线程 | 仅适配普通物理线程 | 完美适配虚拟线程 + 物理线程 |
| 使用复杂度 | 需在 finally 中手动 remove 移除,编码繁琐 | 简单优雅,无需手动清理,自动释放资源 |
5、极简代码演示
java
// 定义作用域变量
private static final ScopedValue<UserInfo> USER_SCOPE = ScopedValue.newInstance();
// 绑定作用域,执行业务,结束自动释放
ScopedValue.where(USER_SCOPE, new UserInfo()).run(() -> {
// 当前作用域内任意位置获取数据
UserInfo info = USER_SCOPE.get();
// 执行业务逻辑
});
// 出作用域,数据自动失效,无需手动删除
6、生产适用场景
① Web请求上下文透传用户、Token、链路ID;
② 虚拟线程异步任务上下文传递;
③ 禁止修改、要求只读的线程私有上下文数据;
④ 规避内存泄漏的常驻线程、线程池场景。
7、高频面试坑点
① JDK21及以上版本专属,低版本无法使用;
② 赋值后不可二次修改 ,适合只读上下文;
③ 不支持无限存储,严格绑定代码作用域,灵活性低于ThreadLocal;
④ 是官方主推的ThreadLocal终极替代方案。
8、一句话总结:
JDK21全新作用域变量,只读不可改、出域自动销毁、零内存泄漏;适配虚拟线程,官方强力替代老旧ThreadLocal。
五、线程池核心(81-90 题)
1.为什么使用线程池?
复用线程、减少创建销毁开销、控制并发、统一管理。
2.线程池七大核心参数?(满分完整版+通俗解释+面试必背)
1、标准答案(七大参数全背诵)
ThreadPoolExecutor 线程池七大核心参数:核心线程数、最大线程数、空闲存活时间、时间单位、阻塞队列、线程工厂、拒绝策略。
2、逐个参数详细拆解(面试必考)
① corePoolSize(核心线程数):线程池常驻存活线程,线程池初始化后不销毁(默认),即使空闲也会保留,用来随时处理任务;
② maximumPoolSize(最大线程数):线程池能创建的线程总数上限,任务暴增、队列塞满后,才会新建非核心线程;
③keepAliveTime(空闲存活时间):非核心线程闲置超时时间,空闲超过该时间,非核心线程自动销毁,节省资源;
④ TimeUnit(时间单位):配合空闲时间使用,毫秒、秒、分钟等时间计量单位;
⑤ BlockingQueue(阻塞队列):任务缓冲队列,核心线程满负荷后,新增任务存入队列排队,常用队列:Array、Linked、SynchronousQueue;
⑥ ThreadFactory(线程工厂):专门用来创建线程,可自定义线程名称、线程优先级、是否守护线程,方便线上日志排查;
⑦ RejectedExecutionHandler(拒绝策略):核心线程+队列+最大线程全部打满,线程池饱和,触发拒绝策略处理超额任务。
3、通俗流程串联(背诵口诀)
常驻核心线程干活 → 任务多了队列排队 → 队列满了新建临时工(非核心线程) → 达到最大线程数触发拒绝策略 → 临时工空闲超时自动销毁。
4、高频面试坑点
① 核心线程默认不会被回收,开启allowCoreThreadTimeOut参数后,核心线程也可超时回收; ② 最大线程数 = 核心线程数 + 非核心线程数;
③ 业务必须自定义线程工厂,给线程命名,便于线上异常排查;
④ 拒绝策略禁止默认抛异常,生产需自定义降级策略。
5、一句话总结 :核心常驻、最大封顶、空闲超时、队列缓冲、工厂造线程、饱和拒任务,七大参数管控线程池全生命周期。
3.线程池执行流程?(满分完整版+通俗流程+面试必背)
1、极简背诵流程(面试直接默写) 接收任务 → 判断核心线程数 → 核心线程未满新建核心线程;核心线程已满存入阻塞队列 → 队列已满新建非核心线程 → 达到最大线程数触发拒绝策略。
2、五步详细执行流程(底层完整版)
① 判断核心线程数:提交任务,线程池检测当前运行线程数是否小于核心线程数;若是,直接新建核心线程执行任务;
② 判断阻塞队列:当前线程数≥核心线程数,线程池判断阻塞队列是否已满;队列未满,将任务存入队列排队等待执行;
③ 判断最大线程数 :队列已经存满,无法继续缓存任务;判断当前总线程数是否小于最大线程数;若是,新建非核心临时工线程处理任务;
④ 触发拒绝策略:总线程数达到最大线程数、队列也塞满,线程池彻底饱和;触发预设拒绝策略,处理超额任务;
⑤ 线程回收销毁:任务执行完毕后,非核心线程空闲时间超过设定存活时间,自动销毁释放资源;核心线程默认常驻不销毁。
3、通俗口诀(快速记忆)
先开核心干活、再进队列排队、队列满了扩招、满人直接拒绝、临时工超时下岗。
4、高频面试坑点
① 线程池不会预先创建核心线程,默认懒加载,提交任务才创建;
② 永远优先用队列缓冲,不会直接新建大量非核心线程,避免频繁创建线程消耗性能;
③ 只有队列满了,才会扩容创建非核心线程;
④ 核心线程可手动开启超时回收,和非核心线程一起销毁。
5、一句话总结 :先核心、再队列、后扩容、满拒绝;临时工超时销毁,核心线程常驻待命。
4.四种拒绝策略?(满分完整版+源码+场景+面试必背)
1、一句话极简背诵 线程池饱和(核心线程+队列+最大线程全部占满)触发拒绝策略;四种策略:抛异常、主线程执行、丢弃队首、直接丢弃。
2、四大拒绝策略详细拆解(面试必背)
Java内置四种拒绝策略,全部实现 RejectedExecutionHandler 接口:
① AbortPolicy(默认策略、直接抛异常)
原理:任务饱和直接抛出 RejectedExecutionException 运行时异常,中断业务;
特点:直白报错、方便排查问题;
缺点:生产无处理会直接接口报错、服务异常;
适用:测试环境、需要强提醒报错、不可丢失任务场景。
② CallerRunsPolicy(调用者主线程执行)
原理:不抛异常、不丢弃任务;把超额任务退回给提交任务的主线程执行;
特点:主线程同步干活,减缓提交速度,天然限流;
缺点:阻塞主线程,影响接口响应速度;
适用:不允许丢任务、流量平缓、对响应耗时不敏感场景。
③ DiscardOldestPolicy(丢弃队首最老任务)
原理:丢弃阻塞队列中**最先排队、最老旧**的未执行任务,腾出位置放入新任务;
特点:牺牲旧任务、保留新任务;
缺点:无日志提示,静默丢失任务,数据无告警;
适用:时效性低、老旧任务无意义、实时性高的业务。
④DiscardPolicy(直接丢弃当前任务)
原理:静默丢弃当前超额新任务,**无异常、无日志、无提示**;
特点:极致安静、不报错不阻塞;
缺点:任务丢失无感知,线上隐患极大;
适用:可丢弃、非核心、无关紧要的流量任务。
3、策略对比汇总表格
| 拒绝策略 | 核心处理方式 | 是否抛异常 | 是否阻塞主线程 | 是否丢失任务 | 核心适用场景 |
|---|---|---|---|---|---|
| AbortPolicy(默认策略) | 任务饱和直接抛出RejectedExecutionException运行时异常,中断业务执行 |
是 | 不阻塞 | 不丢失 | 测试环境、需要强报错校验、不可丢失核心任务的场景 |
| CallerRunsPolicy | 不抛异常、不丢弃任务,将超额任务退回给提交任务的主线程同步执行 | 否 | 阻塞 | 不丢失 | 流量削峰限流、不可丢失任务、对响应耗时不敏感的业务场景 |
| DiscardOldestPolicy | 丢弃阻塞队列中最先排队、最老旧的未执行任务,腾出队列位置放入新任务 | 否 | 不阻塞 | 丢弃老旧任务 | 时效性要求高、老旧任务无业务价值、优先保障新任务执行的场景 |
| DiscardPolicy | 静默丢弃当前超额新任务,无异常、无日志、无任何提示 | 否 | 不阻塞 | 丢弃当前新任务 | 非核心、可舍弃、无业务影响的低优先级流量任务 |
4、生产开发规范(高频面试坑点)
① 线程池默认策略AbortPolicy,线上大量流量会批量抛异常,禁止直接使用;
② 生产绝对不推荐 Discard 系列,静默丢任务无日志,排查灾难;
③ 流量削峰、可控业务优先 CallerRunsPolicy 天然限流;
④ 高并发秒杀、时效性业务可用 DiscardOldestPolicy;
⑤ 大厂生产一般自定义拒绝策略:打印日志、存入MQ、降级兜底、告警通知。
5、一句话总结 :默认抛异常、回退主线程、丢弃老任务、丢弃新任务;线上禁用默认抛异常+静默丢弃,优先自定义降级拒绝策略。
5.禁止使用 Executors 创建线程池原因?(满分完整版+源码风险+生产大坑)
1、一句话极简背诵
Executors 创建的线程池存在无界队列、最大线程数无上限问题,高并发下极易引发 OOM 内存溢出,且参数不透明、不可定制,生产强制禁用。
2、四种常用线程池底层致命风险(面试必背)
① newFixedThreadPool(固定线程池)
底层:使用无界阻塞队列 LinkedBlockingQueue;
风险:任务无限堆积、队列无上限,大量请求积压持续占用内存,直接 OOM。
② newSingleThreadExecutor(单线程池)
底层:同样使用无界 LinkedBlockingQueue;
风险:单线程串行执行,任务无限积压,内存持续暴涨引发 OOM。
③ newCachedThreadPool(缓存线程池)
底层:最大线程数为 Integer.MAX_VALUE,无上限;
风险:无限创建线程,线程数量过多耗尽操作系统资源,导致线程溢出、服务卡死。
④ newScheduledThreadPool(定时线程池)
底层:核心线程固定,非核心线程无上限;
风险:大量定时任务触发,无限创建线程,造成内存溢出、CPU 飙升。
3、三大核心禁用原因(面试满分总结)
① 队列/线程无边界:无界队列无限存任务、部分线程池线程无上限,不可控;
② 风险不可预判:流量突增瞬间堆积任务/线程,线上毫无缓冲直接 OOM、服务宕机;
③ 参数不灵活:Executors 封装死参数,无法自定义拒绝策略、线程名称、空闲时间,生产无法适配复杂业务。
4、生产开发规范(强制编码规约)
✅ 必须手动通过 ThreadPoolExecutor 原生构造方法创建线程池;
✅ 自定义七大核心参数:有界队列、合理线程数、自定义拒绝策略、业务线程名称;
✅ 杜绝无界队列、杜绝线程无上限,从源头规避内存溢出风险。
5、一句话总结 :Executors 封装无脑、无界队列堆任务、无限线程耗资源;极易OOM、生产高危,业务必须手动原生构建线程池。
6.IO 密集与 CPU 密集线程池配置?(满分完整版+计算公式+生产最优配置)
1、核心判定标准
① CPU密集型:大量计算、逻辑运算、循环判断、加密解密、序列化,CPU一直处于高速运算状态,极少阻塞;
② IO密集型:数据库查询、Redis缓存、网络请求、文件读写、接口调用,线程大量时间阻塞等待IO响应,CPU空闲。
2、面试必背计算公式
① CPU密集型:核心线程数 = CPU核心数 + 1;
② IO密集型:核心线程数 = CPU核心数 * 2 ~ 5;
③ 通用优化公式(生产大厂):核心线程数 = CPU核心数 / (1 - 阻塞系数),阻塞系数0~1,IO越多阻塞系数越大。
3、详细配置原理
① CPU密集配置原理:CPU核心有限,线程过多会造成频繁上下文切换,降低运算效率;仅多开1条线程兜底,防止CPU核心空闲,最大化压榨计算性能;
② IO密集配置原理:线程大量时间阻塞等待IO,CPU空闲,可多创建线程抢占CPU资源,提升系统吞吐量,不浪费CPU算力。
4、生产环境最佳实践
✅ CPU密集:核心线程数=本机CPU核心数+1、有界队列偏小、拒绝策略降级,禁止大量线程抢占CPU;适合大数据运算、批量解析、复杂算法业务。
✅ IO密集:核心线程数放大2~5倍、队列适中,容忍大量阻塞线程;适合接口请求、数据库操作、文件、缓存交互业务。
5、高频面试坑点
① 不要盲目设置超大线程数,CPU密集线程过多=频繁上下文切换=性能暴跌;
② IO密集无固定线程数,阻塞时间越长,可配置线程数越大;
③ 线上服务器建议通过代码获取CPU核心数:Runtime.getRuntime().availableProcessors(),不要硬编码;
④ 混合业务(既有计算又有IO),折中配置,压测调优为准。
6、一句话总结 :CPU密集少线程、防切换;IO密集多线程、填空闲;公式为参考、压测定最终。
7.核心线程会被回收吗?
默认不会,allowCoreThreadTimeOut 开启可回收。
8.线程池如何优雅关闭?
shutdown () 平稳关闭;shutdownNow () 强制关闭。
9.ScheduledThreadPool 定时线程池原理?
延迟队列实现定时任务。
10.虚拟线程 Virtual Thread 优点?(JDK21+满分完整版+底层原理+面试必考)
1、一句话极简背诵 虚拟线程是JDK21推出的轻量级用户态线程,不绑定OS内核线程,开销极低、无需手动池化、海量创建无压力,专治IO密集型阻塞浪费,是Java并发革命性升级。
2、五大核心优点(面试必背)
① 极致轻量化、占用资源极低 :虚拟线程不独占操作系统内核线程,栈内存极小(几百字节);传统物理线程栈内存MB级别,一台服务器仅能创建几千条,虚拟线程可轻松创建百万级、千万级,无线程数量上限瓶颈。
② 阻塞不浪费载体线程 :物理线程阻塞时会一直占用内核线程资源;虚拟线程遇到IO、sleep、锁阻塞时,自动卸载载体线程,载体线程去执行其他任务,彻底消除阻塞空转浪费,CPU利用率拉满。
③ 无需手动使用线程池:原生无需池化复用,创建销毁成本近乎忽略不计;业务直接新建虚拟线程,告别线程池参数调优、线程复用脏数据问题,编码极简。
④ 兼容性极强、无侵入改造:完美兼容原有Thread API,语法无改动;原有多线程代码无需重构,仅切换线程实现即可使用虚拟线程,适配所有同步代码、锁工具。
⑤ 配套新特性、生态完善:搭配ScopedValue替代ThreadLocal、StructuredTaskScope结构化并发,自动管控线程生命周期,自动回收资源,杜绝内存泄漏、线程泄露。
3、底层核心原理 虚拟线程属于用户态线程,由JVM自主调度,而非操作系统调度;采用载体线程(平台线程)挂载执行,多虚拟线程复用少量载体线程,阻塞时动态解绑,实现资源高效复用。
4、适用业务场景
✅ 主打IO密集型任务:HTTP接口调用、数据库、Redis、文件IO、网络请求;
✅ 大量短时异步任务、批量查询、第三方接口批量调用;
✅ 简单异步业务,规避线程池复杂参数配置。
5、高频面试坑点(必考)
① 虚拟线程不适合CPU密集型:大量计算场景下,调度切换开销会拉低性能,物理线程更优;
② JDK21及以上长期支持版本可用,低版本无适配;
③ 虚拟线程默认非守护线程,虚拟机不会自动退出;
④ 禁用线程池池化虚拟线程,本身创建销毁无成本,池化无意义。
6、一句话总结 :轻量无栈开销、阻塞不浪费线程、海量创建无压力、无需线程池;专攻IO密集,JDK21并发官方终极优化。
六、死锁 + 高并发 + 进阶(91-100 题)
1.死锁四大必要条件?(满分完整版+通俗解释+面试必背)
1、一句话极简背诵 死锁产生必须同时满足互斥、请求保持、不可剥夺、循环等待四大条件,缺一不可;破坏任意一个条件即可避免死锁。
2、四大条件详细拆解(面试必考)
① 互斥条件:同一资源同一时刻只能被一个线程占用,不可共享;锁资源具有排他性,一个线程持有锁,其他线程只能阻塞等待。
② 请求且保持条件:线程已经持有一把锁,在不释放当前锁的前提下,继续请求获取其他新锁,持有旧锁、申请新锁。
③ 不可剥夺条件:线程获取锁之后,其他线程无法强制抢夺、强行回收该锁;只能由持有锁的线程主动执行完毕后手动释放。
④ 循环等待条件:多个线程之间形成环状锁依赖,线程A等B的锁、线程B等C的锁、线程C等A的锁,无限循环互相等待。
3、通俗举例
线程1持有锁A、请求锁B;线程2持有锁B、请求锁A;互斥占用、互不释放、互相等待,最终形成永久死锁。
4、面试核心口诀 :互斥占有、请求保持、不可强夺、循环等待。
5、一句话总结 :四大条件同时满足必死锁,生产打破任意一条,即可杜绝死锁发生。
2.如何避免死锁?(满分完整版+四大方案+生产规范+口诀)
1、极简背诵核心原理
死锁必须同时满足四大条件,生产只要破坏任意一个必要条件,就能彻底避免死锁,核心优化思路:打散循环等待、减少锁嵌套、可控抢锁、规范编码。
2、四大生产落地解决方案(面试必背)
① 统一锁的加锁顺序(最核心、最简单)
破坏循环等待条件:所有线程严格按照固定顺序获取锁,例如永远先锁A、再锁B,杜绝线程交叉反向加锁,从根源打破环状依赖,是生产最常用、零成本规避方案。
② 减少锁嵌套、降低锁粒度
破坏请求且保持条件:尽量不嵌套加锁,杜绝一把锁不释放、再申请另一把锁;缩小锁范围,同步代码块只包裹核心竞争代码,减少长时间持有多把锁的场景。
③ 使用限时锁、主动放弃抢占
破坏不可剥夺条件:放弃synchronized不可中断死等特性,使用ReentrantLock的tryLock(time)限时抢锁;规定时间内抢不到锁,主动释放已持有锁、退出竞争,避免永久阻塞等待。
④ 避免长时间持有锁+资源合理分配
破坏互斥占用:锁内业务精简,不做耗时IO、远程调用、复杂计算;快速加锁、快速释放,减少锁持有时长,降低锁竞争僵持概率。
3、辅助优化手段(面试加分)
① 禁止随意嵌套锁,业务编码严控双层及以上锁嵌套;
② 定时检测线程状态,线上使用jstack排查死锁;
③ 复杂并发场景优先使用显式锁,灵活可控优于synchronized;
④ 业务拆分,拆分竞争资源,减少多线程争抢同一批锁资源。
4、通俗背诵口诀顺序加锁破循环、减少嵌套破持有、限时抢锁破剥夺、缩短耗时破互斥。
5、一句话总结 :统一顺序、少嵌锁、限时抢、快释放;破坏四大死锁条件,生产彻底规避死锁卡死。
3.死锁排查命令?(满分完整版+生产实操步骤+面试必背)
1、核心常用命令(面试必考)
① jstack -l pid :最核心排查命令,打印线程堆栈信息,底部关键词 Found one Java-level deadlock 精准定位死锁线程、锁对象、代码行数;
② jps -l:查询Java进程pid,快速获取需要排查的进程号;
③ jmap:排查内存快照,辅助分析死锁伴随的内存占用异常;
④ jcmd pid Thread.print:JDK1.8+新版命令,替代jstack,打印线程堆栈、死锁信息。
2、生产完整排查步骤(实操流程)
① 命令 jps -l 获取项目运行pid;
② 命令 jstack -l pid > deadlock.log 导出堆栈日志;
③ 搜索关键字 deadlock,定位死锁线程、锁资源、代码位置;
④ 分析线程互相持有、等待的锁对象,梳理循环依赖链路;
⑤ 修改代码、调整加锁顺序,重启服务修复。
3、可视化排查工具
① JDK自带:jvisualvm 图形化监控线程,直观查看死锁标记;
② 第三方:Arthas线上诊断,thread命令一键检测死锁、打印线程栈。
4、高频面试坑点
① jstack不仅排查死锁,还可排查线程阻塞、死循环、CPU飙高;
② 死锁日志固定在文件最底部,直接搜索deadlock最快定位;
③ 线上禁止频繁dump堆栈,瞬间卡顿影响服务性能。
5、一句话总结 :jps查进程、jstack抓堆栈、搜deadlock定位死锁;线上可用Arthas一键排查,精准定位代码行。
4.死锁、活锁、饥饿区别?(满分完整版+对比表格+通俗口诀+面试必考)
1、一句话极简背诵
死锁互相僵持永久阻塞、活锁反复重试一直失败、饥饿长期抢不到锁;三者都是线程异常阻塞问题,优先级:死锁危害最大、饥饿次之、活锁最轻。
2、三者详细定义(面试必背)
① 死锁(Deadlock) :多条线程互相持有对方需要的锁,互相无限等待、僵持不动,线程全部阻塞,永不释放、永不执行,无任何执行进展。
②活锁(Livelock) :线程不会阻塞挂起,一直主动重试获取资源、频繁切换状态,但是互相谦让、一直抢占失败,忙碌空转、无法执行业务,代码一直跑但是没有结果。
③ 线程饥饿(Starvation) :部分线程优先级低、排队靠后,一直被高优先级线程抢占资源,长期抢不到锁,迟迟无法执行、长期闲置,极端情况下永久得不到执行机会。
3、三者核心区别汇总表格
| 对比维度 | 死锁 | 活锁 | 线程饥饿 |
|---|---|---|---|
| 线程状态 | 永久阻塞、静止挂起 | 一直运行、频繁空转 | 长期等待、极少执行 |
| 阻塞状态 | 阻塞不释放 | 活跃无阻塞 | 长期排队无执行 |
| 产生原因 | 循环等待 + 互相持有锁 | 互相谦让、反复抢占失败 | 非公平锁、优先级差异 |
| 典型案例 | 双线程互相持有对方锁 | 两条线程互相主动退让锁 | 低优先级线程一直抢不到锁 |
| 是否自愈 | 无法自愈、永久卡死 | 极端情况可自行恢复 | 不会自愈、长期等待 |
| 危害程度 | 最高、服务卡死 | 中等、CPU 空转 | 最低、业务延迟 |
4、通俗直白比喻
① 死锁:两个人面对面站死,互相等着对方让路,谁都不动;
② 活锁:两个人互相礼貌让路,左右反复横跳,一直错开永远走不过去;
③ 饥饿:弱小的人一直被插队,永远排不到自己通行。
5、生产解决方案
① 解决死锁:统一加锁顺序、破坏四大必要条件;
② 解决活锁:增加随机重试时间,避免线程同步谦让;
③ 解决饥饿:使用公平锁,保证先来先执行,杜绝插队。
6、一句话终极总结 :死锁卡死不动、活锁空转忙等、饥饿长期插队;死锁最致命、公平锁根治饥饿、随机延时规避活锁。
5.什么是乐观锁悲观锁?(满分完整版+对比表格+面试必考)
1、一句话极简背诵
悲观锁默认并发冲突、全程加锁互斥、串行执行;乐观锁默认无冲突、不加锁、版本校验后置,冲突才重试。
2、悲观锁(Pessimistic Lock)
① 核心思想:极度悲观,默认多线程并发一定会产生资源竞争,访问资源前先加锁,独占资源,防止并发修改。
② 执行特点:独占排他、串行执行、线程阻塞、安全性高、并发性能差。
③ 典型实现:synchronized、ReentrantLock、数据库行锁/表锁。
④ 适用场景:写多读少、竞争激烈、频繁修改、数据一致性要求极高的场景。
3、乐观锁(Optimistic Lock)
① 核心思想:极度乐观,默认线程之间无资源竞争,无需加锁;更新数据时校验版本号/标记,判断是否被其他线程修改,冲突则重试或放弃。
② 执行特点:无锁并发、并行执行、无阻塞、性能高、存在重试开销。
③ 典型实现:CAS无锁算法、数据库版本号锁、ConcurrentHashMap、Atomic原子类。
④ 适用场景:读多写少、竞争微弱、追求高并发吞吐量、允许少量冲突重试的场景。
4、两者核心区别对比(面试必背表格)
| 对比维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 加锁时机 | 访问资源前提前加锁,全程独占资源 | 仅更新数据时做版本校验,全程无锁 |
| 核心并发思想 | 默认多线程并发一定会产生资源冲突,提前加锁规避风险 | 默认线程之间无资源竞争,无锁并行,冲突后再重试处理 |
| 线程执行状态 | 未抢到锁的线程阻塞挂起,任务串行执行 | 无阻塞挂起,多线程并行执行,仅冲突时自旋重试 |
| 代码实现成本 | 简单粗暴,底层已封装加锁释放逻辑,无需额外处理冲突 | 需自行处理冲突重试、版本校验逻辑,代码实现复杂度更高 |
| 核心适用场景 | 写多读少、资源竞争激烈、数据一致性要求极高的场景 | 读多写少、并发量极大、资源竞争微弱、追求高吞吐量的场景 |
| 系统性能开销 | 线程内核态阻塞挂起,上下文切换开销大,系统吞吐量低 | 仅冲突时 CPU 自旋重试,无阻塞挂起开销,系统并发性能更高 |
5、高频面试坑点
① CAS乐观锁存在ABA问题,需加版本号标记规避;
② 悲观锁阻塞挂起、耗费内核态资源;乐观锁自旋占用CPU,大量冲突性能暴跌;
③ MySQL中InnoDB默认行级悲观锁,也可手动实现版本号乐观锁;
④ Java中synchronized是悲观锁,Atomic类、CAS属于乐观锁。
6、一句话总结 :悲观锁上锁防冲突、安全低速;乐观锁无锁验冲突、高速容错;写多用悲观、读多用乐观。
6.分布式并发常用方案?(满分完整版+五大方案+生产代码实现)
1、一句话极简背诵
分布式并发核心解决:多服务多节点共享资源竞争、流量洪峰、重复提交、数据一致性;
五大核心方案:分布式锁、限流熔断、幂等防重、消息队列削峰、分布式事务。
2、五大核心方案详解+代码实现(面试必考+生产落地)
方案一:分布式锁(解决跨节点资源竞争)
① 核心作用:控制分布式环境下多节点争抢同一共享资源,保证同一时间只有一个节点执行业务,替代本地synchronized/Lock;
② 常用实现:Redisson分布式锁(主流)、Zookeeper锁、Redis原生SETNX锁;
③ 生产代码(Redisson最简通用版):
java
// 1、注入Redisson客户端
@Autowired
private RedissonClient redissonClient;
// 2、分布式锁业务实现
public void deductStock() {
// 定义锁key,全局唯一
String lockKey = "stock:lock:1001";
RLock lock = redissonClient.getLock(lockKey);
try {
// 加锁:等待3s,持有锁10s自动过期
boolean isLock = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (isLock) {
// 执行扣库存、修改共享数据等并发业务
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁,防止死锁
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
方案二:流量限流(防止服务被流量打崩)
① 核心作用:限制接口访问频次,拦截恶意流量、突发洪峰,保护服务稳定性;分为单机限流、分布式限流;
② 常用算法:令牌桶、漏桶、滑动时间窗口;常用组件:Sentinel、Gateway限流;
③ 生产代码(Sentinel注解限流):
java
// 接口限流,每秒最大访问5次
@SentinelResource(value = "order:create", blockHandler = "flowBlockHandler")
@GetMapping("/createOrder")
public String createOrder() {
return "下单成功";
}
// 限流降级兜底方法
public String flowBlockHandler(BlockException e) {
return "系统繁忙,请稍后重试";
}
方案三:接口幂等(防止重复提交、重复扣款)
① 核心作用:同一请求多次提交,服务只处理一次,杜绝重复下单、重复扣款、重复数据入库; ② 常用实现:唯一Token、数据库唯一索引、分布式唯一ID、状态机判断;
③ 生产代码(Token幂等防重):
java
// 下单前获取唯一Token
@GetMapping("/getToken")
public String getToken() {
String token = UUID.randomUUID().toString();
// Token存入Redis,有效期5分钟
redisTemplate.opsForValue().set("idempotent:token:"+token, "1",5,TimeUnit.MINUTES);
return token;
}
// 幂等下单接口
@PostMapping("/order")
public String order(String token) {
// 删除Token,删除成功则首次请求,失败则重复请求
Boolean delete = redisTemplate.delete("idempotent:token:" + token);
if (!delete) {
return "请勿重复提交";
}
// 执行业务逻辑
return "下单成功";
}
方案四:消息队列削峰填谷(异步解耦、抗高并发)
① 核心作用:瞬时高流量存入MQ队列,同步转异步,削平流量峰值,防止同步接口卡顿超时;适配秒杀、批量通知、异步日志;
② 常用组件:RabbitMQ、RocketMQ、Kafka;
③ 极简业务代码:
java
// 生产者:接收请求,发送消息至队列,快速响应
@Autowired
private RocketMQTemplate rocketMQTemplate;
@GetMapping("/seckill")
public String seckill(Long goodsId) {
rocketMQTemplate.syncSend("seckill_topic", goodsId);
return "排队中,抢购成功稍后通知";
}
// 消费者:异步消费,扣库存、生成订单
@RocketMQMessageListener(topic = "seckill_topic", consumerGroup = "seckill_group")
public class SeckillConsumer implements MessageListenerConcurrently<Long> {
@Override
public ConsumeResult consumeMessage(List<MessageExt<Long>> list, ConsumeContext consumeContext) {
// 异步执行业务,无阻塞高吞吐
return ConsumeResult.SUCCESS;
}
}
方案五:分布式事务(保证多服务数据一致)
① 核心作用:解决微服务跨库、跨服务数据一致性问题,防止事务回滚导致数据错乱;
② 常用方案:Seata AT模式(无侵入常用)、TCC、SAGA、本地消息表;
③ 核心使用注解:
java
// 全局事务开启,跨服务保证数据一致
@GlobalTransactional(rollbackFor = Exception.class)
public void createGlobalOrder() {
// 1、创建订单
// 2、扣减库存
// 3、账户扣款
}
3、面试高频选型规则(必背)
① 资源竞争、防止超卖 → 分布式锁;
② 流量过大、防雪崩 → 限流熔断;
③ 重复提交、重复扣款 → 接口幂等;
④ 瞬时洪峰、异步业务 → 消息队列削峰;
⑤ 微服务跨库、数据一致 → 分布式事务。
4、高频面试坑点
① Redis分布式锁存在主从延时丢锁问题,生产优先Redisson看门狗续期;
② 幂等必须加过期时间,防止Redis无限堆积脏数据;
③ MQ必须配置消息确认、死信队列,防止消息丢失;
④ 分布式锁禁止使用本地锁,多节点完全失效。
5、一句话总结 :争抢用分布式锁、流量用限流、重复用幂等、洪峰用MQ、跨服务用事务;五大方案组合,覆盖全部分布式并发场景。
7.CompletableFuture 作用?(满分完整版+底层+链式编排+面试必考)
1、一句话极简背诵
JDK8推出的异步任务编排类,替代传统Future,解决回调地狱、手动阻塞等待问题;支持链式编程、多任务组合、异步回调,优雅实现复杂多线程业务编排。
2、传统Future痛点(面试对比加分)
① 获取结果必须阻塞/轮询,代码卡顿、效率低;
② 无回调机制,任务结束无法自动通知;
③ 多任务嵌套串行,代码臃肿、回调地狱;
④ 无法灵活组合多个异步任务。
3、五大核心作用(面试必背)
① 非阻塞回调:任务执行完毕自动回调方法,无需主动阻塞get(),不浪费CPU;
② 链式流式编排:采用Lambda链式写法,代码简洁流畅,彻底消灭回调地狱;
③ 多任务灵活组合:提供allOf()全部等待、anyOf()任意一个完成,精准管控多线程执行逻辑;
④ 异常自动捕获处理:内置exceptionally()异常回调,无需手动try-catch捕获线程异常;
⑤ 线程切换灵活:支持当前线程、自定义线程池切换执行任务,灵活管控执行载体。
4、常用核心方法
① 异步执行:supplyAsync(有返回值)、runAsync(无返回值);
② 结果回调:thenApply、thenAccept、thenRun;
③ 任务组合:allOf(全部完成)、anyOf(任一完成);
④ 异常兜底:exceptionally异常补偿、whenComplete最终执行。
5、极简代码演示
java
// 异步执行+链式回调,无阻塞、无回调地狱
CompletableFuture.supplyAsync(() -> {
// 异步耗时任务
return "异步查询结果";
}).thenAccept(res -> {
// 任务完成自动回调处理结果
System.out.println(res);
}).exceptionally(e -> {
// 异常兜底处理
return null;
});
6、生产高频使用场景
① 多接口并行查询、批量异步调用,缩短接口响应时间;
② 复杂业务任务拆分、异步编排执行;
③ 无需同步等待结果的后台异步任务;
④ 替代原始Thread+Future,简化异步编码。
7、高频面试坑点
① 默认使用ForkJoinPool公共线程池,IO密集任务建议自定义线程池隔离,防止公共池被耗尽; ② get()方法会阻塞线程,生产优先使用异步回调,禁止主动阻塞;
③ 多任务编排优先allOf/anyOf,不要嵌套多层CompletableFuture;
④ 必须捕获异常,否则异步异常静默丢失,排查困难。
8、一句话总结 :弥补Future阻塞缺陷、链式异步编排、支持多任务组合、自带异常回调;简化异步代码、消灭回调地狱,生产异步业务首选。
8.结构化并发 StructuredTaskScope 作用?(JDK21+满分完整版+底层+面试必考)
1、一句话极简背诵
JDK21推出的结构化并发工具类,统一管控一批子线程生命周期,父子线程绑定、异常联动、自动取消、无线程泄露,解决传统异步线程分散失控、排查困难、资源泄露痛点,搭配虚拟线程最佳。
2、四大核心作用(面试必背)
① 生命周期绑定:父线程创建的所有子线程统一纳入管控,父线程等待全部子线程执行完毕后才结束,杜绝线程泄露、残留后台幽灵线程;
② 异常联动传播:任意一条子线程抛出异常,自动取消其他未完成子线程,快速失败、避免无效任务空转,防止资源浪费;
③ 自动回收资源:无需手动关闭线程、无需兜底捕获,作用域结束自动销毁所有子线程,彻底规避内存泄漏、句柄残留;
④ 链路清晰、排查简单:线程层级关系明确,堆栈信息可追溯父子线程调用链路,解决异步线程日志混乱、线上排查难问题。
3、底层设计思想 遵循结构化并发编程思想:任务有边界、线程有归属、异常可联动、资源必回收;替代手动CountDownLatch、CompletableFuture批量管控,简化多任务编排。
4、极简代码演示
java
// 创建结构化任务作用域,统一管控子线程
try(StructuredTaskScope<String> scope = new StructuredTaskScope<>()){
// 提交多条异步子任务
StructuredTaskScope.Subtask<String> task1 = scope.fork(() -> 接口查询1);
StructuredTaskScope.Subtask<String> task2 = scope.fork(() -> 接口查询2);
// 等待所有子任务执行完毕
scope.join();
// 获取结果,任意任务异常自动取消其他任务
}
5、高频面试坑点
① JDK21及以上专属,专为虚拟线程优化,物理线程同样兼容;
② try-with-resources语法自动关闭作用域,无需手动销毁线程;
③ 区别CompletableFuture:主打线程生命周期管控、异常联动取消,杜绝泄露;
④ 生产适合批量并行查询、多接口聚合、多级异步编排业务。
6、一句话总结 :父子线程绑定、统一生命周期、异常联动取消、自动回收资源;专治异步线程泄露、链路混乱,JDK21结构化并发官方标准方案。
9.高并发接口优化手段?(满分完整版+七层优化+企业实战代码+生产配置)
1、一句话极简背诵
高并发优化核心:抗流量、降耗时、削峰值、保稳定;
分层优化:前端防重、网关限流、本地缓存+分布式缓存、异步解耦、数据库优化、锁优化、兜底降级,层层拦截流量。
2、七层完整优化方案(企业标准架构)
第一层:前端优化(拦截无效流量)
① 页面静态化、CDN加速,静态资源缓存至边缘节点;
② 按钮防抖、禁止重复提交、前端本地缓存;
③ 图片/资源压缩、懒加载,减少请求频次与请求体积。
第二层:网关层优化(流量第一道关卡)
① 网关统一限流、黑名单拦截、恶意IP封禁;
② 接口权限校验、请求参数脱敏、非法请求过滤;
③ 路由负载均衡,Nginx+网关实现请求分发,避免单点压力。
第三层:应用层优化(代码核心优化)
① 缓存优化:多级缓存(本地缓存Caffeine+Redis分布式缓存),减少DB查询;
② 异步解耦:CompletableFuture异步编排、MQ削峰,同步转异步,缩短接口响应;
③ 线程池优化:区分CPU/IO密集线程池,隔离业务线程,避免线程互相抢占;
④ 锁优化:缩小锁粒度、禁用悲观锁、高并发使用ReentrantLock、规避死锁;
⑤ 接口幂等:Token+唯一索引,防止重复提交、重复扣款。
第四层:缓存层优化(减少DB压力)
① 多级缓存:本地缓存优先查询,未命中再查Redis,最后查数据库;
② 缓存策略:热点数据永不过期、定时更新,冷门数据设置过期淘汰;
③ 缓存问题解决:缓存穿透(布隆过滤器)、缓存击穿(互斥锁+热点永不过期)、缓存雪崩(过期随机+集群部署)。
第五层:数据库优化(最终承压层)
① SQL优化:禁止大字段查询、避免联表、加合理索引、防止慢SQL;
② 读写分离:主库写、从库读,拆分读写流量;
③ 分库分表:大数据量水平分表,单表数据控制在1000w以内;
④ 连接池优化:调整MySQL连接数,避免连接耗尽。
第六层:流量管控(防止服务雪崩)
① 限流:Sentinel令牌桶限流、网关限流,限制QPS;
② 降级:非核心业务降级、返回兜底数据,保障核心流程;
③ 熔断:依赖服务异常熔断,防止级联雪崩。
第七层:架构优化(海量流量终极方案)
① 服务集群:多实例部署,负载均衡分摊流量;
② 异地多活、机房容灾,防止单点机房故障;
③ 业务拆分:微服务拆分,业务隔离,故障不扩散。
3、企业开发实战代码(可直接上线)
① 多级缓存实战(Caffeine+Redis)
java
// 1、本地缓存Caffeine配置(高性能本地缓存)
@Bean
public Cache<Long, GoodsInfo> caffeineCache() {
return Caffeine.newBuilder()
.maximumSize(10000) // 最大缓存数量
.expireAfterWrite(3, TimeUnit.MINUTES) // 写入过期
.recordStats() // 统计缓存命中率
.build();
}
// 2、业务查询多级缓存执行逻辑
@Autowired
private Cache<Long, GoodsInfo> localCache;
@Autowired
private StringRedisTemplate redisTemplate;
public GoodsInfo getGoodsInfo(Long goodsId){
// 1、查询本地缓存
GoodsInfo local = localCache.getIfPresent(goodsId);
if(Objects.nonNull(local)) return local;
// 2、查询Redis分布式缓存
String redisKey = "goods:info:" + goodsId;
String json = redisTemplate.opsForValue().get(redisKey);
if(StringUtils.hasNotText(json)){
GoodsInfo goodsInfo = JSON.parseObject(json, GoodsInfo.class);
localCache.put(goodsId,goodsInfo); // 回填本地缓存
return goodsInfo;
}
// 3、查询数据库+回填缓存
GoodsInfo dbInfo = goodsMapper.selectById(goodsId);
if(Objects.isNull(dbInfo)) return null;
redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(dbInfo),5,TimeUnit.MINUTES);
localCache.put(goodsId,dbInfo);
return dbInfo;
}
② Sentinel限流降级实战(生产通用)
java
// 接口限流+降级,高并发保护接口
@SentinelResource(value = "goods:detail", blockHandler = "flowHandler", fallback = "fallbackHandler")
@GetMapping("/goods/detail/{id}")
public Result<GoodsInfo> getDetail(@PathVariable Long id){
// 正常业务查询逻辑
return Result.success(goodsService.getGoodsInfo(id));
}
// 限流兜底:流量超限触发
public Result<GoodsInfo> flowHandler(Long id, BlockException e){
return Result.fail("访问人数过多,请稍后重试");
}
// 降级兜底:业务异常触发
public Result<GoodsInfo> fallbackHandler(Long id, Throwable e){
return Result.fail("服务繁忙,临时降级兜底");
}
③ CompletableFuture异步并行优化(缩短接口耗时)
java
// 多接口并行查询,串行500ms+,并行压缩至150ms
@Override
public Result<GoodsDetailVO> getGoodsDetailAsync(Long goodsId) {
// 1、并行执行三个耗时查询
CompletableFuture<GoodsInfo> infoFuture = CompletableFuture.supplyAsync(() -> goodsMapper.selectById(goodsId), goodsThreadPool);
CompletableFuture<List<GoodsSku>> skuFuture = CompletableFuture.supplyAsync(() -> skuMapper.selectList(goodsId), goodsThreadPool);
CompletableFuture<GoodsStock> stockFuture = CompletableFuture.supplyAsync(() -> stockMapper.selectById(goodsId), goodsThreadPool);
// 2、全部任务执行完毕,汇总结果
CompletableFuture.allOf(infoFuture,skuFuture,stockFuture).join();
// 3、封装返回数据
GoodsDetailVO vo = new GoodsDetailVO();
vo.setGoodsInfo(infoFuture.join());
vo.setSkuList(skuFuture.join());
vo.setStock(stockFuture.join());
return Result.success(vo);
}
④ Redis布隆过滤器解决缓存穿透
java
// 拦截不存在的商品ID,防止穿透DB
@Bean
public BloomFilter<Long> goodsBloomFilter(){
return BloomFilter.simple(1000000,0.01);
}
// 业务使用:查询前先校验
if(!bloomFilter.contains(goodsId)){
return Result.fail("商品不存在");
}
4、生产避坑规范(面试高频)
① 禁止大事务、长事务,事务内不做远程调用、循环查询;
② 缓存设置过期时间,热点数据定时刷新,避免缓存一致性问题;
③ 线程池务必自定义,禁止使用Executors,隔离业务线程;
④ 限流降级必须配置兜底策略,避免前端报错白屏;
⑤ 高并发接口禁止频繁打印日志、禁止序列化大对象。
5、一句话终极总结 :七层分层拦截流量、多级缓存压DB、异步并行缩耗时、限流降级保稳定、规范编码避坑;以上代码全部生产通用,可直接落地企业高并发项目。
10.多线程面试最终总结核心?(满分终极总结+背诵万能话术+逻辑闭环)
1、核心底层逻辑
多线程本质就是利用CPU资源、切换任务执行;进程隔离资源,线程共享资源;并发交替执行、并行同时执行。所有线程底层依托JVM+操作系统调度,靠上下文切换实现多任务运行。
2、线程核心基础
线程七种生命周期,重点区分RUNNABLE、BLOCKED、WAITING、TIMED_WAITING;
sleep休眠不释放锁、wait等待主动释放锁;
线程终止优先volatile标记位+interrupt中断,禁止stop暴力销毁;
守护线程依附用户线程消亡,仅做后台监控。
3、锁体系核心
① synchronized:JDK内置悲观锁,自动加解锁,底层依靠对象头MarkWord+Monitor,存在无锁→偏向锁→轻量级锁→重量级锁升级机制,低并发性能优秀;
② Lock显式锁:基于AQS实现,手动加解锁,支持公平锁、超时锁、中断锁、精准唤醒,高并发性能更强;
③ 锁通用规范:尽量缩小锁粒度、避免锁嵌套、统一加锁顺序,防止死锁。
4、JUC常用工具
CAS无锁乐观锁解决轻量并发,存在ABA问题;
线程池七大参数必须熟记,生产禁止Executors,手动自定义有界队列线程池;区分CPU密集、IO密集线程池配置;
Atomic原子类、并发容器保证线程安全。
5、异步与新版特性
CompletableFuture实现异步任务链式编排,解决Future阻塞痛点;
JDK21推出虚拟线程+结构化并发+ScopedValue,轻量化管控线程、杜绝内存泄漏,专为IO密集业务优化。
6、并发常见问题与解决方案
死锁四大必要条件,破坏任意一条即可规避;死锁、活锁、饥饿三者区分清晰;
高并发三大缓存问题:穿透、击穿、雪崩,配套对应解决方案;
分布式并发依靠分布式锁、限流、幂等、MQ、分布式事务保障业务稳定。
7、生产编码硬性规范(面试必考兜底)
① 业务优先线程池,禁止手动new Thread;
② 低并发用synchronized,高并发复杂场景用ReentrantLock;
③ 减少上下文切换、避免频繁创建销毁线程;
④ 禁止死锁、禁止暴力销毁线程、禁止无界队列;
⑤ 高并发采用多级缓存、异步解耦、限流降级架构优化。
8、一句话终极面试万能总结多线程核心就是合理抢占CPU、安全共享资源、规范管控线程;基础看懂生命周期、锁吃透两套体系、线程池规范使用、并发问题对症下药,兼顾性能与线程安全,适配生产高并发业务。