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

2026-10-17

Java 并发 —— 从 JMM/工具类到锁机制与无锁设计

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

#1 什么是happens-before原则? low java-concurrency

题目

什么是happens-before原则?

我的回答

heppends-before原则是JMM,或者说JIT中防止指令重排的设定原则,规定了什么情况下不能进行重排,什么内容要在什么内容之前,在内存中指令要如何排列; 对应的底层依赖是读取和写入的规则,以及内存屏障等指令做支持; 具体的规则比较多,只说几个内存屏障相关的,读读屏障,读写屏障,写写屏障,写读屏障

点评

优点:抓住"规定先后顺序、防止乱序"的方向,也提到底层靠内存屏障。
不足:把 happens-before 等同于"防重排+内存屏障"是概念错位——HB 是 JMM 的**语义偏序契约**,用来定义"可见性+有序性";内存屏障只是它的**实现手段**之一。没说出 HB 的**规则清单**(面试真正要的)。
补充:八大规则(程序顺序、监视器锁、volatile 写读、线程 start/join、传递性等);"A happens-before B" 的含义是 A 的结果对 B 可见。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清 HB 是"可见性与有序性的契约",并背出核心规则。

**知识点拆解**:
1. **本质**:HB 是 JMM 定义在操作之间的**偏序关系**。若 A hb B,则 A 的所有写对 B 可见——它回答的是"一个线程的写何时对另一个线程可见"。
2. **核心规则**:
- 程序顺序:同一线程内,前面的操作 hb 后面的操作
- 监视器锁:对锁的解锁 hb 后续对同一把锁的加锁
- volatile:对 volatile 变量的写 hb 后续对它的读
- 线程启动:`start()` hb 该线程内的所有操作
- 线程终止:线程内所有操作 hb 其他线程从 `join()` 返回
- 传递性:A hb B,B hb C ⇒ A hb C
3. **实现**:JVM 用内存屏障(LoadLoad/StoreStore/LoadStore/StoreLoad)落地 HB;volatile 插屏障、synchronized 靠 Monitor。

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

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

**考察方向**:
- **深度**:HB 的偏序性、与 as-if-serial 的关系
- **广度**:与内存屏障、volatile、synchronized 的关系
- **应用**:DCL 单例为什么必须给实例加 volatile

**延伸递进面试题**:
1. **volatile 的写/读建立什么 HB?**
- 解答要点:写 hb 后续读;保证可见性 + 有序性,不保证原子性
2. **为什么 DCL 单例要加 volatile?**
- 解答要点:HB 防止"引用赋值先于构造完成"被其他线程看到半构造对象
3. **happens-before 和 as-if-serial 的区别?**
- 解答要点:as-if-serial 是单线程内的重排约束;HB 是跨线程可见性契约

第三部分:面试者参考回答(三步法口语化)

**【一句话结论】**
面试官您好,happens-before 是 JMM 规定的一种偏序关系,用来保证"前面操作的结果对后面操作可见"。

**【三点展开】**
第一,它是**语义契约不是实现**——描述的是可见性与有序性,不是"禁止重排"本身;
第二,核心规则有程序顺序、监视器锁、volatile 写读、线程 start/join、以及传递性;
第三,底层靠**内存屏障**落地,volatile 插屏障、synchronized 靠 Monitor 的解锁-加锁规则。

**【一句边界】**
当然,它只管可见性和有序性,**不保证原子性**——i++ 依然线程不安全,该上锁还得上锁。

#2 CountDownLatch、CyclicBarrier、Semaphore区别? low java-concurrency

题目

CountDownLatch、CyclicBarrier、Semaphore区别?

我的回答

三者是AQS中共享模式下的三个不同实现; countDownlatch是用于单个主线程下执行计数递减使用,每个对象只能用一次,所以要设置在主方法内使用,用过归零后就需要重新创建了,比如对资源的调用控制; cyclicbarrier是用于进行线程启动控制,用于等待多个线程到达后再执行下一步操作,可以用来进行结果组合,多线程调用不同下游,然后等待所有下游结果返回后,进行对结果统一组合; semaphore是信号量,控制一次可以通过的线程数,超过的会被阻塞,可以用来做限流,一次只允许一定个数的线程通过执行

点评

优点:三者用途都讲对了——CDL 计数归零、CB 线程到齐、Semaphore 控制通过数量;CDL/Semaphore 属于 AQS 共享模式也对。
不足:**"三者都是 AQS 共享模式"错**——CyclicBarrier 是 `ReentrantLock + Condition` 实现的,不走 AQS。也没点出"一次性 vs 可复用"这个核心对比。
补充:CDL 一次性、CB 可循环复用(有 reset/barrierAction);CDL 等的是"事件",CB 等的是"彼此",Semaphore 控制的是"并发许可数"。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:三者的语义差异 + 底层实现方式。

**知识点拆解**:
1. **CountDownLatch**:一次性的"等待计数归零"。AQS 共享模式,state = 计数;`await()` 阻塞、`countDown()` 减到 0 唤醒全部等待线程。
2. **CyclicBarrier**:可循环的栅栏,N 个线程互相等待到齐再一起放行。**ReentrantLock + Condition 实现**(不是 AQS),可传 barrierAction、可 `reset()` 复用。
3. **Semaphore**:许可信号量,`acquire/release` 控制同时通过的线程数。AQS 共享模式,state = 剩余许可。
4. **一句话区分**:CDL 等"事件"、一次性;CB 等"彼此"、可复用;Semaphore 控"数量"。

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

**考察方向**:
- **深度**:AQS 共享模式的传播唤醒(`tryAcquireShared` + `doReleaseShared`)
- **广度**:与 Phaser 对比;CB reset 的坑
- **应用**:CDL 做并行汇总、CB 做分阶段并行、Semaphore 做限流/连接池

**延伸递进面试题**:
1. **CyclicBarrier 为什么能复用,CDL 为什么不能?**
- 解答要点:CB 靠 ReentrantLock+Condition 可重置代(generation);CDL 的 state 归零即终结
2. **Semaphore 能替代限流器吗?和令牌桶区别?**
- 解答要点:Semaphore 控"并发数",令牌桶控"速率";瞬时突发语义不同
3. **AQS 共享模式如何唤醒多个线程?**
- 解答要点:`setHeadAndPropagate` 向后传播,连续唤醒共享节点

第三部分:面试者参考回答(三步法口语化)

**【一句话结论】**
面试官您好,三者都是并发协调工具,但语义和实现都不同。

**【三点展开】**
第一,**CountDownLatch** 是一次性的"等待事件",主线程 `await` 等计数归零,底层是 AQS 共享模式;
第二,**CyclicBarrier** 是可循环的"互相等待到齐",底层是 `ReentrantLock + Condition`——注意它**不走 AQS**;
第三,**Semaphore** 是许可信号量,控制同时通过的线程数,底层是 AQS 共享模式,常用来限流。

**【一句边界】**
当然,CyclicBarrier 的 `reset()` 会打断已等待的线程抛 `BrokenBarrierException`,生产环境要慎用 reset。

#3 什么是线程池,如何实现的? low java-concurrency

题目

什么是线程池,如何实现的?

我的回答

线程池是池化资源提高利用率的一个工具,核心功能就是将创建出来的线程进行复用,而不是每次创建新的线程; 线程池通过配置核心线程数,阻塞队列,最大线程数,拒绝策略,线程池工程,非核心线程保留时间和对应时间单位; 当主线程从线程池中申请新线程时,先判定核心线程是否都已经被占用,如果没占用完就用一个核心线程执行任务,如果已经占完没有空闲的核心线程,那么就判定阻塞队列是否已经满了,如果没有满,就把任务先放到阻塞队列中,如果阻塞队列已经满了,就判定当前线程池中的线程数量是否已经达到最大线程数,如果没有,则创建一个新的线程去执行,如果活跃的线程数量已经达到了最大线程数量,那么就要执行拒绝策略了; 默认的拒绝策略有,吞没,拒绝了没什么反馈,直接就不执行了; 抛出异常,反馈没有办法正常执行了; 开启额外新的线程去进行处理; 使用当前主线程进行处理; 还可以自定义拒绝策略,比如出现问题后,发送告警,或者发送mq消息等后续异步执行。具体使用什么需要根据业务形态去考虑,比如业务侧会做处理,对异常进行把控,那么就用抛出异常,业务侧自己考虑处理方案

点评

优点:相比首次(2026-09-20,3/5)明显进步——7 大核心参数基本说全(`corePoolSize/maximumPoolSize/workQueue/handler/threadFactory/keepAliveTime/unit`)、执行流程(core → 队列 → max → 拒绝)顺序完全正确、拒绝策略能结合业务举例。
不足:漏 `threadFactory` 的**命名作用**(排查必备)、WorkQueue 类型(有界/无界/`SynchronousQueue`)、`Executors` 禁用原因、监控指标。
补充:参数估算(CPU 密集 N_cpu×1、IO 密集 N_cpu×2);有界队列防 OOM;`CallerRunsPolicy` 天然限流。 > ℹ️ 完整讲解见上方首次记录(2026-09-20)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 ThreadPoolExecutor 的参数体系、执行流程、工程选型的全链路掌握。

**知识点拆解**:

1. **池化思想**:线程创建成本(内核态切换、栈内存、GC 压力);池化收益 = 复用 + 限流 + 隔离
2. **7 大核心参数**:corePoolSize / maximumPoolSize / keepAliveTime / unit / workQueue / threadFactory / handler
3. **执行流程**(三步判断):core → 队列未满 → max → 拒绝策略;队列优先级高于 max 扩容
4. **4 种拒绝策略**:AbortPolicy(默认抛异常)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢队列头)、CallerRunsPolicy(调用者执行,天然限流)
5. **4 种队列**:ArrayBlockingQueue(有界)、LinkedBlockingQueue(可无界 OOM 风险)、SynchronousQueue(不存储)、PriorityBlockingQueue(优先级)
6. **5 态**:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED
7. **Executors 禁用**:FixedThreadPool/SingleThreadExecutor 用无界队列 OOM;CachedThreadPool 无 max 线程暴涨;ScheduledThreadPool 无界队列
8. **参数估算**:CPU 密集 N_cpu×1;IO 密集 N_cpu×2 或 N_cpu×(1+wait/compute)
9. **监控**:JMX MBean 暴露 queueSize/activeCount/completedTaskCount/rejectedCount

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

**考察方向**:
- 深度:execute() 源码执行路径,为什么队列优先级高于 max?
- 广度:与 ForkJoinPool / 虚拟线程对比
- 应用:线程暴涨排查、队列积压排查、动态调优

**延伸递进面试题**:

1. **ThreadPoolExecutor.execute 的源码流程?**
- 解答要点:三关判断(core → queue → max),CAS 修改 workerCount,addWorker 创建线程
2. **为什么队列满了才扩非核心线程?**
- 解答要点:避免频繁创建销毁线程,队列作为缓冲池;代价是延迟升高
3. **线程暴涨如何排查?**
- 解答要点:jstack 抓栈 → 看线程名 → 定位提交方 → 看 queue 是否无界
4. **ForkJoinPool 和 ThreadPoolExecutor 区别?**
- 解答要点:ForkJoin 用工作窃取,无界任务队列,适合并行任务;TP 适合独立任务
5. **JDK 21 虚拟线程能替代线程池吗?**
- 解答要点:虚拟线程 M:N 调度,阻塞时释放载体线程;CPU 密集仍需池;不适合 synchronized 大量竞争
6. **动态线程池如何落地?**
- 解答要点:阿里 Hippo4j / 美团动态线程池;配置中心推送 + JMX 回调 + 告警

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

面试官您好,线程池本质是池化思想,核心目标是复用线程 + 限流 + 隔离。

参数方面有 7 个:corePoolSize 常驻线程数、maximumPoolSize 最大线程数、keepAliveTime 非核心线程空闲回收时间、unit 时间单位、workQueue 阻塞队列、threadFactory 线程工厂(一定要给线程命名,方便排查)、handler 拒绝策略。

执行流程是三步判断:任务进来先看当前线程数是否小于 core,是就建核心线程;否则看队列是否满,不满入队;否则看是否小于 max,是就建非核心线程;都不行触发拒绝策略。注意队列优先级高于 max 扩容,这是很多人讲错的地方。

拒绝策略 4 种内置:AbortPolicy 抛异常(默认)、DiscardPolicy 静默丢弃、DiscardOldestPolicy 丢队列头重试、CallerRunsPolicy 调用者自己执行——这个我比较常用,天然限流让上游感受慢。

队列选择我一般看场景:CPU 密集用 SynchronousQueue 或无队列,max 拉高;IO 密集用 ArrayBlockingQueue 有界队列控内存;任务有优先级用 PriorityBlockingQueue。

我们项目里禁用 Executors,因为它内部用无界队列或无上限线程,容易 OOM。参数估算用 N_cpu × 2(IO 密集)做初值,再压测调优。动态调优接了配置中心,运行时改 corePoolSize / maximumPoolSize,配 JMX 监控 activeCount、queueSize、rejectedCount 告警。

我们踩过的坑是线程暴涨:某个下游变慢,队列积压,线程飙到几千,最后 JVM OOM。定位靠 threadFactory 命名的线程名 + jstack 抓栈。之后改成有界队列 + CallerRunsPolicy,队列满后直接压回上游,问题就解决了。

---

#4 创建线程有几种方式? medium java-concurrency

题目

创建线程有几种方式?

我的回答

java创建线程可以通过thread来直接new创建,也可以通过实现runnable接口来实现,或者是从线程池中取用线程来实现,所有线程创建回收等操作由线程池来管控; 常规情况主要使用线程池来进行线程管控,可以更好的利用线程资源;

点评

优点:答到 Thread 子类、Runnable、线程池三种,并指出工程上首选线程池。
不足:**漏了 Callable + FutureTask(带返回值)** 这条最常考的方式;也没说清 Runnable 与 Callable 的区别;没说"线程池本质也是复用 Thread/Runnable"。
补充:标准 4 种——继承 Thread、实现 Runnable、实现 Callable(配 FutureTask)、线程池(ExecutorService);Java 单继承限制决定了"实现接口"优于"继承 Thread"。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能不能列出 4 种方式,并讲清继承 vs 接口、有无返回值的取舍。

**知识点拆解**:
1. **继承 Thread**:重写 `run()`,`new` 后 `start()`。受单继承限制,不推荐。
2. **实现 Runnable**:把任务与线程解耦,可配合线程池、lambda,推荐。
3. **实现 Callable**:`call()` 有返回值、可抛异常,需 `FutureTask` 包装(也实现了 Runnable)才能交给 Thread。
4. **线程池**:`ExecutorService.submit/execute`,复用线程、可控并发,**工程首选**。

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

**考察方向**:
- **深度**:`Thread.start()` → native `start0()` → JVM 创建内核线程
- **广度**:Future/FutureTask、CompletableFuture
- **应用**:为什么大厂规范强制用线程池

**延伸递进面试题**:
1. **Runnable 和 Callable 区别?**
- 解答要点:Callable 有返回值+可抛异常,需 FutureTask 适配
2. **调用 run() 和 start() 有什么区别?**
- 解答要点:run() 是普通方法调用(当前线程执行);start() 才创建新线程
3. **为什么阿里规范不推荐 new Thread / Executors?**
- 解答要点:无法管控资源、无界队列 OOM、线程暴涨

第三部分:面试者参考回答(三步法口语化)

**【一句话结论】**
面试官您好,创建线程标准有 4 种方式,实际开发首选线程池。

**【三点展开】**
第一,**继承 Thread** 重写 run,简单但受单继承限制;
第二,**实现 Runnable / Callable**——Callable 有返回值、能抛异常,要用 FutureTask 包装;
第三,**线程池** ExecutorService 提交任务,线程复用、并发可控,是工程首选。

**【一句边界】**
当然,简单的一次性任务用 Runnable 就行,但生产环境不要裸 `new Thread`,也不要用 `Executors` 快捷工厂。

#5 Java是如何判断一个线程是否存活的? medium java-concurrency

题目

Java是如何判断一个线程是否存活的?

我的回答

java中判断线程是否存活可以通过线程池提供的一个方法来判定,该方法是通过判定线程当前状态来识别线程池存活的,线程状态不为new和结束的都算存储

点评

优点:状态判断逻辑对——**非 NEW 且非 TERMINATED 即存活**,这是核心。
不足:**API 归属说错**——是 `Thread.isAlive()`,不是"线程池提供的方法";没点名 `getState()`。对比首次(2026-09-29,3/5)在 API 精确性上**退步**。
补充:`isAlive()` ≈ `state != NEW && state != TERMINATED`;存活 ≠ 运行——BLOCKED/WAITING/TIMED_WAITING 都算存活。 > ℹ️ 完整讲解见上方首次记录(2026-09-29)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 `Thread` 类 API 的精确掌握,尤其"存活"与"运行"两个概念的边界。

**知识点拆解**:
1. **底层原理**:`isAlive()` 的判断逻辑是 `state != NEW && state != TERMINATED`,本质是排除两端——没启动的、已结束的不算活
2. **设计权衡**:JDK 同时提供 `isAlive()` 和 `isTerminated()` 两个方法,覆盖正反向两种判断习惯;`!isTerminated()` 是官方推荐的等价写法
3. **工程实践**:主线程 join 子线程前先检查 `isAlive()` 避免 NPE;线程池复用前也检查(不过线程池有自己的一套,不用这个方法)

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

**考察方向**:
- **深度**:`isAlive()` 的 native 层实现(`_thread` 字段 + 状态机)
- **广度**:与 `getState()`、`isTerminated()`、`join()` 的组合用法
- **应用**:线程管理器中判断"是否需要补充新线程"

**延伸递进面试题**:
1. **NEW 状态算存活吗?**
- 解答要点:不算。`new Thread()` 没调 start,`isAlive()` 返回 false
2. **TERMINATED 和 NEW 怎么区分?**
- 解答要点:用 `getState()` 精确判断;`isAlive()` 只告诉你"是否活着",不区分是"没出生"还是"已死亡"
3. **isAlive() 是线程安全的吗?**
- 解答要点:是。它内部读的是 volatile 状态字段,无锁读取

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

面试官您好,Java 判断线程存活最直接的 API 是 `thread.isAlive()`,它内部判断 `state != NEW && state != TERMINATED`——只要不是"还没启动"或"已经结束",就算存活。反向判断可以用 `!thread.isTerminated()`,语义上更明确。

如果要用更底层的方式判断,可以先调 `thread.getState()` 拿到 `Thread.State` 枚举值,然后自己排除 NEW 和 TERMINATED 两个值。这种方式在需要区分"没启动"和"已死亡"的场景下更灵活。

有三个容易搞错的点我要强调:第一,NEW 不算存活——线程 new 出来但没调 start 的话,`isAlive()` 返回 false,很多人以为"存在对象就活着"是错的;第二,BLOCKED / WAITING / TIMED_WAITING 都算存活,它们只是"没在跑",不是"死了";第三,`isAlive()` 是线程安全的,底层读的是 volatile 状态,可以放心并发调用。

工程上常见用法:主线程在 join 子线程前先检查 `isAlive()`,避免对一个从未 start 的线程调 join 抛异常;线程管理框架在补充新线程前也会检查旧线程是否还在。

---

#6 synchronized的锁升级过程是怎样的? medium java-concurrency

题目

synchronized的锁升级过程是怎样的?

我的回答

synchronize,在jdk1.6之后增加了锁升级的逻辑,整个升级过程是根据锁的对象被访问的线程频率来的。当没有线程访问的时候是无锁的; 当有单个线程访问,没有并发线程进入的时候,是偏向锁,在线程头部记录当前线程,如果一致则直接访问不需要做什么控制判断; 当有多个线程访问,但是竞争度不高的时候使用的是轻量级锁,或者说自旋锁,持有锁的线程执行过程中,其他线程在自循环,等待持有者释放锁,释放后新线程直接持有锁,其他线程继续自旋,当自旋到一定次数还没有获取锁后就会进行锁升级,转为重量级锁,这时候直接是使用操作系统底层的锁机制来进行线程控制,持有锁的线程正常执行,其他线程进入阻塞状态,等待持有锁线程执行完毕唤醒所有阻塞中的线程争抢锁的持有状态

点评

优点:**四态路径完整**(无锁→偏向→轻量级→重量级)、触发条件准确(单线程→偏向、多线程低竞争→轻量级、自旋超阈值→重量级)、"对象头记录线程 ID"和"重量级用 OS 挂起"都讲到了;比首次(2026-09-29,3/5)更有条理。
不足:版本事实仍错("JDK 1.6 之后"——实际 **1.5 引入**、**JDK 15 默认关偏向**、**JDK 18 移除**);漏 Lock Record 结构、锁消除/锁粗化、"升级不可逆"的准确表述。
补充:现代 JDK(16+)实际只有"无锁 → 轻量级 → 重量级"三态。 > ℹ️ 完整讲解见上方首次记录(2026-09-29)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 `synchronized` 底层优化机制的理解,尤其偏向锁的生命周期与 JDK 版本演进。

**知识点拆解**:
1. **底层原理**:锁升级本质是 JIT 编译器针对不同竞争程度选择不同实现策略——**单线程走 CAS 无竞争、双线程走自旋、多冲突走内核阻塞**,全部靠 Mark Word 里的 4 位锁标志位切换
2. **设计权衡**:偏向锁在单线程场景零开销,但撤销代价高(需要等所有持有偏向锁的代码块执行完或全局安全点);轻量级锁自旋在 CPU 空闲时好,CPU 忙时会浪费;重量级锁有线程挂起的系统调用开销,但真正多冲突时最省心
3. **工程实践**:现代 JDK(16+)默认关闭偏向锁,因此实际开发中"锁升级"更多是"轻量级 → 重量级"两态;业务代码应尽量避免长时间持有锁,让锁停留在轻量级

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

**考察方向**:
- **深度**:Mark Word 4 位标志位变化、Lock Record 结构、偏向锁撤销的 safepoint 机制
- **广度**:与 `ReentrantLock` 的公平/非公平、`StampedLock` 的读优化对比
- **应用**:如何用 `jcmd`、`-XX:+PrintLocks` 观察锁状态;如何避免锁升级到重量级

**延伸递进面试题**:
1. **偏向锁为什么在 JDK 15 被默认关闭?**
- 解答要点:偏向撤销代价高,G1 等现代 GC 的 safepoint 频繁,撤销的延迟代价超过了本身收益
2. **如何判断锁是否升级到重量级?**
- 解答要点:`jcmd <pid> GC.heap_dump` 后用 MAT 查看 Mark Word;或开启 `-XX:+PrintLocks`
3. **synchronized 和 ReentrantLock 的选型依据是什么?**
- 解答要点:默认用 synchronized(简单、有 JIT 优化);需要超时、中断、公平、多 condition 时选 ReentrantLock

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

面试官您好,`synchronized` 的锁升级从 JDK 1.5 就引入了,本质是 JIT 根据竞争程度选择不同实现策略,全部靠对象头 Mark Word 里的 4 位标志位切换。

一共四态。**无锁**是初始状态,Mark Word 存对象元数据。**偏向锁**在单线程访问时启用,把当前线程 ID 写进 Mark Word,后续这个线程再访问时 CAS 校验线程 ID 匹配就无需加解锁,开销接近零。JDK 15 之后偏向锁**默认关闭**,JDK 18 **彻底移除**,原因是撤销代价高、G1 的 safepoint 频繁。**轻量级锁**在有第二个线程竞争时启用,把 Mark Word 拷贝到线程栈上的 Lock Record,然后 CAS 自旋,其他线程不会挂起。**重量级锁**在自旋超过阈值或多线程争抢时启用,通过操作系统 mutex 挂起竞争线程,有系统调用开销。

升级方向是**不可逆**的,但每次解锁后 Mark Word 会回到无锁状态,下次加锁可能重新走偏向锁。JIT 还有两个额外优化:**锁消除**(局部变量 StringBuilder 的 synchronized 会被直接删掉)、**锁粗化**(连续对同一对象加解锁合并成大锁)。

工程上,JDK 8 是"四态全开"的黄金版本;JDK 16+ 实际就是"轻量级 → 重量级"两态。业务要避免长时间持有锁,让锁停留在轻量级阶段。选型上默认用 synchronized,需要超时、中断、公平或多 condition 时才用 ReentrantLock。

---

#7 如何实现无锁化编程? medium java-concurrency

题目

如何实现无锁化编程?

我的回答

无锁编程的核心手段包括CAS,原子类,无共享状态等; CAS的核心是比较交换,以及原子性的单更新,通过比较预想值,当前值和目标值来实现对于属性更新时确保是从自己知道的改到自己目标的,但是存在ABA问题也就是当前的这个A和自己预期的A本质并不是一个,是其他线程修改后的结果; 原子类是通过cpu包装的原子操作进行对象的简单变更,但是只能应对简单的对象变更增加,并不能应对太复杂的操作; 无共享状态则是基于多线程没有交集,也就不会有线程安全问题,但是共享状态是很多业务所必需的,应用场景相对比较受限;

点评

优点:CAS/原子类/无共享状态三条都答到,ABA 问题点出且解释准确,也意识到"只能处理单变量、简单操作"的边界。
不足:没给**具体手段**(`AtomicInteger`/`AtomicReference`/`LongAdder`/`Unsafe`/`VarHandle`);没说 **ABA 的解法**(版本号 `AtomicStampedReference`);没提高并发下的 **自旋开销**。
补充:无锁 = 乐观重试 + 原子指令;适用"单变量、低到中竞争",高竞争时 LongAdder 优于 AtomicLong,再高就走分段/有锁。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:无锁的手段清单 + 各自边界 + ABA 治理。

**知识点拆解**:
1. **CAS**:`Unsafe.compareAndSwapInt`,靠 CPU 的 `cmpxchg` + `lock` 前缀保证原子;三要素 expected/update/new,失败重试(自旋)。
2. **原子类**:`AtomicInteger/AtomicReference/AtomicLong`;高并发计数用 `LongAdder`(分段 Cell 降低热点)优于 `AtomicLong`。
3. **无共享状态**:不可变对象(final + 构造期完成)、ThreadLocal、栈封闭。
4. **ABA 治理**:`AtomicStampedReference`(版本号)、`AtomicMarkableReference`(布尔标记)。
5. **边界**:只能保证**单个变量**的原子性;自旋在竞争激烈时浪费 CPU;复合操作要 `AtomicReference` 或直接加锁。

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

**考察方向**:
- **深度**:CAS 在 CPU 层怎么保证原子(缓存锁定/MESI)
- **广度**:与 volatile 的关系(可见性靠 volatile)、与 cell 分段
- **应用**:并发计数选型、无锁队列(Disruptor)

**延伸递进面试题**:
1. **CAS 的 ABA 怎么解决?**
- 解答要点:版本号/时间戳 `AtomicStampedReference`
2. **LongAdder 为什么比 AtomicLong 快?**
- 解答要点:分段 Cell,分散热点;`sum()` 是弱一致
3. **CAS 一定自旋吗?竞争大怎么办?**
- 解答要点:不一定;高竞争退化为自旋浪费,改用 LongAdder 或加锁

第三部分:面试者参考回答(三步法口语化)

**【一句话结论】**
面试官您好,无锁化编程本质是"用乐观重试 + 原子指令替代阻塞锁"。

**【三点展开】**
第一,核心手段是 **CAS 和原子类**,CAS 靠 CPU 的 `cmpxchg` 加锁前缀保证原子,失败就自旋重试;
第二,它解决的是**单变量**的原子更新,复合操作要用 `AtomicReference` 打包,高并发计数用 `LongAdder` 分段更好;
第三,还有**无共享状态**的思路——不可变对象、ThreadLocal,从根上避开竞争。

**【一句边界】**
当然,CAS 有 ABA 问题要用版本号,而且竞争激烈时自旋反而浪费 CPU——这时不如退回到锁或分段。

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

题目

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

我的回答

synchronized在不同锁等级下使用不同的方式来实现原子,可见,有序的,以重量锁来说 通过cpu提供的lock指令来实现多线程的互斥,实现一个线程在临界区域进行原子操作的; 通过释放锁后,通知主存,唤醒其他阻塞线程,来实现多线程的可见性的; 通过happend-before语义,内存屏障来实现阻止指令重排实现有序性的

点评

优点:**三条机制映射正确**——原子性 = lock 指令互斥、可见性 = 释放刷主存、有序性 = 屏障禁重排。比首次(2026-09-26,2/5,且当时把 synchronized 误说成公平锁)**大幅进步**。
不足:缺 Monitor/entryList 机制、加锁时 reload、"unlock happens-before lock" 规则;"通知主存唤醒其他线程"表述偏松——**唤醒靠 monitor,可见靠刷主存,是两件事**。
补充:synchronized 是**非公平锁**;volatile 只保可见+有序,synchronized 三者都保。 > ℹ️ 完整讲解见上方首次记录(2026-09-26)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否说清"原子性 = 互斥"、"可见性 = 解锁刷内存"、"有序性 = 禁止指令重排"这三条底层机制,以及 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 高并发下 synchronized 竞争激烈导致线程上下文切换开销过大,如何定位并优化? high java-concurrency

题目

高并发下 synchronized 竞争激烈导致线程上下文切换开销过大,如何定位并优化?

我的回答

可以通过检测线程状态通过jstack查看线程状态分布,如果有大量线程处于阻塞状态 ,那么可能是出现锁竞争激烈的情况,根据具体线程栈信息,确认阻塞位置,进行对应查看和优化; 竞争激烈,可能有两个原因,一个是并发度很高,大量线程访问,唤起竞争的线程很多,另一个是锁的粒度太大了,导致执行的时间较长,线程等待比较多; 根据不同情况做不同处理,大量线程访问的情况下,考虑是否可以跳过锁,使用异步或者缓存的方式不使用锁进行操作; 如果是线程粒度太大,内部执行内容时间过长那么就缩小粒度控制锁的范围,以及锁内部执行的内容进行不同的优化

点评

优点:定位思路(jstack 看阻塞 + 线程栈定位热点)和归因(并发高 / 锁粒度大)都对,优化方向(去锁/缓存/缩小锁范围)也合理。
不足:缺**量化定位**——上下文切换要看 `pidstat -w` 的 cswch/nvcswch、`vmstat` 的 cs 列,锁竞争要看火焰图/JFR 的 monitor blocked、`-XX:+PrintLocks`;优化手段太笼统,缺**锁分段、读写锁/StampedLock、LongAdder、CAS 乐观锁、减少临界区**等具体手段。
补充:上下文切换本身不一定是问题,要看"切换是否带来收益";先用数据确认瓶颈是锁还是 CPU,再动手。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否用数据定位锁竞争/上下文切换,并给出分层优化手段。

**知识点拆解**:
1. **定位**:
- `jstack` 看线程状态分布(大量 BLOCKED 指向 monitor 竞争)
- `pidstat -w` 看每秒上下文切换(cswch 自愿 / nvcswch 非自愿)
- `vmstat 1` 的 cs 列、JFR/火焰图的 "Java Monitor Blocked"、`-XX:+PrintLocks`
2. **归因**:并发线程数远超 CPU 核数(调度切换);临界区太长(持锁时间久);锁粒度过粗。
3. **优化**:
- **减小锁范围**:只锁必要代码,去掉临界区里的 IO/计算
- **降低锁粒度**:锁分段(ConcurrentHashMap 思路)、每对象独立锁
- **换锁类型**:读多写少用读写锁/StampedLock;高竞争计数用 LongAdder
- **去锁**:CAS 乐观更新、不可变、ThreadLocal、异步/缓存
- **削峰**:有界队列 + 幂等 + 限流,避免线程暴涨

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

**考察方向**:
- **深度**:上下文切换的成本构成(寄存器/栈/缓存失效)
- **广度**:与线程池调优、无锁、读写锁的联动
- **应用**:线上 CPU 飙高的完整排查链路

**延伸递进面试题**:
1. **怎么确认瓶颈是锁竞争而不是 CPU 计算?**
- 解答要点:BLOCKED 比例高 + cswch 高;若 RUNNABLE 高且 CPU 打满则偏计算
2. **锁分段怎么实现?**
- 解答要点:把一个锁拆成 N 个按 key 取模,降低竞争(ConcurrentHashMap 1.7 思路)
3. **自旋锁在什么场景反而更差?**
- 解答要点:竞争激烈时自旋空耗 CPU,不如直接阻塞挂起

第三部分:面试者参考回答(三步法口语化)

**【一句话结论】**
面试官您好,我会先用数据确认瓶颈在锁竞争,再做分层优化。

**【三点展开】**
第一,**定位**:`jstack` 看大量 BLOCKED、`pidstat -w` 看上下文切换次数、火焰图看 monitor blocked,确认热点锁;
第二,**归因**:要么并发线程数远超 CPU 核数,要么临界区太长、锁粒度太粗;
第三,**优化**:缩小临界区、锁分段降粒度、读多写少换读写锁/StampedLock、高竞争计数用 LongAdder,必要时用 CAS/异步彻底去锁。

**【一句边界】**
当然,上下文切换不一定是坏事——要确认它真的没带来收益;也不能盲目"去锁",语义正确性优先于性能。

#10 如何借鉴 Disruptor 的 RingBuffer 思想设计一个低延迟的无锁并发队列? high java-concurrency

题目

如何借鉴 Disruptor 的 RingBuffer 思想设计一个低延迟的无锁并发队列?

我的回答

通过环形数组,来实预创建空间,通过序号实现不同索引位置的读取,使用cas不用锁来实现序号更新;去读取数组内的内容,他没有锁,使用序号可以快速读取目标位置内容实现低延迟,高内存利用,而不是像是链表读取内容还需要遍历

点评

优点:抓住"环形数组 + 预分配 + 序号 + CAS 无锁"这四个关键点,方向对。
不足:**缺了低延迟的灵魂——生产者/消费者序号协调**。只说了"用序号读",没说多个生产者的序号怎么 CAS 竞争、消费者怎么通过 `Sequence`/`SequenceBarrier` 判断可消费位置、如何避免覆盖未消费数据。
补充:还有两个关键点没提——**缓存行填充(padding)消除伪共享**、**预分配对象避免 GC**;以及 Disruptor 用"批处理 + 序号依赖"做到无锁且高吞吐。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:能否讲清 RingBuffer 的无锁协调机制(序号)与低延迟手段(防伪共享、无 GC)。

**知识点拆解**:
1. **环形数组 + 预分配**:容量为 2 的幂,取模用位运算 `index & (size-1)`;元素在启动时全部预创建,运行期不再 new,避免 GC 抖动。
2. **序号(Sequence)协调**:
- 生产者声明位置:用 CAS 竞争 `cursor`/`next` 序号,多生产者用 `getAndIncrement` 抢占槽位
- 消费者:通过 `SequenceBarrier` 等待"可消费序号",用 `min(生产者序号, 依赖序号)` 决定能消费到哪
- 避免覆盖:生产者发布前检查"序号 - bufferSize"是否已被消费
3. **消除伪共享**:对序号字段做**缓存行填充**(padding 到 64 字节),避免相邻字段互相失效缓存行。
4. **批处理**:一次拿到连续可用区间,减少 CAS 和等待次数,提升吞吐。

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

**考察方向**:
- **深度**:多生产者如何用 CAS 保证序号不冲突;SequenceBarrier 的依赖等待
- **广度**:与 `ArrayBlockingQueue`(锁)、`ConcurrentLinkedQueue`(无界 CAS 链表)对比
- **应用**:日志/交易撮合等低延迟场景为何选 Disruptor

**延伸递进面试题**:
1. **Disruptor 为什么比 BlockingQueue 快?**
- 解答要点:无锁序号 + 预分配无 GC + 缓存行填充防伪共享 + 批处理
2. **多生产者如何避免序号冲突?**
- 解答要点:对 cursor 用 CAS 抢占,失败重试;或用 `Sequencer`
3. **为什么环形容量要取 2 的幂?**
- 解答要点:`index & (n-1)` 位运算代替取模,更快

第三部分:面试者参考回答(三步法口语化)

**【一句话结论】**
面试官您好,RingBuffer 的核心是"环形数组 + 序号协调 + 无锁",本质是用序号代替锁来协调生产消费。

**【三点展开】**
第一,**空间预分配**:容量取 2 的幂,启动时把元素全部创建好,取模用位运算,运行期零 GC;
第二,**序号协调是无锁的关键**:生产者用 CAS 抢占写序号,消费者用 `SequenceBarrier` 按依赖等待可读序号,靠"序号差 ≥ 容量"判断能否覆盖;
第三,**低延迟的细节**:序号字段做缓存行填充消除伪共享,再靠批处理一次取连续区间,减少 CAS 和等待。

**【一句边界】**
当然,Disruptor 适合吞吐极高、依赖简单的场景;如果只是普通异步解耦,`BlockingQueue` 更简单够用。

📚 查看全部面试记录