← 学习门户 每日面试题 学习看板

2026-09-26

Java 并发与同步原语深度(JMM / volatile / CAS / synchronized / AQS → Netty 无锁 → String 设计)

10 题 · 平均 2.6/5 · low 3.33 · medium 3 · high 1.33 · 第 1 轮

#1 什么是并发,什么是并行? low java-concurrency

题目

什么是并发,什么是并行?

我的回答

并发跟并行看起来是一样的,但实际上是完全两种东西。并行是两任务同时在进行,它需要 CPU 多核去支撑,单核 CPU 是没有办法支撑这件事的。它的要求是两个任务同时并行的执行。而并发指的是在一定时间段内,同时时间段内有大量的请求被处理,一起来了多个请求,这个可以是单个 CPU 内核去处理,它可以通过它的分时系统,给不同的任务分配不同的时间片去处理这一件事,看上去是多个任务也是一起进行的,但实际上同一时间点上只进行了一个任务。这是我对并行跟并发的理解。

点评

评分:⭐⭐⭐⭐☆
优点:核心区别抓得准;明确指出并行需要多核 CPU;提到分时系统和时间片,理解 OS 层调度
不足:"大量请求"表述稍偏,并发本质是"多任务交替"不限定数量;未延伸到 Java 层面(线程/线程池/虚拟线程);未点出并发带来的工程代价(竞态、可见性、有序性)
补充:更严谨的定义——并行 = N 个任务在 N 个执行单元上真正同时运行;并发 = M 个任务在单/多执行单元上通过调度交替运行 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否清晰区分并发/并行本质,并延伸到并发编程的工程代价。

**知识点拆解**:
1. **底层定义差异**:并行 = "同一时间点上多个任务真的同时执行",需要多个执行单元(多核 CPU);并发 = "多个任务在一段时间窗口内交替执行、看上去同时",可在单核通过时间片实现
2. **调度机制**:并发依赖操作系统分时调度(time-slicing),上下文切换是核心动作;并行由 OS 把任务分配到多核,各核独立跑,还涉及 NUMA 亲和性
3. **工程代价**:并发引入三大问题——竞态(race condition)、可见性(visibility)、有序性(ordering);这是后续 JMM/volatile/synchronized 的根源
4. **Java 中的体现**:线程(用户态并发)、线程池(复用减少创建开销)、CompletableFuture(异步编排)、虚拟线程(JDK21,M:N 调度让并发更轻量)

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否指出"同一时间点"这个关键差别
- **广度**:能否延伸到线程、线程池、虚拟线程、JMM
- **应用**:能否主动指出并发带来的工程代价

**延伸递进面试题**:
1. **Java 中有哪些并发编程的工具?分别解决什么问题?**
- 解答要点:线程(基础)、线程池(复用)、ExecutorService(抽象)、CompletableFuture(编排)、ForkJoinPool(分治)、AtomicXxx/CAS(无锁)、虚拟线程(轻量)
2. **并发和并行哪个性能更好?**
- 解答要点:并行扩展性线性(受核数约束),并发受上下文切换开销影响;Amdahl 定律限制并行加速比
3. **虚拟线程为什么让并发更简单?**
- 解答要点:M:N 调度(少量载体线程映射大量虚拟线程)、栈内存只占几十 KB、同步阻塞不占平台线程

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,我先说一下我的理解。并发和并行看着像一回事,但本质是完全两种东西。

**并行的核心是"同一时间点上真的同时"**,它需要多个执行单元——也就是多核 CPU——来支撑。双核 CPU 上两个任务可以真的在同一时钟周期上各占一个核跑。单核 CPU 无论怎么折腾都做不出真正的并行。

**并发的核心是"多个任务在一段时间窗口内交替执行,看上去像是同时"**,它允许在单核上通过操作系统的分时调度实现。OS 给每个任务分配一个时间片,用完就切换上下文,因为切换足够快,用户看上去就是同时在进行。

所以关键区别是:并行强调"同一时间点",并发强调"一段时间窗口"。并行是硬件能力决定的,并发是调度策略决定的。

进一步说,并发会带来工程代价。多个任务同时访问共享资源时有**竞态条件**;一个线程改了共享变量另一个看不到,这是**可见性**问题;指令重排会带来**有序性**问题。这三个是并发编程最头疼的部分。Java 提供了一整套工具来解决:volatile 保证可见性和有序性、synchronized 和 Lock 保证互斥、AtomicXxx 和 CAS 提供无锁原子操作。

Java 生态里常见载体有:线程是基础、线程池做复用减少创建开销、CompletableFuture 编排异步任务、JDK 21 虚拟线程让并发更轻量。

以上就是我对并发和并行的理解。

#2 什么是线程安全 low java-concurrency

题目

什么是线程安全

我的回答

线程安全本质映射的问题就是线程不安全,然后它更衍生,其实它名字应该是多线程下的安全,单线程不存在所谓线程安全的问题。多线程下出现线程安全问题,指的是对有状态的情况下,不同的线程做不同的修改,导致信息错乱,应该是这么一种情况,是信息不安全。然后为了处理这个事,这个信息不安全的问题,才衍生了线程安全的这么一个事情。所以所谓线程安全,就是处理多线程下有状态的数据的变更。要保证这个线程更新数据的时候,各个线程看到的是自己的数据,或者是大家同一时间只能有一个人更改,其他人等待。比如线程锁,锁住一个进程,锁住一个数据,这个对象要修改的时候,只能由一个线程去进行修改,其他线程需要等待。另外还有前置的条件,就是线程安全有状态,无状态它就线程安全了,没有多线程并行修改的影响,就不会有相互的线程安全问题存在,也就多线程可以正常使用了。定了两个方向,一个是 Redis 选择,就单线程,大家就单线程走,没有什么多线程并发的问题,我也不做处理,大家就一个线程跑。Java 里对于并发线程安全,就是常规的加锁或者是无状态,这么去处理。就比如线程栈、任务栈,任务栈内的数据,它不逃离这个方法栈,它就在方法栈内执行,它就不会涉及这个问题,不会有多线程进来去影响。

点评

评分:⭐⭐⭐☆☆
优点:抓到"多线程+有状态"才是前提这一关键直觉;举了 Redis 单线程、方法栈局部变量等具体例子;有工程实例感
不足:表述冗长、逻辑跳跃;未从"共享资源"核心点切入;"锁住进程"表述不准(应为锁对象/代码块);未提到底层机制(JMM/可见性/有序性);未系统归纳三个层次
补充:标准定义——多线程访问共享资源时,任意时刻执行结果都符合预期;三个层次:互斥、可见性、有序性;实现手段:synchronized、Lock、volatile、AtomicXxx、ThreadLocal、无状态化 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清"多线程+共享资源"这个前提,以及互斥/可见性/有序性三个层次。

**知识点拆解**:
1. **底层前提**:线程安全只在"多线程 + 共享可变资源"同时成立时才有意义;无共享、无状态、无状态可变都不构成安全问题
2. **三个层次**:
- **互斥(Mutual Exclusion)**:同一时刻只有一个线程能修改,避免"读改写"竞态
- **可见性(Visibility)**:一个线程的修改对其他线程立即可见(涉及内存屏障、CPU 缓存一致性)
- **有序性(Ordering)**:指令不能随意重排破坏业务语义(涉及 JIT 优化、CPU 乱序执行)
3. **实现手段谱系**:
- 强制同步:synchronized、ReentrantLock、ReadWriteLock
- 无锁原子:AtomicXxx、CAS、VarHandle
- 弱化共享:ThreadLocal、不可变对象、无状态化
- 单线程:Redis 主线程、Netty 单 Reactor 线程
4. **反例与陷阱**:ConcurrentHashMap 不是线程安全的(只保证基本操作原子,复合操作仍需同步);synchronized 块内调用外部方法可能丢锁

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否区分"互斥/可见性/有序性"三个层次
- **广度**:能否列举多种实现手段,说出各自的适用场景
- **应用**:能否举出工程中的具体线程安全/不安全案例

**延伸递进面试题**:
1. **ConcurrentHashMap 是线程安全的吗?**
- 解答要点:基本操作(get/put)线程安全,但复合操作(先判断再 put、遍历中修改)不安全;需用 computeIfAbsent 等原子方法
2. **synchronized 和 volatile 分别解决什么问题?**
- 解答要点:synchronized 同时解决互斥+可见性+有序性;volatile 只解决可见性+有序性,不保证互斥(i++ 不行)
3. **有哪些"看起来安全实际不安全"的坑?**
- 解答要点:双重检查单例必须 volatile;HashMap 并发 put 会死循环;SimpleDateFormat 非线程安全;ThreadLocal 在线程池会污染

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,线程安全的本质是"多线程访问共享可变资源时,任意时刻的执行结果都符合预期"。我把它拆成三个层次来说。

**第一个层次是互斥**——同一时刻只能有一个线程修改共享数据,避免"读-改-写"竞态。经典例子是 i++,其实是三条指令:读 i、i+1、写回,两个线程同时执行就会丢更新。synchronized 和 Lock 就是干这个的。

**第二个层次是可见性**——一个线程改了共享变量,其他线程必须能立刻看到新值。这里有个反直觉的点:现代 CPU 每个核有自己的 L1 缓存,一个核改了主存,另一个核的缓存不会立刻失效,JIT 还会重排指令。volatile 就是给编译器加屏障、给 CPU 加 MESI 协议失效,强制写操作刷到主存、读操作从主存取。

**第三个层次是有序性**——编译器和 CPU 会重排指令来提升性能,但有时候会破坏业务语义,比如双重检查锁单例,如果不加 volatile,实例可能以"半初始化"的状态被其他线程看到。

工程上的应对手段我大概分四类:一是**强制同步**,synchronized 和 ReentrantLock,重量级但灵活;二是**无锁原子**,AtomicXxx 和 CAS,性能好但有 ABA 和自旋开销;三是**弱化共享**,ThreadLocal、不可变对象、纯函数;四是**单线程化**,比如 Redis 主线程、Netty 单 Reactor。

举几个反例说明"看起来安全实际不安全":ConcurrentHashMap 的 get/put 安全,但复合操作还是要同步;HashMap 并发 put 甚至会死循环;SimpleDateFormat 是经典的非线程安全类。

这就是我对线程安全的理解。

---

#3 什么是CAS?存在什么问题? low java-concurrency

题目

什么是CAS?存在什么问题?

我的回答

CAS 其实是对线程安全问题的一种处理方式,它本身的处理方式指的是比较,然后交换。如果比较的过程中发现比较的对象发生了变化,就先不做处理,进行再一次比较,然后再去进行处理,然后确保修改修改的时候是符合自身预期的,然后来实现线程安全的保障。但是它本身存在一个所谓 ABA 的问题。我修改前这个目标是 A,但是中间的过程中有另一个线程把目标修改为了 B,而我修改的时候预期是把 A 修改成其他的,但是它已经是 B 了。而这中间又出现了一个线程,把这个 B 改成了 A,但是我修改的时候看到的还是 A。实际上本身预期是我从 A 改到另一个值,但实际上我改的是有一个人把 A 改成了 B,另一个人把 B 又改成了 A。我修改的实际内容并不是我原本预期的,这就是 CAS 中最常见的,也是最核心的问题,ABA 问题。它处理的时候可以使用版本号,就不存在 ABA就是你每次只能往下更改,不会说 A 又能改回去的情况。

点评

评分:⭐⭐⭐☆☆
优点:准确抓住 CAS = Compare And Swap 的本质;ABA 问题描述清晰(A→B→A 全过程);正确给出版本号解决方案
不足:只覆盖 ABA 一个痛点,遗漏自旋开销、单变量原子性;未说明 CAS 在 OS 层的实现(cmpxchg 硬件指令);未提 AtomicStampedReference 官方实现
补充:三要素 expected/actual/new;三大痛点 ABA、自旋开销、单变量原子性;AtomicStampedReference 是版本号的官方实现 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清 CAS 的三要素、OS 层面实现,以及三个常见痛点。

**知识点拆解**:
1. **底层机制**:CAS 全称 Compare And Swap,三个要素:
- `expected`(期望值):线程认为内存中的当前值
- `actual`(实际值):内存中真实的值
- `new`(新值):期望交换成的值
只有当 `actual == expected` 时才把内存改为 `new`,否则什么都不做(CAS 失败,通常自旋重试)
2. **OS/硬件层面**:靠 CPU 的原子指令实现,x86 上是 `cmpxchg`,单条指令完成比较+交换,不会被中断打断;Java 里通过 Unsafe 或 VarHandle 调用,是用户态的"无锁"原语
3. **三大痛点**:
- **ABA 问题**:A → B → A,中间被改过又改回来了,CAS 看不出差异;解决:AtomicStampedReference(值+版本号)、AtomicMarkableReference(值+boolean 标记)
- **自旋开销**:长时间 CAS 失败会一直占着 CPU,CPU 密集场景反而拖慢整体;解决:LongAdder 分段累加、退避策略
- **单变量原子性**:只能保证一个变量,复合操作(a++ 且 b--)仍要外部同步
4. **典型应用**:AtomicInteger/AtomicLong(计数器)、AtomicReference(引用计数)、ConcurrentHashMap 的 put(cell 级别的 CAS)

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 CAS 在硬件层的实现(cmpxchg)
- **广度**:能否系统覆盖 ABA、自旋、单变量三个问题
- **应用**:能否举出 AtomicXxx 和 ConcurrentHashMap 中的 CAS 用法

**延伸递进面试题**:
1. **ABA 问题在工程里真的遇到过吗?怎么发现的?**
- 解答要点:栈的无锁实现最经典,push 一个元素被两次弹出再压回去;用 AtomicStampedReference 解决
2. **CAS 和 synchronized 怎么选?**
- 解答要点:低竞争 + 临界区极短用 CAS;高竞争或临界区长用 synchronized(锁的线程阻塞让出 CPU)
3. **LongAdder 和 AtomicLong 的区别?**
- 解答要点:AtomicLong 单变量 CAS 竞争激烈;LongAdder 用 Cell 数组分段累加,读时再汇总,高并发写入性能更好

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,CAS 全称 Compare And Swap,是并发编程里最基础的无锁原子操作。

**它的三个要素是 expected、actual 和 new**:线程先读一下内存值,作为 expected;如果内存中的实际值 actual 等于 expected,就把内存改成 new;否则什么都不做,通常自旋重试。CAS 之所以能"无锁",是因为它依赖 CPU 的原子指令,x86 上是 cmpxchg,一条指令完成比较+交换,不会被中断打断,也不用进内核态拿锁。

**CAS 有三个常见的问题**:

**第一是 ABA 问题**。我先看到值是 A,想把它改成 C;但我准备执行 CAS 的这段时间里,另一个线程把 A 改成了 B,紧接着又把它改回了 A;这时候我的 CAS 看到还是 A,就成功了,但实际上这个"A"已经和当初的"A"不是同一个东西了,中间的业务语义全错了。解决方案是引入版本号,比如 Java 的 AtomicStampedReference 就是"值 + 版本号"配对,每次修改都让版本号递增,ABA 自然被识别出来。

**第二是自旋开销**。CAS 失败要重试,如果竞争激烈,线程就一直自旋占着 CPU,不像锁那样会阻塞让出。极端情况下 CPU 会被打满。工程上遇到这种场景常用 LongAdder,它把累加器拆成 Cell 数组,多线程各写各的分段,读的时候再汇总。

**第三是单变量原子性**。CAS 只能保证一个变量的原子,如果你要做 a++ 和 b-- 这种复合操作,CAS 管不住,还是得回到 synchronized 或者用复合类型的原子方法。

典型应用上,AtomicInteger、AtomicLong、AtomicReference 都是基于 CAS 的,ConcurrentHashMap 的 put 也是靠 cell 级别的 CAS 来保证并发安全。

这就是我对 CAS 的理解。

#4 volatile是如何保证可见性和有序性的? medium java-concurrency

题目

volatile是如何保证可见性和有序性的?

我的回答

volatile是 Java 的一个核心的内部自定义的关键字。它的语义就是保证可见性,内核其实是保证内核的可见性。然后它实现可见性的原理就是,它在被这个关键字标注的对象更新的时候,它是一定要从内存中拿最新的对象的,确保这个对象永远是拿到的是最新的,和内核中是一样的,不会出现线程上的问题,拿到对象一定是新的,更新后的。然后它保证有序性是它在修饰的时候会在对象前后加上它的读写屏障,确保这个对象不会被 Java 的优化器优化更新到不符合预期的位置。当然这个更新的位置本身是比较困难的,就是说更新的可能性比较低,但是是有这种可能性的。就是说在编译的执行的过程中,把一个两个相互可能它觉得不相干的,或者有优化可能性的对象的位置,会去进行调整。这时候可能会影响整个程序的逻辑,产生不可预期的成结果。Volatile 可以保证它的有序,不会被它重新编译更新位置。

点评

评分:⭐⭐⭐☆☆
优点:抓住两个核心语义(可见性+有序性);提到"读写屏障"关键点;理解指令重排可能破坏程序语义
不足:表述混乱;未说清可见性底层机制(MESI + lock 前缀指令 + 刷主存);未列举 4 种内存屏障;未提 i++ 经典反例;未说适用场景(DCL、状态标志位)
补充:volatile = 编译器禁止重排 + CPU 加 lock 前缀刷缓存;4 种屏障 StoreStore/StoreLoad/LoadLoad/LoadStore;三大特性:可见性、有序性、不保证原子性 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清 volatile 的两大特性底层机制(MESI + 内存屏障),以及"不保证原子性"的边界。

**知识点拆解**:
1. **可见性的两层机制**:
- **CPU 层**:写 volatile 变量时,Java 会生成 `lock` 前缀指令,触发 **MESI 协议**把本核 L1 缓存的 modified 状态写回主存,同时广播"无效"指令给其他核,让它们下次必须从主存取
- **JVM/编译器层**:写操作强制刷写、读操作强制 reload,禁止指令和数据的缓存优化
2. **有序性的内存屏障**(JMM 定义 4 种):
- StoreStore:volatile 写之前,把之前所有普通变量都刷出去
- **StoreLoad**:volatile 写之后(最贵的一种),确保写之前的写都对其他核可见、以及写之后的读一定拿到新值
- LoadLoad:volatile 读之后,读后续操作不能提前到读之前
- LoadStore:volatile 读之前,确保读的读不会重排到 store 之后
3. **不保证原子性**:volatile 只保证每次单条读/写操作的可见性,`i++` 是三步(读、加、写),两个线程同时执行会丢更新——这是 volatile 最经典的反例
4. **典型应用场景**:
- **状态标志位**:`volatile boolean stop;` 主线程改了从线程立刻看到
- **DCL 单例**:`private volatile static Instance instance;`——不加 volatile 会出现"半初始化"实例被其他线程看到(指令重排:分配内存 → 赋引用 → 初始化构造器)
- **双端指针的 tail 引用**

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 MESI + 内存屏障 + lock 前缀这条链路
- **广度**:能否对比 volatile 和 CAS、synchronized
- **应用**:能否举出 DCL、状态标志位等真实用例

**延伸递进面试题**:
1. **volatile 能保证原子性吗?为什么?**
- 解答要点:不能,i++ 是三步;volatile 只保证单条读/写的可见性,不保证读-改-写的互斥
2. **有了 CAS 为啥还需要 volatile?**
- 解答要点:CAS 保证原子操作但只作用于一个变量;volatile 保证可见性和有序性,是"读/写屏障";两者解决的是不同维度的问题,DCL 必须同时用
3. **DCL 单例为什么必须加 volatile?**
- 解答要点:new 指令会被重排成"分配内存→赋引用→初始化",不加 volatile 时其他线程可能看到"已赋引用但未初始化"的半构造实例

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,volatile 是 Java 最经典的并发关键字,它保证两件事:**可见性**和**有序性**,但**不保证原子性**。

**先说可见性**。现代 CPU 每个核都有自己的 L1/L2 缓存,线程改了共享变量可能只改了自己核的缓存,别的核看不到。volatile 通过两层机制解决:

第一层是 **CPU 层的 MESI 协议**。Java 在写 volatile 变量时会生成 `lock` 前缀的指令,这条指令会强制把本核 L1 的 modified 数据写回主存,同时广播"无效"命令给其他核,让它们下次读必须从主存重新加载。

第二层是 **JVM/编译器层**。volatile 写会禁止 store-store 重排、volatile 读会禁止 load-load 和 load-store 重排。

**再说有序性**。JVM 层面定义了 4 种内存屏障:StoreStore、StoreLoad、LoadLoad、LoadStore。volatile 写之前是 StoreStore、写之后是 StoreLoad(也是最贵的屏障),volatile 读之后是 LoadLoad 和 LoadStore。这些屏障本质上是禁止 JIT 优化器把指令重排到破坏语义的位置。

**关键的反例是 i++ 不安全**。i++ 其实是三步:读 i、i+1、写回。volatile 只能保证每步的可见性,没法保证三步作为一个整体互斥,两个线程并发执行还是会丢更新。所以要用 AtomicInteger,或者加锁。

**典型应用场景**有几个:

第一个是**状态标志位**,比如 `volatile boolean stop;`,主线程改了从线程能立刻看到。

第二个是**双重检查锁单例**,`private volatile static Instance instance;`。这里必须加 volatile,因为 `new` 指令会被 JVM 重排成"分配内存→赋引用→初始化构造器",不加 volatile 时,另一个线程可能看到"已经赋引用但还没初始化"的半构造实例,直接用就 NPE。

第三个是**双端队列的 tail 引用**,比如 JDK 里 ArrayBlockingQueue 的 last 字段也是 volatile。

总结:volatile 用 MESI + 内存屏障保证可见性和有序性,但不保证原子性,所以适合单变量读写场景,复杂复合操作还是要用 synchronized 或 CAS。

#5 如何理解AQS? medium java-concurrency

题目

如何理解AQS?

我的回答

AQS 是 Java 中对于并发编程处理的一系列的工具的整合。核心是锁、阻塞队列以及相应的对于并发相关处理的一系列工具,形成的一个包中内容; 提供了可以更为便捷跟主动控制的一系列锁的操作,以及对多线程的相应支持,包括读写锁、阻塞队列、信号量等一系列的工具。然后最终实现对于整个并发编程方面的支撑。在 JDK1.8 之前,由于 JVM 自己定义的 synchronized 并不是很好用,所以有个大佬写了这套东西。在 JDK1.8 之前并发编程方面的处理,很多其实都是使用 AQS 相关的 API 去做处理的。

点评

评分:⭐⭐☆☆☆
优点:知道 AQS 是并发工具的整合基座;举了 ReentrantLock、读写锁、信号量等实现类;有历史感
不足:未说出全称 AbstractQueuedSynchronizer;缺失四大核心机制(state 变量、CLH 队列、独占/共享模式、模板方法);阻塞队列属于用 AQS 实现的组件不是 AQS 本身;对 synchronized 演进史的描述不准确
补充:AQS = state + CLH 双向队列 + 独占/共享两种模式;主要子类 ReentrantLock/ReentrantReadWriteLock/Semaphore/CountDownLatch/CyclicBarrier ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清 AQS 的全称、state 变量、CLH 队列、独占/共享两种模式,以及模板方法的设计。

**知识点拆解**:
1. **全称与定位**:AQS = AbstractQueuedSynchronizer(抽象队列同步器),位于 `java.util.concurrent.locks` 包,是 JUC 并发工具的基座。几乎所有同步器(ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock)都直接或间接继承它
2. **两大核心数据结构**:
- **`volatile int state`**:同步状态,记录锁的"当前情况"。ReentrantLock 用 state 表示重入次数、Semaphore 用 state 表示剩余许可数、CountDownLatch 用 state 表示倒计时
- **CLH 双向队列**(Treiber 队列变种):获取锁失败的线程会被封装成 Node 节点,加入尾链表等待。head 是头节点(已获取锁的线程),tail 是尾指针
3. **两种工作模式**:
- **独占模式(Exclusive)**:一次只有一个线程能持有锁,如 ReentrantLock、ReentrantReadWriteLock 的写锁。子类实现 `tryAcquire` / `tryRelease`
- **共享模式(Shared)**:多个线程可同时持有,如 ReadLock、Semaphore、CountDownLatch。子类实现 `tryAcquireShared` / `tryReleaseShared`
4. **模板方法模式**:父类 AQS 负责排队、唤醒、调度;子类只实现"拿到锁/释放锁"的具体判断逻辑。这是 AQS 设计得优雅的关键——把"如何判断"和"如何调度"解耦
5. **与 synchronized 的区别**:
- synchronized 由 JVM 内置实现,不可中断、不可超时、公平/非公平由 JVM 决定
- AQS 是用户态实现,支持 `lockInterruptibly`、`tryLock(timeout)`、`isFair` 显式控制
- 高竞争下 AQS 通常更快(可优化调度),低竞争下两者差异不大

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 state 变量 + CLH 队列 + 独占/共享两条线
- **广度**:能否列举主要子类并说出它们的 state 含义
- **应用**:能否举出工程中用 ReentrantLock 而非 synchronized 的场景

**延伸递进面试题**:
1. **AQS 为什么用双向链表而不是单向?**
- 解答要点:释放锁时要唤醒前驱的后继节点(独占)或所有后继(共享),单向链表无法高效遍历;且线程中断场景需要反向唤醒
2. **ReentrantLock 怎么实现可重入?**
- 解答要点:state 记录重入次数,同一线程再次获取时 state++,释放时 state--,直到 0 才真正释放
3. **公平锁 vs 非公平锁的区别?**
- 解答要点:公平锁 tryAcquire 时先看 hasQueuedPredecessors(队列里有没有更老的线程);非公平锁直接 CAS 抢,抢到就是它。非公平锁吞吐更高,公平锁避免饥饿

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,AQS 全称是 **AbstractQueuedSynchronizer,抽象队列同步器**,位于 `java.util.concurrent.locks` 包下,是 JUC 整个并发包的基座。ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock 都是直接或间接继承它。

**AQS 的核心有两个**:

第一是 **`volatile int state` 状态变量**。它记录了同步器的"当前情况",但具体含义由子类决定:ReentrantLock 用 state 表示重入次数,Semaphore 用 state 表示剩余许可数,CountDownLatch 用 state 表示倒计时。state 用 volatile 修饰,保证可见性。

第二是 **CLH 双向队列**。当线程获取锁失败时,会被封装成一个 Node 节点挂到队列尾部,进入等待。队列头节点代表当前持有锁的线程,队列尾指针是 tail。释放锁时要遍历队列唤醒后继线程。

**AQS 有两种工作模式**:

**独占模式**:一次只能一个线程拿到锁,典型例子是 ReentrantLock 和 ReadWriteLock 的写锁。子类实现 `tryAcquire` 和 `tryRelease` 两个方法,告诉父类"我拿到了吗"和"我释放了"。

**共享模式**:多个线程可以同时持有,典型例子是 ReadLock、Semaphore、CountDownLatch。子类实现 `tryAcquireShared` 和 `tryReleaseShared`。

AQS 的设计思想是 **模板方法模式**:父类负责排队、唤醒、调度这些通用逻辑;子类只需要关心"怎么判断能不能拿到锁"和"怎么释放"。这样把"调度"和"策略"解耦,扩展性极强。

**和 synchronized 的对比**:synchronized 是 JVM 内置的,不可中断、不可超时、公平性由 JVM 决定;AQS 是用户态实现,支持 `lockInterruptibly`、`tryLock(timeout)`、`isFair` 显式控制,所以业务上需要这些能力时就用 AQS 的 ReentrantLock。

典型用法上,需要可中断、可超时、读写分离、信号量限流、倒计时这类场景,基本都是 AQS 的子类。

这就是我对 AQS 的理解。

---

#6 synchronized是怎么实现的? medium java-concurrency

题目

synchronized是怎么实现的?

我的回答

Synchronized 的本质上就是在对象头上做限定,定义当前这个对象是否被加了锁。当线程进行访问的时候,确认这个线程持有的线程是什么。啊,如果是当前线程就可以正常访问,如果不是当前线程,就放入队列等待。核心核心原理其实就这个,然后在 1.8 之后做了一系列的优化,包括锁升级,然后锁的力度的调整,这些东西。它核心的就是我刚才说的,在对象头中记录锁的信息、线程的信息,确保是当前线程去访问这个对象,而不是其他线程进入的时候,也能访问到这个对象。然后当其他线程访问的时候做一个阻塞。核心原理就是这个。哦,JDK1.8 的时候做了一个优化升级,核心的作用就是优化线程内的性能,不是一上来就用极重的锁持有,最开始使用的就是轻向锁。你一个线程来了,保持有一个记录下来是你这个线程,但是并不加锁。而后如果多个线程进来了,就开始升级变成轻量级锁,做简单的重试等待,也不是直接加锁,类似于 CAS 的操作。然后再往后,更多的线程并发产生了,才直接上锁,做重量级的锁持有限定。确保一个线程只被等待,其他线程只确保一个线程在使用,其他线程要等待做阻塞。

点评

评分:⭐⭐⭐☆☆
优点:抓到核心点——对象头(Mark Word)记录锁信息;提到锁升级、CAS、性能优化
不足:锁升级顺序描述有误(跳过了偏向锁);未提 Mark Word、Monitor、monitorenter/monitorexit 术语;未说三种使用场景;未提 JDK 15/18 偏向锁废弃新知识点
补充:正确路径:无锁→偏向锁→轻量级锁→重量级锁;对象头结构 Mark Word+Klass Pointer;JDK 15 默认关闭偏向锁,JDK 18 彻底移除 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清对象头 + Mark Word + 锁升级四条路径 + 字节码层实现。

**知识点拆解**:
1. **对象头结构**(HotSpot 64 位):
- **Mark Word(8 字节)**:存储哈希码、GC 分代年龄、**锁标志位(2 位)**、锁指针/偏向线程 ID
- **Klass Pointer(4-8 字节)**:指向方法区的方法元数据
- 数组对象多一个 length 字段
2. **锁的三种使用场景**:
- 修饰**实例方法**:锁 `this` 对象
- 修饰**静态方法**:锁 Class 对象
- 修饰**代码块**:锁指定的对象
3. **锁升级四条路径**(JDK 6 引入,JDK 15 后偏向锁被默认关闭,JDK 18 彻底移除):
- **无锁 → 偏向锁**:只有一个线程访问时,Mark Word 记录线程 ID,之后同线程访问直接放行,不加锁、不 CAS。适合"一次获取、多次访问"的低竞争场景
- **偏向锁 → 轻量级锁**:第二个线程到达时撤销偏向锁,升级为轻量级锁;用 CAS 自旋尝试获取,不进入 OS 阻塞。适合"锁持有时间极短"的中竞争场景
- **轻量级锁 → 重量级锁**:CAS 自旋失败达到阈值(默认 10 次,可 `-XX:PreBlockSpin` 调),膨胀为重量级锁,线程挂起进入 OS 内核调度
- **重量级锁不可降级**:JVM 保证锁只有升级路径,没有降级路径
4. **字节码层**:
- synchronized 方法:类文件常量池里的方法有 `ACC_SYNCHRONIZED` 标志
- synchronized 代码块:使用 `monitorenter` / `monitorexit` / `athrow` 三条指令
5. **Monitor(管程)**:重量级锁的真正实现,每个对象都有一个隐含的 Monitor 对象,包含 owner、waitSet、entryList 等字段

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清对象头 Mark Word 的锁标志位 + 四条升级路径
- **广度**:能否对比 synchronized 和 ReentrantLock
- **应用**:能否举出代码块 vs 方法、实例 vs 静态的选择场景

**延伸递进面试题**:
1. **synchronized 的锁能降级吗?**
- 解答要点:不能。JVM 规定锁只有升级没有降级,避免锁状态频繁切换的开销
2. **偏向锁为什么被废弃了?**
- 解答要点:JDK 15 JEP 374 默认关闭,JDK 18 移除。原因是:偏向锁撤销代价高(需要 STW),现代 GC 让偏向锁优化不划算;STW 时代结束偏向锁失去了主要场景
3. **synchronized 和 ReentrantLock 怎么选?**
- 解答要点:低竞争用 synchronized(JVM 内置、零开销、代码简洁);高竞争、需要中断/超时/公平锁/多条件变量用 ReentrantLock

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,synchronized 的实现我分三个层次来说:**字节码层、对象头层、锁升级层**。

**字节码层**,synchronized 有三种使用方式:修饰实例方法锁 `this`、修饰静态方法锁 Class 对象、修饰代码块锁指定对象。在字节码层面,synchronized 方法有 `ACC_SYNCHRONIZED` 标志位,synchronized 代码块使用 `monitorenter` 和 `monitorexit` 两条指令(异常路径下还有 `athrow` 保证锁释放)。

**对象头层**是核心。HotSpot 64 位对象的对象头主要由两部分组成:**Mark Word**(8 字节,存哈希码、GC 分代年龄、锁标志位、锁指针/线程 ID)和 **Klass Pointer**(4-8 字节,指向方法区的元数据)。Mark Word 里的锁标志位(最后 2 位)直接决定了对象当前是什么锁状态。

**锁升级层**是 synchronized 性能优化的关键,从低到高四条路径:

第一条是 **无锁 → 偏向锁**。当只有一个线程访问时,Mark Word 会记录这个线程的 ID,之后同线程访问直接放行,不加锁也不 CAS。这是最轻量化的状态,适合"一次获取、多次访问"的低竞争场景。不过要注意 **JDK 15 之后偏向锁被默认关闭,JDK 18 已经彻底移除**了,因为偏向锁撤销代价高、需要 STW,在现代 GC 下不划算。

第二条是 **偏向锁 → 轻量级锁**。当第二个线程到达时撤销偏向锁,升级为轻量级锁;线程用 CAS 自旋尝试获取,不进入 OS 阻塞。适合"锁持有时间极短"的中竞争场景。

第三条是 **轻量级锁 → 重量级锁**。当 CAS 自旋失败达到阈值(默认 10 次,可用 `-XX:PreBlockSpin` 调),就膨胀为重量级锁,线程挂起进入 OS 内核调度,通过 Monitor 的 entryList 排队。重量级锁是不可降级的——JVM 保证锁只有升级没有降级路径。

重量级锁的真正实现是 **Monitor(管程)**,每个对象都隐含一个 Monitor 对象,包含 owner、entryList、waitSet 等字段,wait() 方法就是把线程挂到 waitSet 上。

**和 ReentrantLock 的对比**:低竞争场景 synchronized 更合适(JVM 内置、零开销、代码简洁),高竞争或需要中断/超时/公平锁/多条件变量时用 ReentrantLock。

这就是我对 synchronized 实现的理解。

#7 有了CAS为啥还需要volatile? medium java-concurrency

题目

有了CAS为啥还需要volatile?

我的回答

CAS 跟 Volatile 不是一个层面的东西,它们处理的事情也不是一个。Volatile 是处理可见性和避免指令重排的,而 CAS 它是一个并发方面的处理方式,它的核心是比较交换,并且在 Java 中,CAS 其实是基于 Volatile 去实现的,二者存在依附关系。因为 CAS 进行比较的时候,需要通过 volatile 去保证可见性,确保比较的对象是当前的值,来最终实现 CAS 比较交换的功能。CAS 跟 Voltaio 相互之间并不是竞争关系,是依附关系。

点评

评分:⭐⭐⭐⭐☆
优点:抓住核心洞察——依附而非竞争关系;正确说明 CAS 基于 volatile 实现;简洁准确
不足:未举 AtomicInteger 源码例子;未展开 DCL 单例为什么两者都要;未说"互补关系"(volatile 管可见性,CAS 管原子性)
补充:依附关系链 volatile 屏障→MESI→CAS 读真实值;互补关系 volatile 管单操作可见、CAS 管复合操作原子;AtomicInteger.value 就是 volatile ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清"依附 + 互补"两条关系链,而不是把二者对立。

**知识点拆解**:
1. **底层依附关系**:CAS 在 Java 里就是**基于 volatile 实现**的。
- `AtomicInteger` 的核心字段是 `private volatile int value;`
- CAS 依赖 volatile 提供的内存屏障,才能保证"比较"时读到的是主存的最新值;否则 JIT 会把值缓存在寄存器里,永远拿不到更新
- 底层链条:volatile 写加 `lock` 前缀指令 → CPU 触发 MESI 失效广播 → 其他核下次必须读主存 → CAS 才能读到真实值
2. **互补关系(不是替代关系)**:
- **volatile** 保证:**单条读/写操作**的可见性 + 有序性
- **CAS** 保证:**读-改-写复合操作**的原子性
- 二者一个管"看"一个管"改",谁也替代不了谁
3. **典型反例**:
- `i++` 用 volatile 不行(读-改-写三步,volatile 只保证每步的可见性)
- `i++` 用 CAS 就行(compareAndSet 把三步打包成一个原子操作)
- 反过来:`AtomicInteger.get()` 不加 CAS 也能正确读,因为它就是 volatile 读
4. **DCL 单例为什么两者都要**:
```java
private volatile static Singleton s; // volatile 防止指令重排
public static Singleton get() {
if (s == null) { // volatile 读,保证可见性
synchronized (Singleton.class) {
if (s == null) { // 二次检查
s = new Singleton(); // CAS 或 volatile 写
}
}
}
return s;
}
```
- synchronized/CAS 保证"判断+赋值"原子性
- volatile 防止 `new` 被重排成"分配内存→赋引用→初始化"导致其他线程看到半构造实例

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 CAS 依赖 volatile 内存屏障这条链路
- **广度**:能否说清"互补而非替代"
- **应用**:能否举 DCL、AtomicXxx 源码等真实用例

**延伸递进面试题**:
1. **i++ 能不能用 volatile?为什么?**
- 解答要点:不能。i++ 是读-改-写三步,volatile 只保证单条操作的可见性,不保证三步互斥
2. **CAS 失败后为什么会自旋?自旋期间能看到最新值吗?**
- 解答要点:能看到,因为每次重试都是 volatile 读,强制从主存拿最新值
3. **Unsafe 和 VarHandle 的 CAS 实现有什么区别?**
- 解答要点:Unsafe 是老 API(JDK 9 后私有化)、VarHandle 是新 API(JDK 9 引入、JDK 17 推荐),底层都走 cmpxchg,但 VarHandle 支持 lambda、可组合、更受支持

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,CAS 和 volatile 不是竞争关系,而是**依附 + 互补**的关系,这是我觉得这道题的关键点。

**先说依附关系**。在 Java 里,CAS 其实**就是基于 volatile 实现的**。最典型的例子就是 AtomicInteger,它里面的核心字段是 `private volatile int value`,没有这个 volatile,CAS 根本没法工作。为什么?因为 CAS 要做的是"比较并交换",这个比较动作必须先读到主存的真实值。如果没有 volatile 的内存屏障,JIT 会把值缓存在寄存器里,永远拿不到最新值,CAS 的比较就失去了意义。所以从底层链条看:volatile 写时加 `lock` 前缀指令 → CPU 触发 MESI 协议让其他核缓存失效 → 下次读必须从主存取 → CAS 才能读到真实值。CAS 是 volatile 的"消费者"。

**再说互补关系**。volatile 保证的是**单条读/写操作**的可见性和有序性;CAS 保证的是**读-改-写复合操作**的原子性。一个管"看",一个管"改",谁也替代不了谁。

**最经典的反例是 i++**。用 volatile 修饰 i,i++ 仍然不安全,因为 i++ 是三步:读 i、i+1、写回。volatile 只能保证每步操作的可见性,没法保证三步作为一个整体的互斥。这里必须用 AtomicInteger,它的 `incrementAndGet` 方法内部就是用 CAS 把这三步打包成一个原子操作。反过来,如果只是想读一个值,`AtomicInteger.get()` 其实就是个 volatile 读,不需要 CAS。

**还有一个典型场景是双重检查锁单例**,这里 volatile 和 CAS(或 synchronized)必须同时用:

```java
private volatile static Singleton s;
public static Singleton get() {
if (s == null) {
synchronized (Singleton.class) {
if (s == null) {
s = new Singleton();
}
}
}
return s;
}
```

synchronized 保证"判断+赋值"的原子性,但 `new` 指令会被 JVM 重排成"分配内存→赋引用→初始化构造器",如果不加 volatile,另一个线程可能看到"已经赋引用但还没初始化"的半构造实例,直接用就 NPE。所以 volatile 在这里不是可选的,是必须的。

总结一句话:**volatile 是 CAS 的底层基石,CAS 是 volatile 的能力延伸,两者是配合关系不是替代关系**。

这就是我对这道题的理解。

#8 synchronized是如何保证原子性、可见性、有序性的? high java-concurrency

题目

synchronized是如何保证原子性、可见性、有序性的?

我的回答

Xenon 是通过锁去保证这一整个的线程并发的处理的。通过锁来保证原子性,内核级的锁,它在指令中添加了锁的指令,去确保锁内只有同一个线程在执行。可见性方面,由于它通过锁控制,其他线程是不会进入的,其他线程会被阻塞,放到阻塞队列中,并不存在可见性问题,当前线程可见当前内容。有序性方面,它也是通过它是通过公平锁来实现的,顺序的去处理锁的线程。如果是这个有序性指的是指令重排,那么它也是通过锁的指令,在内核程序去保证锁指令重排的不发生。

点评

评分:⭐⭐☆☆☆
优点:抓到核心方向——synchronized 通过"锁"机制保证原子性;大致意识到可见性来自其他线程进不来
补充:原子性=Monitor 互斥、可见性=解锁刷内存+加锁 reload、有序性=JIT 禁止重排;synchronized 是非公平锁;volatile 只保证可见+有序,synchronized 三个都保证 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清"原子性 = 互斥"、"可见性 = 解锁刷内存"、"有序性 = 禁止指令重排"这三条底层机制,以及 happens-before 规则。

**知识点拆解**:
1. **原子性 ← Monitor 互斥机制**:
- 每个对象有一个隐含的 **Monitor(管程)**,包含 owner 指针
- 线程进入 synchronized 块时通过 CAS 尝试设置 Monitor 的 owner;成功后独占执行;其他线程进不来
- 重量级锁状态下线程通过 OS 阻塞挂起到 entryList;轻量级锁通过自旋 CAS 竞争
2. **可见性 ← 解锁刷内存 + 内存屏障**:
- **解锁时**:把工作内存中的修改**强制刷回主存**(StoreLoad 屏障)
- **加锁时**:**强制从主存 reload** 到工作内存,丢弃本地缓存
- 这就保证了"解锁 → 加锁"之间的修改对所有线程可见
3. **有序性 ← 禁止指令重排**:
- JIT 编译器被禁止把 synchronized 块**内部**的指令重排到块**外部**
- CPU 层面的 StoreLoad 屏障防止跨块重排
- 注意:这是**指令级**的有序性,不是"线程获取锁的顺序"
4. **happens-before 规则**(JMM 第 17 条):
- 对一个 synchronized 块的解锁,happens-before 后续对**同一把锁**的加锁
- 这条规则是"可见性 + 有序性"能被 JIT 优化的契约基础
5. **三大特性对比 volatile**:
- volatile:可见性 ✓、有序性 ✓、原子性 ✗(i++ 三步不能打包)
- synchronized:可见性 ✓、有序性 ✓、原子性 ✓(互斥机制保证复合操作整体执行)
- 所以 synchronized 是 volatile 的"超集",但开销更大

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 Monitor + 内存屏障 + JIT 重排禁止这条链路
- **广度**:能否区分 synchronized 和 volatile 各自覆盖的特性
- **应用**:能否举出代码里为什么 synchronized 块比 volatile 更强的场景

**延伸递进面试题**:
1. **synchronized 能保证原子性,为什么不用 volatile 也能保证可见性和有序性?**
- 解答要点:volatile 加内存屏障保证可见+有序,但读-改-写三步仍会交错;synchronized 用互斥把三步包成一个原子块
2. **synchronized 是公平锁吗?为什么说是非公平锁?**
- 解答要点:非公平锁。JVM 内部不保证先到先得;多个线程竞争时随机调度。需要公平性时用 ReentrantLock(true)
3. **happens-before 有哪些传递性规则?**
- 解答要点:锁规则(释放→获取)、volatile 规则(写→读)、传递性、启动线程、传递性;是 JMM 保证并发正确性的最小契约

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,synchronized 同时保证三个特性:**原子性、可见性、有序性**,但每一条的底层机制是不同的,我分开说。

**先说原子性**。原子性的本质是"互斥"——同一时刻只能有一个线程在 synchronized 块里执行。它的底层是 **Monitor(管程)机制**。每个对象都隐含一个 Monitor 对象,里面有个 owner 指针记录当前的持锁线程。线程进入 synchronized 块时,先尝试用 CAS 把 owner 设成自己;成功了就独占执行,失败了就在队列里等。重量级锁状态下其他线程会挂起到 entryList 阻塞等待,轻量级锁下通过自旋 CAS 竞争。所以从"互斥"这个角度看,原子性其实是锁的基本性质——你进得去,别人就进不来,操作就整体执行。

**再说可见性**。可见性靠的是**解锁和加锁时强制刷内存**。具体规则是:

- **解锁时**:把工作内存里的修改**强制刷回主存**,并加 StoreLoad 内存屏障,确保之前的所有写操作都被刷出去
- **加锁时**:**丢弃本地工作内存**,强制从主存 reload 最新值

这样"解锁 → 加锁"之间的修改对其他线程一定可见。JMM 把它总结成一条规则:**对一个 synchronized 块的解锁,happens-before 后续对同一把锁的加锁**。这是 JMM 第 17 条规则,是并发编程可见性的契约基础。

**第三个是有序性**。这里的有序性不是指"线程获取锁的顺序",而是**指令重排**——JIT 编译器不会把 synchronized 块内部的指令重排到块外部去。CPU 层面的 StoreLoad 屏障也保证了跨块的指令顺序不会被破坏。这个很重要,比如经典的 DCL 单例如果不加 volatile,`new` 指令会被重排成"分配内存→赋引用→初始化构造器",不加锁的话其他线程可能看到半构造实例。synchronized 块里因为有序性保证,指令不会被这样重排。

**对比一下 volatile**:volatile 只保证可见性和有序性,但**不保证原子性**——i++ 是读-改-写三步,volatile 只能让每步都可见,但三步之间可能交错。synchronized 通过互斥机制把三步包成一个整体,所以三个特性都保证。所以可以说 synchronized 是 volatile 的"超集",但开销也更大。

**关于公平性**——这里要纠正一个常见误解:**synchronized 不是公平锁,它是非公平锁**。JVM 内部不保证先到先得,多个线程竞争时随机调度。如果需要公平性,必须用 `ReentrantLock(true)`。

这就是我对 synchronized 三大特性的理解。

---

#9 能不能说一说Netty的无锁化设计? high java

题目

能不能说一说Netty的无锁化设计?

我的回答

无锁化一般指的是无状态,多线程使用的时候进行无状态的处理,就不会存在锁的问题。Naty 本身是通过多路复用器去进行线程处理的,它如果不保持状态的话,它本身就是无锁化的,不需要做锁的考虑。

点评

评分:⭐☆☆☆☆
优点:提到多路复用器(方向对,术语应为 Reactor + NIO Selector/Epoll)
补充:无锁化 = 用 CAS/原子操作替代锁;Netty 通过"单线程独占 Channel"消除共享,自然无需锁;MPSC 队列 + ThreadLocal 池化是关键实现 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清 Netty 通过"单线程独占 Channel"和"MPSC 无锁队列 + ThreadLocal 池化"来消除锁竞争,而不是简单理解为"无状态"。

**知识点拆解**:
1. **先纠正概念**:
- **无锁化(lock-free)**:用 CAS、原子变量、MPSC 队列等**无锁数据结构**替代 synchronized/Lock
- **无状态**:代码不保留可变数据(如纯函数、单例工具类)
- Netty 的无锁化是**消除锁竞争**,不是"没有状态"——Channel、EventLoop、ByteBuf 都有大量状态,但它们通过"单线程独占"避免了跨线程共享,因此不需要锁
2. **Reactor 单线程模型(核心)**:
- **主 Reactor(BossGroup)**:负责 accept 连接,把新建的 Channel 注册到 Worker
- **子 Reactor(WorkerGroup)**:每个 Worker 是一个 EventLoop 线程,管理多个 Channel
- **一对一绑定**:每个 Channel 绑定到一个 EventLoop 线程,所有 IO 事件和业务回调都在**同一个线程**里顺序执行
- **关键收益**:单线程内不需要锁;跨线程调用要用 `channel.eventLoop().submit(task)` 把任务投递到 EventLoop 队列
3. **MPSC 无锁任务队列**:
- 多个线程可以 `submit` 任务到 EventLoop 队列(多生产者)
- EventLoop 单线程从队列消费(单消费者)
- 实现用 `MpscLinkedQueue`,基于 CAS 操作无锁完成入队出队
4. **PooledByteBufAllocator 线程局部化**:
- 每个 EventLoop 线程持有自己的 ThreadLocal Chunk 链表
- 分配、释放 ByteBuf 都在同一线程完成,不跨线程竞争
- 相比 JDK 原生 ByteBuffer,内存分配/回收更快、GC 压力更低
5. **Recycler 对象池**:
- 用 ThreadLocal 无锁回收频繁创建的对象(如 ByteBuf、ChannelHandlerContext)
- 避免 GC 压力,也避免多线程竞争
6. **零拷贝(Zero-Copy)**:
- 用 DirectByteBuffer 减少 JVM 堆到内核的内存拷贝
- 用 FileChannel.transferTo 直接在内核态搬运大文件
7. **对比 NIO**:
- NIO 是 Reactor 模式的简化版,需要业务代码自己处理线程安全
- Netty 通过封装 + 单线程独占,把"线程安全"变成了框架的内置能力

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 Reactor 单线程模型 + MPSC 队列 + ThreadLocal 池化这条链路
- **广度**:能否对比 NIO 原生实现
- **应用**:能否举出业务代码里的"跨 EventLoop 调用"场景

**延伸递进面试题**:
1. **业务代码里要修改 Channel 状态怎么办?**
- 解答要点:必须 `channel.eventLoop().submit(...)` 把任务投递到 Channel 绑定的 EventLoop 线程执行,避免跨线程访问
2. **Netty 为什么用 PooledByteBufAllocator 而不用 JDK 原生 ByteBuffer?**
- 解答要点:原生 ByteBuffer 每次 new 都堆外分配,GC 压力大;池化复用减少分配/回收,且线程局部化避免竞争
3. **Netty 的零拷贝体现在哪几个层面?**
- 解答要点:堆外内存、CompositeByteBuf 组合、FileChannel.transferTo 内核态搬运、SliceByteBuf 引用计数切片

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,我先纠正一个常见的误解:**Netty 的无锁化不是"没有状态"**,而是通过"单线程独占 + 无锁数据结构"来**消除锁竞争**。Channel、EventLoop、ByteBuf 都有大量状态,但它们被设计成不需要多线程访问,所以也就不需要锁。

**第一层是 Reactor 单线程模型**,这是 Netty 的核心。Netty 有一个主 Reactor(BossGroup)负责 accept 新连接,然后把 Channel 注册到子 Reactor(WorkerGroup)。每个 Worker 是一个 EventLoop 线程,管理多个 Channel。**关键点是每个 Channel 绑定到唯一一个 EventLoop 线程**——这个 Channel 的所有 IO 事件、所有业务回调,都在同一个线程里顺序执行。因为单线程内不需要互斥,自然就不需要锁。如果业务代码要从别的线程改 Channel 状态,必须用 `channel.eventLoop().submit(task)` 把任务投递过去,让 EventLoop 线程自己执行。

**第二层是 MPSC 无锁任务队列**。多个线程都可以 submit 任务到 EventLoop 的任务队列(多生产者),EventLoop 单线程从队列消费(单消费者)。Netty 用 `MpscLinkedQueue` 实现,基于 CAS 操作无锁完成入队出队,避免了 synchronized 队列的开销。

**第三层是 PooledByteBufAllocator 线程局部化**。每个 EventLoop 线程持有自己 ThreadLocal 的 Chunk 链表,分配和释放 ByteBuf 都在同一线程完成,不跨线程竞争。相比 JDK 原生 ByteBuffer,内存分配/回收更快、GC 压力更低。这里也用了 **Recycler 对象池**,通过 ThreadLocal 无锁回收频繁创建的对象。

**第四层是零拷贝**。用 DirectByteBuffer 避免 JVM 堆到内核的拷贝;用 FileChannel.transferTo 直接在内核态搬运大文件;用 CompositeByteBuf 组合避免内存复制;用 SliceByteBuf 引用计数切片。

**对比 NIO 原生实现**:NIO 只是给了 Reactor 的骨架,业务代码自己处理线程安全;Netty 通过封装 + 单线程独占,把"线程安全"变成了框架的内置能力,业务代码只需要写单线程逻辑,框架保证了并发正确性。

总结一句话:**Netty 的无锁化是"通过避免共享来实现",而不是"通过没有状态来实现"**。

这就是我对 Netty 无锁化设计的理解。

---

#10 String的设计,用到了哪些设计模式? high system-design

题目

String的设计,用到了哪些设计模式?

我的回答

String 用到的设计模式我没什么印象,那么我们来分析下 string 的特性来推导下,首先 string 的第一个特性是不可变,另外 string 还有常量的特性,创建出来但不可变队形会被放入常量空间进行复用;string 对象自身是通过 byte 数组或者 char 数组实现的,在不同的jdk 版本中;所有 string 对象都已一种模式的方法,那么是否算模板方法模式?

点评

评分:⭐☆☆☆☆
优点:主动推导分析(没有直接放弃),提到不可变、常量池、字节数组实现、JDK 版本差异
补充:三大模式——享元(常量池复用)、不可变(线程安全+哈希稳定)、代理(StringBuilder+JDK9 内部存储) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否识别出 String 里的**享元模式**(最核心)、**不可变模式**、**代理模式**,并理解它们的设计动机。

**知识点拆解**:
1. **享元模式(Flyweight)—— String 最经典的设计模式**:
- **核心思想**:把相同字符串对象共享给多个调用方,减少内存占用
- **实现机制**:**字符串常量池**(String Constant Pool),位于堆内存的方法区/堆中
- **入池规则**:
- 字面量字符串(`String s = "hello";`)自动入池
- 调用 `s.intern()` 手动入池
- `new String("hello")` 创建堆对象,不入池(除非调 intern)
- **效果**:`"hello" == "hello"` 为 true(引用相等),不同变量共享同一个对象
2. **不可变模式(Immutable Object Pattern)**:
- **设计要点**:
- `public final class String`:类不可继承
- `private final byte[] value`:字段不可变
- 无 setter:所有"修改"操作返回新对象(`concat`、`substring`、`toUpperCase`)
- 构造器防御性拷贝(`new String(String original)` 会复制底层数组)
- **设计动机**:
- **线程安全**:不可变对象天生线程安全
- **哈希缓存**:`hashCode()` 可缓存,HashMap key 性能更好
- **安全性**:可作 `System.getProperty` 返回值、可作方法参数不被外部修改
- **可缓存**:可以安全地共享引用
3. **代理模式(Proxy)**:
- **StringBuilder/Buffer 是 String 的可变代理**:频繁拼字符串用 StringBuilder,最终 `toString()` 转回不可变 String
- **JDK 9+ 的 `StringLatin1`/`StringUTF16`**:String 内部用这两个紧凑存储类作为代理,节省内存
- **`new String(byte[])`**:把外部数组的引用代理进 String,并做防御性拷贝
4. **为什么 String 要设计成不可变?**(延伸理解)
- 线程安全:并发场景无需同步
- 哈希稳定:可作 HashMap 的 key
- 安全性:不可被外部篡改
- 常量池可复用:相同内容共享一份
5. **String 常见陷阱**:
- `==` vs `equals`:常量池内 `==` 可能为 true,堆对象 `==` 为 false
- `new String("a")` 和 `"a"` 不是同一个对象(前者在堆,后者在常量池)
- `intern()` 在 JDK 7+ 之后常量池移到了堆中

> 按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清享元模式 + 常量池入池规则
- **广度**:能否覆盖不可变模式 + 代理模式
- **应用**:能否举出 String 不可变带来的工程收益

**延伸递进面试题**:
1. **`"a"` 和 `new String("a")` 是同一个对象吗?调 `intern()` 后呢?**
- 解答要点:字面量在常量池,`new` 在堆,引用不同;调 `intern()` 后如果常量池已有就返回常量池引用,否则把堆对象加入常量池
2. **String 不可变有什么好处和代价?**
- 解答要点:好处——线程安全、可缓存、可作 key;代价——每次修改要新建对象,频繁拼字符串性能差,用 StringBuilder
3. **String 内部 JDK 9+ 为什么改成 `byte[]` + coder 字段?**
- 解答要点:JDK 8 用 `char[]`(2 字节/字符),JDK 9 用 `byte[]` + `coder` 区分 Latin1(1 字节)和 UTF16(2 字节),Latin1 字符场景内存减半

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,String 的设计主要用到三个设计模式:**享元模式、不可变模式、代理模式**。

**第一个是享元模式**,这是 String 最经典的设计。享元的核心思想是"相同对象共享"。在 String 里的体现就是**字符串常量池**:字面量 `"hello"` 会自动放入常量池,第二次写 `"hello"` 直接复用同一个对象。效果就是 `"hello" == "hello"` 为 true,多个变量引用同一个对象,节省大量内存。除了字面量入池,还有 `s.intern()` 手动入池——`new String("hello").intern()` 会返回常量池里的引用。

**第二个是不可变模式(Immutable Object Pattern)**。String 是不可变对象,体现在:类是 `public final` 不能继承;底层 `private final byte[] value` 字段不可变;没有 setter,所有"修改"方法如 `concat`、`substring`、`toUpperCase` 都返回新对象;构造器还做防御性拷贝,防止外部修改内部数组。

**不可变带来四个好处**:
- **线程安全**:不可变对象天生线程安全,并发场景无需同步
- **哈希缓存**:`hashCode()` 可以缓存,作 HashMap 的 key 性能更好
- **安全性**:可作返回值、方法参数不被外部篡改
- **可缓存**:安全共享引用

代价是每次修改都要新建对象,所以频繁拼字符串要用 StringBuilder。

**第三个是代理模式**。String 的不可变性催生了两类代理:

第一类是 **StringBuilder/Buffer 作为可变代理**——频繁修改字符串的场景,先用 StringBuilder 拼接,最后 `toString()` 转回不可变 String。

第二类是 **JDK 9+ 的 `StringLatin1` 和 `StringUTF16`**——String 内部用这两个紧凑存储类作为代理,通过 `coder` 字段区分 Latin1(1 字节)和 UTF16(2 字节)。JDK 8 用 `char[]` 每个字符固定 2 字节,JDK 9 改成 `byte[]` + `coder`,Latin1 字符场景内存减半。

**还有一个常见陷阱**:`"a"` 在常量池,`new String("a")` 在堆,两者引用不同,`==` 为 false;但调 `intern()` 后,如果常量池已有相同内容,就返回常量池的引用,`==` 变 true。

总结一句话:**String 是"享元模式 + 不可变模式 + 代理模式"的典型组合**,既节省内存,又保证了线程安全和哈希稳定性。

这就是我对 String 设计的理解。

📚 查看全部面试记录