线程基础与生命周期

共 49 题
📑 题目列表 49 题
#
★★★

1. Exchanger 在双线程数据交换场景下的工程用例与注意事项

Exchanger 在双线程数据交换场景下的工程用例与注意事项是什么?

  • Exchanger 语义
  • 双线程交换
  • 注意事项

Exchanger 用于两个线程在某个栅栏点交换数据:两个线程都调用 exchange(x),各自返回对方的数据,实现"我给你的、你给我"。工程用例:双线程流水线(如 A 线程处理前半、B 线程处理后半,在交接点交换缓冲区)、成对的双缓冲交换、对称算法。注意事项:1) 必须恰好两个线程,线程数不符会永久等待;2) 用超时 exchange(timeout) 避免阻塞;3) 可中断;4) 记得 exchange 后返回的是对方的数据,双方要正确使用;5) 若线程数动态变化,Phaser 更合适。

Exchanger 是"双线程栅栏交换",适合固定两线程的对称交接。注意线程数匹配与超时。

#
★★★

2. InheritableThreadLocal 的父子线程值传递

InheritableThreadLocal 的父子线程值传递机制是什么?

  • InheritableThreadLocal 语义
  • 创建时传递
  • 局限

InheritableThreadLocal 让子线程在创建时继承父线程的 ThreadLocal 值:父线程创建子线程时,子线程的 ThreadLocalMap 会复制父线程的值。只在新线程创建时复制一次,之后父/子独立修改互不影响。局限:1) 线程池复用线程时不自动继承(新任务用旧线程的值,造成脏数据);2) 只传递一次,不随父线程更新而更新;3) 传递的是引用(共享可变对象需注意)。替代:TransmittableThreadLocal 或手动包装。

InheritableThreadLocal 传递发生在"创建时",线程池复用场景需 TransmittableThreadLocal 或手动透传。

#
★★★

3. LongAccumulator 的累加函数为何应满足结合性,非交换函数在并发分段合并时有何后果

LongAccumulator 的累加函数为何应满足结合性?非交换函数在并发分段合并时有何后果?

  • 结合性要求
  • 分段合并
  • 非交换后果

LongAccumulator 内部用 base+Cell[] 把累加分散到多个 Cell,最终 sum 时合并各 Cell 的值。若累加函数非结合(或非交换),合并顺序不同会得到不同结果,且并发更新顺序不确定,导致 sum 结果不确定。满足结合性(如加法、取最大值)保证无论分段、合并顺序如何,结果一致。非交换函数(如减法、减法依赖顺序)在分段合并时结果会出错,因此 LongAccumulator 要求函数可结合(通常也交换)。

分段累加要求函数"可结合(associative)",否则合并顺序影响结果。加法、max、min 满足,减除不满足。

#
★★★

4. Striped64 如何通过 cells 分散热点写入,竞争扩容期间 base 与 cells 分别承担什么角色

Striped64 如何通过 cells 分散热点写入?竞争扩容期间 base 与 cells 分别承担什么角色?

  • base 与 cells
  • 竞争扩容
  • 角色分工

Striped64 用 base 和 Cell[] 数组。低竞争时直接更新 base(CAS);竞争加剧时,通过线程哈希定位到某个 Cell 更新,把写入分散到多个 Cell,避免单点竞争。竞争扩容期间:base 仍是"低竞争时的兜底计数",cells 是"高竞争时的分散存储";当 Cell 更新冲突(CAS 失败)时,扩容 Cell 数组(翻倍)或迁移到其他 Cell,把热点进一步分散。base 与 cells 的角色:base 兜底单点,cells 分散热点,sum 时合并两者。

"分散写入"是 Striped64 的核心。base 兜底、cells 分散,扩容把竞争摊到更多 Cell。

#
★★★

5. ThreadLocal 在虚拟线程下的内存膨胀问题与 Scoped Values(JEP 506)的替代方案

ThreadLocal 在虚拟线程下的内存膨胀问题是什么?Scoped Values(JEP 506)如何替代?

  • ThreadLocal 与虚拟线程
  • 内存膨胀
  • Scoped Values

虚拟线程数量可达百万级,每个虚拟线程若用 ThreadLocal 存值,会为每个虚拟线程创建 ThreadLocalMap,内存占用随虚拟线程数线性膨胀,且大量虚拟线程的 ThreadLocal 难以回收,造成内存压力。Scoped Values(JEP 506)是替代方案:它把值绑定到"作用域"而非线程,在结构化并发中由框架隐式传递,只读、不可变、生命周期明确,不随线程数膨胀,且天然支持虚拟线程。Scoped Values 用于高效传递不可变上下文(如请求 ID、配置),避免 ThreadLocal 的膨胀与泄漏。

ThreadLocal 是"每线程一份",虚拟线程海量时内存膨胀;Scoped Values 是"每作用域一份",不随线程数增长,是虚拟线程时代的上下文传递方案。

#
★★★

6. ThreadLocal 的实现原理(ThreadLocalMap)与内存泄漏

ThreadLocal 的实现原理(ThreadLocalMap)与内存泄漏是什么?

  • ThreadLocalMap 结构
  • 弱引用 key
  • 内存泄漏

ThreadLocal 每个线程持有 ThreadLocalMap,key 是 ThreadLocal(弱引用),value 是用户值。内存泄漏:ThreadLocal 被回收(弱引用 key 变为 null)后,value 仍被 ThreadLocalMap 的强引用持有,若线程长期存活(如线程池),value 无法回收,造成内存泄漏。解决:用完调用 remove() 清除;ThreadLocalMap 的 get/set 会清理部分失效 entry,但不可靠。线程池中更易泄漏,因为线程复用且存活久。

泄漏根源是"弱引用 key + 强引用 value + 线程长期存活"。remove() 是根治手段,线程池场景尤其重要。

#
★★★

7. opaque 访问只保证什么一致性,在哪些高级算法中可用而不应被当作普通发布手段

opaque 访问只保证什么一致性?在哪些高级算法中可用而不应被当作普通发布手段?

  • opaque 一致性
  • 保证范围
  • 适用而不误用

opaque(VarHandle 的 opaque 访问)保证:1) 原子性(读/写不撕裂);2) 禁止 out-of-thin-air 值(读到的是某个实际写入的值);3) 保持"因果一致性"(同一线程内按因果顺序)。但不保证与其他线程操作的全序排序或及时可见性。它可用在高级算法中作为"不变量/进度标记"(如计数器、标志位,不依赖严格顺序)等,但不应当作普通发布手段(发布不可变对象、需要跨线程可见性时需 volatile/acquire/release)。opaque 是最弱的"非 plain"访问。

opaque 提供原子性+因果一致,但不提供排序/可见性。用于"确定性但无需强序"的标记,发布关键数据需更强的序。

#
★★★

8. 线程 start() 与 run() 的区别、为什么不能对同一线程重复 start

线程 start() 与 run() 的区别是什么?为什么不能对同一线程重复 start?

  • start 与 run 的区别
  • 重复 start 的异常
  • 线程状态机

run() 是普通方法,直接调用只执行 run 方法体(当前线程),不启动新线程;start() 才真正创建并启动新线程,并在线程内执行 run()。start() 只能调用一次:启动后线程进入 RUNNABLE,重复 start 会抛 IllegalThreadStateException(线程状态机不允许再次启动)。正确用法是新线程用 start(),当前线程直接调用 run() 则无并发。start 后线程经历 NEW→RUNNABLE→...→TERMINATED。

start 启动新线程,run 只是方法体。线程状态机(NEW→TERMINATED)不允许重复 start,故抛异常。

#
★★★

9. 虚拟线程的载体线程调度(ForkJoinPool)与并发上限

虚拟线程的载体线程调度(ForkJoinPool)与并发上限是什么?

  • 载体线程
  • ForkJoinPool 调度
  • 并发上限

虚拟线程由 JVM 调度到载体线程(平台线程)上执行,默认载体线程池是 ForkJoinPool(并行度=CPU 核数)。虚拟线程阻塞时被卸载,载体线程被释放去执行其他虚拟线程,因此并发上限不再受线程数限制,而是受"载体线程数(核数)+ 阻塞/卸载"影响。同一时刻最多核数个虚拟线程在物理执行,其余在等待或卸载。CPU 密集型任务依旧受核数限制,IO 密集型靠卸载提升并发。可通过 -Djava.util.concurrent.ForkJoinPool.common.parallelism 调整。

虚拟线程是"1:N 调度",载体线程池(ForkJoinPool)决定物理并行度。并发上限受核数而非虚拟线程数限制。

#
★★★

10. Java 21 虚拟线程(Virtual Threads,JEP 444)的核心特性与启用边界

Java 21 虚拟线程(Virtual Threads,JEP 444)的核心特性与启用边界是什么?

  • 轻量线程
  • 1:N 调度
  • 适用边界

虚拟线程(JEP 444,Java 21 正式)是 JVM 管理的轻量级线程,核心特性:1) 数量可达百万级(廉价);2) 1:N 映射到平台线程(载体线程),阻塞时自动卸载、恢复时重挂载;3) 简化并发编程(thread-per-request 风格,无需回调/异步);4) 与现有 API 兼容(可阻塞,JVM 处理卸载)。启用边界:适合 IO 密集型(阻塞等待多);不适合 CPU 密集(无卸载收益,受核数限制);不适合持锁内长阻塞(原生路径可能钉住);不适合需要大量共享可变状态或精确线程控制的场景。

虚拟线程解决"阻塞浪费载体线程"问题,适合 IO 密集;CPU 密集与持锁长阻塞场景收益有限。

#
★★★

11. Thread.setName/setContextClassLoader 的运维价值

Thread.setName/setContextClassLoader 的运维价值是什么?

  • setName 运维
  • setContextClassLoader
  • 场景

setName 给线程起有意义的名字(如业务线、池名),线程 dump、日志、监控中能快速定位线程归属,是运维排障的基础(配合 ThreadFactory 统一命名)。setContextClassLoader 设置线程上下文类加载器,用于让框架动态加载类(如 Spring、JDBC 驱动、SPI)时能加载到正确类,是类加载隔离的关键。运维价值:命名提升可观测性,上下文类加载器保证类加载正确(避免 ClassNotFoundException)。

setName 提升可观测性(dump/日志定位),setContextClassLoader 保证动态类加载正确。两者都是运维与框架协作的基础。

#
★★

12. 伪共享(False Sharing)的诊断与避免

伪共享(False Sharing)如何诊断与避免?

  • 伪共享诊断
  • 缓存行填充
  • 避免方法

伪共享:多个独立变量共享同一缓存行,更新一个使整个缓存行失效,拖慢其他更新。诊断:用 perf/PMU 观测 cache miss 与缓存行争用(HITM 事件)、对比加填充前后的吞吐、用 JFR 或采样看热点。避免:1) 缓存行填充(@Contended 或手动 padding 到 64 字节);2) 变量分散(不同缓存行);3) 用分段(如 LongAdder 的 Cell[]);4) 减少共享热字段。核心是"让热变量各自独占缓存行"。

伪共享诊断靠硬件计数器,避免靠缓存行填充/分段。堆高并发性能优化的经典手段。

#
★★

13. 守护线程(Daemon Thread)的退出语义与坑点

守护线程(Daemon Thread)的退出语义与坑点是什么?

  • 守护线程语义
  • 退出时机
  • 坑点

守护线程(setDaemon(true))用于后台服务(如 GC、监控),当所有非守护线程终止时,JVM 退出并强制终止守护线程(不等待其完成)。坑点:1) 守护线程不保证执行完,可能被中途终止(不能用于关键任务);2) 在守护线程中做资源清理/事务可能不完整;3) start 前设置 setDaemon,start 后不能再改;4) 守护线程的 clean 逻辑被截断。适合周期性辅助任务,不适合关键业务。

守护线程"随 JVM 退出而终止",不保证完成。关键业务不能放守护线程。

#
★★

14. 条件等待必须使用 while 而非 if 的原因是什么,虚假唤醒和条件被其他线程抢先改变如何处理

条件等待必须使用 while 而非 if 的原因是什么?虚假唤醒和条件被其他线程抢先改变如何处理?

  • while 循环
  • 虚假唤醒
  • 条件抢先改变

条件等待(wait/await)必须用 while 循环而非 if,原因:1) 虚假唤醒(spurious wakeup):即使没有 notify,wait 也可能返回,需重新检查条件;2) 条件被其他线程抢先改变:被唤醒后,其他线程可能又改条件,持有锁后需重新校验。用 while 循环在唤醒后重新检查条件,不满足则继续等待,保证正确性。这是"等待-检查-再等待"的规范模式。

while 循环保证"唤醒后重新验证条件",防虚假唤醒与条件被抢先改变。if 只检查一次,易出错。

synchronized (lock) {
    while (!condition) { lock.wait(); } // 必须 while
    useData();
}
#
★★

15. 线程 CPU 时间统计(MXBean.getThreadCpuTime)

线程 CPU 时间统计(MXBean.getThreadCpuTime)如何应用?

  • getThreadCpuTime
  • 采样 CPU 时间
  • 定位热点

ThreadMXBean.getThreadCpuTime(threadId) 返回指定线程累计的 CPU 时间(纳秒),可用于:1) 采样线程 CPU 使用,识别 CPU 密集线程;2) 计算线程实际执行时间(与等待时间区分);3) 监控线程是否空转(CPU 时间高但无进展)。应用:结合 getCurrentThreadCpuTime 测量临界区耗时,或周期性采样 getThreadCpuTime 差值定位热点线程。需 JVM 支持(默认开启)。

CPU 时间统计区分"线程跑了多久"与"等待多久",是定位 CPU 热点与空转的关键指标。

#
★★

16. 线程 dump(jstack)的分析关键字段

线程 dump(jstack)的分析关键字段是什么?

  • 线程状态
  • 锁标记
  • 调用栈

jstack 关键字段:1) 线程名与 id(定位线程归属);2) 线程状态(RUNNABLE/BLOCKED/WAITING/TIMED_WAITING,判断在运行/等锁/等条件);3) 栈帧(调用路径,定位业务);4) "locked"(已持有锁)、"waiting to lock"(等待锁)、"parking to wait"(等待 LockSupport)等锁标记;5) 死锁检测部分(Found one Java-level deadlock);6) 线程优先级与 daemon 标志。分析时结合状态+锁标记+栈定位阻塞点与竞争点。

线程 dump 的关键是"状态 + 锁标记 + 栈"三要素,结合死锁检测定位问题。

#
★★

17. 线程与进程的本质差异、线程共享的资源与隔离的资源

线程与进程的本质差异是什么?线程共享哪些资源、隔离哪些资源?

  • 线程 vs 进程
  • 共享资源
  • 隔离资源

进程是资源分配的基本单位,线程是 CPU 调度的基本单位,一个进程含多个线程。线程共享:进程的地址空间(堆、静态变量、代码段)、文件描述符、打开的文件、信号等。线程隔离:各自的栈、寄存器、程序计数器、线程本地存储(ThreadLocal)。差异:线程切换快(共享地址空间)、通信方便(共享内存)但线程间同步问题多;进程隔离强、切换慢、通信需 IPC。线程的优势是轻量共享,代价是并发安全。

线程共享"进程级资源",隔离"执行上下文"。共享带来高效与并发挑战。

#
★★

18. 线程中断(interrupt())与 InterruptedException 捕获的协作规范及与 Future.cancel 的关系

线程中断(interrupt())与 InterruptedException 捕获的协作规范是什么?与 Future.cancel 有什么关系?

  • interrupt 协作式
  • InterruptedException 处理
  • Future.cancel

线程中断是协作式的:interrupt() 设置中断标志,被中断线程需自行检查(Thread.interrupted/isInterrupted)或阻塞操作抛 InterruptedException 来响应。规范:捕获 InterruptedException 后应恢复中断标志(Thread.currentThread().interrupt())或重新抛出,不能吞掉,否则上层无法感知中断。Future.cancel(mayInterruptIfRunning) 取消任务:若为 true 则调用执行线程的 interrupt(),依赖任务对中断的协作响应;若任务不响应中断则无法真正取消。因此中断协作规范是 Future.cancel 生效的前提。

中断是"协作式",靠响应规范传递。Future.cancel 用 interrupt 实现,需任务正确响应中断。

#
★★

19. 线程亲和性(CPU Affinity)的 Java 现状

线程亲和性(CPU Affinity)在 Java 中的现状是什么?

  • CPU 亲和性概念
  • Java 支持
  • 现状

CPU 亲和性(把线程绑定到特定 CPU)可减少缓存失效与调度开销,但 Java 标准库不提供线程 CPU 亲和性 API。现状:需借助 JNI/native 库(如 JNA 调用 pthread_setaffinity_np、sched_setaffinity)或第三方库实现,受平台限制。Java 层无法直接设置亲和性,JVM 的线程调度由 OS 决定。虚拟线程下亲和性更不适用(虚拟线程在载体线程间迁移,无法绑定具体 CPU)。因此 Java 中 CPU 亲和性支持有限,且仅在特定高性能场景(如绑定核的实时处理)需要。

Java 无标准亲和性 API,需 native 实现,且虚拟线程的迁移特性使亲和性失去意义。

#
★★

20. 线程优先级(setPriority)在不同操作系统上的实际效果与 JVM 的处理策略

线程优先级(setPriority)在不同操作系统上的实际效果与 JVM 的处理策略是什么?

  • setPriority
  • OS 差异
  • JVM 策略

setPriority(1-10) 是给 OS 的调度提示,实际效果因 OS 而异:Windows/Linux 上通常会对应用户态优先级/调度类,但现代 Linux CFS 调度器基本忽略 java 优先级;macOS 支持有限。JVM 把 Java 优先级映射到 OS 优先级,但现代 OS 线程调度多为公平/动态,优先级影响有限甚至无效。处理策略:不依赖优先级保证调度顺序或公平性;若需确定性用锁的公平性、无锁或显式调度。优先级在 Java 中最多是"软提示"。

线程优先级在现代 OS 上作用有限,不能依赖它保证顺序。公平性靠锁策略而非优先级。

#
★★

21. 线程优先级(setPriority)的真实影响与 OS 调度

线程优先级(setPriority)的真实影响与 OS 调度是什么?

  • 真实影响
  • OS 调度
  • 平台差异

setPriority 的真实影响较小:Java 优先级映射到 OS 优先级,但现代 OS(Linux CFS、Windows 多级队列)的调度主要基于公平与动态,Java 优先级常被忽略或影响有限。Linux 上 Java 线程优先级映射到 nice 值,但 CFS 对 nice 的响应有限;Windows 上映射到线程优先级类,有一定影响。因此不能依赖优先级提高某线程的绝对调度优先权。OS 调度是抢占式+公平,Java 优先级只是"提示"。

优先级是"提示",OS 调度以公平/动态为主。依赖优先级做调度控制不可靠。

#
★★

22. 线程命名(setName)在生产环境日志追踪与 Micrometer 集成中的工程价值

线程命名(setName)在生产环境日志追踪与 Micrometer 集成中的工程价值是什么?

  • 日志追踪
  • Micrometer 集成
  • 命名价值

线程命名(配合 ThreadFactory 统一命名)让日志、线程 dump、监控中能识别线程归属(如业务池、HTTP 线程、调度线程),便于日志追踪与排障。Micrometer 集成:线程池可通过 Micrometer 的 ExecutorServiceMetrics 暴露活跃线程数、队列大小等指标,线程名帮助区分不同池的指标。工程价值:命名+指标结合,能快速定位"哪个池的线程满、哪个池阻塞",实现可观测性。命名是运维与监控的基础。

线程命名提升可观测性,配合 Micrometer 指标定位池瓶颈。命名规范是生产监控的基础设施。

#
★★

23. 线程状态在 Linux 上的 native 状态映射

线程状态在 Linux 上的 native 状态映射是什么?

  • Java 状态
  • Linux native 状态
  • 映射

Java 线程状态与 Linux native 状态的映射:NEW→(未创建);RUNNABLE→R(running 或 ready);BLOCKED→S(sleeping,等待锁);WAITING→S(sleeping);TIMED_WAITING→S;TERMINATED→Z(zombie,如果 peek 到)。jstack 显示 Java 状态,ps/jstack -l 或 /proc/pid/status 显示 native 状态(S=sleeping、R=running、D=disk sleep、Z=zombie)。分析时 Java 层的 BLOCKED/WAITING 对应 native 的 S(sleeping),RUNNABLE 对应 R 或短暂 S。

Java 状态是抽象,native 状态是 OS 视角。RUNNABLE 可能包含 native 的 D(disk IO 阻塞)等,需结合两者。

#
★★

24. 线程生命周期(NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED)

线程生命周期(NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED)是什么?

  • 六种状态
  • 状态转换
  • 触发条件

Java 线程六种状态:NEW(已创建未 start);RUNNABLE(就绪或运行中,可被调度);BLOCKED(等待获取监视器锁);WAITING(无限等待,如 wait/park/join 无超时);TIMED_WAITING(限时等待,如 sleep/wait(timeout)/parkNanos);TERMINATED(run 结束)。转换:NEW→start→RUNNABLE;RUNNABLE 获取锁失败→BLOCKED;调用 wait/park/join→WAITING/TIMED_WAITING;阻塞结束→RUNNABLE;run 结束→TERMINATED。理解状态用于线程 dump 与调试。

六状态是线程 dump 的基础。BLOCKED 等锁、WAITING 等条件、RUNNABLE 执行,状态转换看触发条件。

#
★★

25. 线程的 UncaughtExceptionHandler 设置与日志收集

线程的 UncaughtExceptionHandler 如何设置?如何用于日志收集?

  • UncaughtExceptionHandler
  • 设置方式
  • 日志收集

UncaughtExceptionHandler 捕获线程未处理异常(run 方法抛出的异常)。设置方式:Thread.setUncaughtExceptionHandler(单线程)、Thread.setDefaultUncaughtExceptionHandler(全局默认)、线程池的 ThreadFactory 中设置。日志收集:在 handler 中记录异常到日志、监控、告警,统一处理未捕获异常,避免静默吞掉。注意:线程池任务内自己捕获的异常不会触发 handler;Handler 是"漏网之鱼"的兜底。

UncaughtExceptionHandler 是未捕获异常的兜底,用于统一日志与告警。任务内吞掉的异常不触发它。

#
★★

26. 线程的创建方式(继承 Thread、实现 Runnable/Callable、线程池)

线程的创建方式(继承 Thread、实现 Runnable/Callable、线程池)有哪些?

  • 继承 Thread
  • Runnable/Callable
  • 线程池

创建方式:1) 继承 Thread 重写 run()(简单但耦合、难复用);2) 实现 Runnable(run 无返回值)传给 Thread;3) 实现 Callable(call 有返回值、可抛异常)配合 FutureTask/Executor;4) 线程池(ExecutorService.submit(Callable) 返回 Future)——推荐,复用线程、管理生命周期。实际开发优先用 Runnable/Callable + 线程池,避免继承 Thread 与裸线程。

推荐"任务与线程分离":Runnable/Callable 定义任务,线程池管理执行。继承 Thread 不推荐。

#
★★

27. 线程组(ThreadGroup)的现状与替代

线程组(ThreadGroup)的现状与替代是什么?

  • ThreadGroup 现状
  • 局限
  • 替代

ThreadGroup 用于管理线程集合,但已过时:JDK 5+ 线程池等不再依赖它,JEP 与 JDK 建议不推荐使用;其功能(统一异常处理、批量操作)有限且未被广泛使用。替代:线程池(ThreadPoolExecutor、ForkJoinPool)管理线程生命周期;虚拟线程无需 ThreadGroup;用 ExecutorService 与监控 API 替代线程组管理。ThreadGroup 保留仅为了兼容,新代码不用。

ThreadGroup 已过时,线程池供管理,虚拟线程更不需要。新代码用 ExecutorService 体系。

#
★★

28. Exchanger 的使用场景

Exchanger 的使用场景是什么?

  • Exchanger 场景
  • 双线程交换
  • 典型应用

Exchanger 用于两个线程在栅栏点交换数据,典型场景:1) 双缓冲流水线(A 线程生产、B 线程消费,在交接点交换缓冲区);2) 对称算法中的位置互换;3) 两线程并行处理时交换中间结果;4) 基因/图像算法中成对交换数据。要求恰好两个线程、同步会合。若参与方动态变化,用 Phaser 更合适。

Exchanger 是"双线程栅栏交换",适合固定两方的对称交接,用 exchange 交换数据。

#
★★

29. Exchanger 的双槽实现

Exchanger 的双槽实现是什么?

  • 双槽机制
  • 数据交换
  • 实现原理

Exchanger 内部用"槽位"(最近使用的 slot 和 arena 数组)实现交换。双槽/arena 机制:为并发交换优化,多个线程竞争时用 arena 数组分散,避免单一 slot 竞争。基础交换:一个线程把数据放入 slot 并等待,另一个线程读取并放入自己的数据,双方完成交换。双槽(双 paticipant)让两个线程在各自 slot 上交换,减少竞争。实现基于 CAS 更新 slot 状态,配合自旋与 park。

双槽/arena 是 Exchanger 减少竞争的设计,让并发交换在多个槽位分散。

#
★★

30. LockSupport.park 与 unpark 的许可语义如何避免丢失通知,为什么仍要在条件循环中检查状态

LockSupport.park 与 unpark 的许可语义如何避免丢失通知?为什么仍要在条件循环中检查状态?

  • 许可语义
  • 避免丢失通知
  • 条件循环

park/unpark 用"许可(permit)"机制:unpark 给线程一个许可(可累积一次),park 消费许可。若 unpark 先于 park 调用,许可被缓存,park 会立即返回(不阻塞),因此不会丢失通知——这是相对 wait/notify 的优势(notify 先于 wait 会丢失)。但 park 仍可能因虚假唤醒或条件实际未满足而返回,因此必须在条件循环中检查状态:park 返回后重新检查条件,不满足则继续 park。许可机制保证"不丢唤醒",循环保证"唤醒后正确重验"。

许可可缓存避免丢失唤醒,循环防虚假唤醒。二者配合是 park 的正确使用模式。

#
★★

31. Thread.join() 的实现原理(wait/notify)与超时 join 的语义

Thread.join() 的实现原理(wait/notify)与超时 join 的语义是什么?

  • join 实现
  • wait/notify
  • 超时语义

join 的实现原理:底层通过 wait()。join() 内部调用 while (isAlive()) wait(0)(0 表示无限等待),线程终止时 JVM 会 notifyAll 唤醒等待者,join 返回。因此 join 等待的是"目标线程终止"事件。超时 join(timeout):wait(timeout),等待至多 timeout 毫秒,超时后即使线程未终止也返回(通常需检查 isAlive 判断是否超时)。join 是 wait/notify 的封装,等待线程终止。

join 底层是 wait(0) 直到线程终止,超时 join 是 wait(timeout),返回后需检查 isAlive 判断是否超时。

#
★★

32. sleep 与 yield 在让出 CPU 上的语义差异与适用场景

sleep 与 yield 在让出 CPU 上的语义差异与适用场景是什么?

  • sleep 语义
  • yield 语义
  • 适用场景

sleep(ms):当前线程休眠指定毫秒,进入 TIMED_WAITING,确定让出 CPU 且不参与调度(到期后恢复),可被中断(抛 InterruptedException)。yield():提示当前线程让出 CPU,但只进入 RUNNABLE 队列(可能立即被调度回),无确定等待,不保证让出。适用:sleep 用于"定时/限速/等待",yield 用于"忙等时让出"或"低优先级任务让步"。实际并发中 sleep 更常用,yield 只在忙等循环中偶尔用。

sleep 确定休眠、可中断;yield 只是提示、不保证。定时用 sleep,忙等让步用 yield。

#

33. JDK 25 虚拟线程(JEP 444,VirtualThread)与平台线程的 1:N 调度模型对传统线程池设计的影响

JDK 25 虚拟线程(JEP 444,VirtualThread)与平台线程的 1:N 调度模型对传统线程池设计有什么影响?

  • 1:N 调度
  • 传统线程池
  • 影响

虚拟线程 1:N 调度(多个虚拟线程映射到少量载体线程)让"线程"变得廉价,传统线程池的"池大小 = 核数"经验公式对 IO 密集不再适用:IO 密集任务无需池化,直接用虚拟线程(newVirtualThreadPerTaskExecutor)即可,无需手动调池大小。影响:1) IO 密集场景传统线程池被虚拟线程替代;2) CPU 密集仍需平台线程池(受核数限制);3) 线程池的"避免线程创建开销"价值在虚拟线程下减弱;4) 池化语义(复用、限流)仍需线程池或信号量。设计时应按任务类型选择。

虚拟线程让线程廉价,IO 密集免池化,CPU 密集仍池化。传统线程池的价值转移到"限流/隔离"而非"省线程"。

#

34. Thread.UncaughtExceptionHandler 在虚拟线程中独立异常传播路径的设计

Thread.UncaughtExceptionHandler 在虚拟线程中独立异常传播路径的设计是什么?

  • 虚拟线程异常
  • 独立传播路径
  • 设计

虚拟线程的异常传播路径与平台线程一致:未捕获异常由虚拟线程的 UncaughtExceptionHandler 处理(每个虚拟线程可设置,或默认处理器)。虚拟线程的"独立异常传播路径"指:虚拟线程代表一个独立任务,其异常只影响该虚拟线程,不污染其他虚拟线程(除非共享数据)。设计上通过 UncaughtExceptionHandler 捕获虚拟线程异常,统一记录/告警,避免静默。在结构化并发(StructuredTaskScope)中,子任务的异常会传播到父任务,需在 scope 中处理。

虚拟线程异常传播独立,用 UncaughtExceptionHandler 兜底;结构化并发中异常聚合到父作用域。

#

35. Thread.interrupt() 的协作式中断机制与 InterruptedException 处理

Thread.interrupt() 的协作式中断机制与 InterruptedException 处理是什么?

  • 协作式中断
  • 中断标志
  • InterruptedException

interrupt() 是协作式:只设置中断标志,不强制停止线程。被中断线程通过:1) 检查 isInterrupted()/interrupted();2) 阻塞操作(wait/sleep/join/锁等待)抛 InterruptedException 响应。InterruptedException 处理规范:捕获后恢复中断标志(Thread.currentThread().interrupt())或重新抛出,将中断传递给上层,不吞掉。响应中断是"及时停止"的协作方式,任务应尽快清理并退出。

中断是协作标志,靠检查与 InterruptedException 响应。处理 InterruptedException 要恢复标志不吞掉。

#

36. Thread.onSpinWait 如何向处理器提示自旋,为什么它不能替代超时、退避或阻塞机制

Thread.onSpinWait 如何向处理器提示自旋?为什么它不能替代超时、退避或阻塞机制?

  • onSpinWait 提示
  • 处理器行为
  • 不能替代的机制

Thread.onSpinWait()(JDK 9)向处理器提示"本线程正在自旋等待",处理器可降低功耗、优化流水线(如 x86 的 PAUSE 指令),减少自旋的功耗与流水线影响。它只是"提示",不改变自旋逻辑。局限:1) 只在自旋时有效,不能替代超时(自旋可能无限期);2) 不能替代退避(若无退避,争用仍高);3) 不能替代阻塞(长等待应 park 阻塞而非自旋)。onSpinWait 用于"短期待自旋"的优化提示。

onSpinWait 是自旋的"节能提示",不是机制。长等待仍需超时/退避/阻塞兜底。

#

37. Thread.setDaemon(true) 在虚拟线程下的语义变化与守护线程边界

Thread.setDaemon(true) 在虚拟线程下的语义变化与守护线程边界是什么?

  • setDaemon 语义
  • 虚拟线程
  • 边界

虚拟线程(Thread.ofVirtual)默认是守护线程(daemon),setDaemon 在虚拟线程上语义受限:虚拟线程不能修改其 daemon 属性(继承 true 并固定),因为虚拟线程是"短期、随主线程结束"的轻量任务。守护线程边界:虚拟线程作为守护线程,JVM 退出时不会等待其完成;虚拟线程的存活受其载体与主线程影响。因此虚拟线程适合短生命周期任务,不适合需要 JVM 等待完成的长期后台任务(需显式 join 或平台线程)。

虚拟线程默认守护且不可改,适合短任务;长期后台任务需平台线程或显式等待。

#

38. Thread.stop()/suspend()/resume() 为何废弃

Thread.stop()/suspend()/resume() 为何废弃?

  • 废弃原因
  • 不安全
  • 替代

Thread.stop() 在线程任意位置强制终止,可能抛出没有预期的异常(ThreadDeath),导致数据不一致(如破坏锁、对象状态半更新),且会释放持有的锁,安全无法保证。suspend()/resume() 暂停/恢复线程,可能造成死锁(暂停线程持有锁时,恢复它的线程等锁)。三者都不安全,JDK 明确废弃。替代:用中断(interrupt())协作式停止,配合标志位与条件检查逐步退出。

stop/suspend/resume 不安全(破坏状态、死锁),废弃后用于中断协作式停止。

#

39. compareAndExchange 与 compareAndSet 的返回语义有何不同,失败时 witness value 可怎样利用

compareAndExchange 与 compareAndSet 的返回语义有何不同?失败时 witness value 可怎样利用?

  • 返回语义
  • witness value
  • 利用

compareAndSet 返回 boolean(是否成功)。compareAndExchange 返回当前值(witness value):成功时返回期望值(确认更新),失败时返回实际当前值。失败时的 witness value 有利用价值:它告诉调用方"当前实际值是什么",调用方可直接基于该实际值构造下一次更新,省去一次单独的 get() 读取,减少 CAS 循环中的读操作。这在无锁循环中可优化(一次调用同时获得"是否成功"与"当前值")。

compareAndExchange 返回 witness value,失败时可复用为下一次 CAS 的期望值,减少一次 get。

#

40. compareAndSet、compareAndExchange 与不同内存序应如何选择

compareAndSet、compareAndExchange 与不同内存序应如何选择?

  • 方法选择
  • 内存序选择
  • 场景

方法选择:需布尔结果用 compareAndSet,需返回值/witness 用 compareAndExchange(失败复用当前值)。内存序选择:默认 compareAndSet/Exchange 是 volatile 语义(强序);用 VarHandle 可指定弱序(weakCompareAndSetPlain/acquire/release/opaque)。选择依据:需要强可见性(全序)用 volatile;只需原子性、允许弱序用 plain/opaque;发布-获取模式用 acquire/release。弱序性能更高但语义弱,需谨慎。一般默认用强序,优化热点时才降内存序。

方法选结果语义,内存序选可见性强度。默认强序,性能敏感时按需降序。

#

41. final 字段安全发布的边界(this 引用逃逸)

final 字段安全发布的边界(this 引用逃逸)是什么?

  • 安全发布
  • this 逃逸
  • 边界

final 字段的安全发布保证有边界:若构造器内 this 引用逃逸(如下发监听器、启动线程、把 this 传给外部),则 final 字段的冻结语义可能失效,其他线程可能看到未完全构造的对象。final 字段的安全发布仅当"构造完成后、通过安全路径发布"才成立。因此构造器内不能泄露 this(如把 this 发布给新线程、注册监听器),否则读者可能读到半初始化对象。这是 final 安全发布的边界条件。

this 逃逸破坏 final 安全发布。构造器内不得把 this 暴露给其他线程,否则冻结语义失效。

#

42. lockInterruptibly 与普通 lock 在排队期间收到中断时有何差异,调用方应如何清理状态

lockInterruptibly 与普通 lock 在排队期间收到中断时有何差异?调用方应如何清理状态?

  • lockInterruptibly
  • 中断响应
  • 状态清理

lock() 在排队期间不响应中断(即使被 interrupt 也继续等待,直到获取锁;获取后中断标志保留,需自行检查)。lockInterruptibly() 在排队期间响应中断:被 interrupt 时抛 InterruptedException 并放弃等待。调用方清理状态:捕获 InterruptedException 后恢复中断标志(Thread.currentThread().interrupt()),并清理相关资源(如已获取的锁、临时状态),避免残留。需可中断获取用 lockInterruptibly,否则用 lock。

lock 不响应中断,lockInterruptibly 响应。捕获后清理状态并恢复中断标志。

#

43. out-of-thin-air 值问题与 JLS 限制

out-of-thin-air 值问题与 JLS 限制是什么?

  • out-of-thin-air
  • JLS 限制
  • 保证

out-of-thin-air 值指:在无同步的并发下,读取操作可能读到"凭空出现"的值(既不是默认值也不是任何写入的值),由循环依赖产生。JLS 限制:JMM 规定读取必须读到默认值(0/null)或某个实际写入的值,禁止 out-of-thin-air 值,保证"无因果关系"的值不会出现。这通过 JMM 的因果关系(causality)约束实现。实际影响:合法的并发读值只能是"某次写入的值或默认值",不能是计算的残影。opaque/volatile 保证读到合法值。

JMM 禁止 out-of-thin-air 值,读到的值必须是默认值或某次写入的值。这是 JMM 形式化的因果约束。

#

44. weakCompareAndSet 允许伪失败意味着什么,为什么它通常必须放在可重试循环中

weakCompareAndSet 允许伪失败意味着什么?为什么它通常必须放在可重试循环中?

  • 伪失败
  • 可重试循环
  • 原因

weakCompareAndSet 允许"伪失败":即使条件满足也可能返回 false(硬件/内存序考虑),不保证失败的确切语义。因此它必须放在可重试循环中:失败时循环重试,直到成功(或达到上限),否则误把"伪失败"当作"真正失败"而放弃。伪失败是弱 CAS 的松弛行为,换取更低的硬件开销。用于无锁算法中,循环保证任何伪失败最终会被重试补救。注意 JDK 9 起 weakCompareAndSet 语义与 compareAndSet 合并,真正弱版本用 VarHandle。

伪失败需循环重试补救。弱 CAS 用松弛语义换性能,正确性靠循环保证。

#

45. 如何用 JMH 设计可重复实验并排除预热、伪共享与调度噪声

如何用 JMH 设计可重复实验并排除预热、伪共享与调度噪声?

  • 预热
  • 伪共享
  • 调度噪声

JMH 设计要点:1) 预热(@Warmup 多轮)消除 JIT 编译与类加载影响;2) 多次迭代(@Measurement)与多 Fork 减少单次抖动;3) 用 @State(Scope.Benchmark) 共享状态,避免 JIT 消除;4) 控制线程数(@Threads)与避免调度噪声(绑定核、减少其他负载);5) 用 @Blackhole 消费结果防优化;6) 伪共享注意共享字段的缓存行布局。可重复性靠充分预热+多迭代+固定环境。用黑盒与防抖参数输出稳定结果。

JMH 可重复性 = 预热 + 多迭代 + 环境控制。排除 JIT/伪共享/调度噪声是公平基准的前提。

#

46. 自定义共享同步器的 tryAcquireShared 返回值如何控制传播,返回零与正数分别意味着什么

自定义共享同步器的 tryAcquireShared 返回值如何控制传播?返回零与正数分别意味着什么?

  • tryAcquireShared 返回值
  • 传播控制
  • 零与正数

tryAcquireShared 返回值:负数=获取失败需排队;零=获取成功但无剩余许可(不再传播);正数=获取成功且有剩余许可(可继续传播唤醒后继)。零与正数的区别在传播:返回 0 时,setHeadAndPropagate 只唤醒 head 一个后继,不继续传播;返回正数时,会继续唤醒更多后继(传播共享许可)。因此"是否继续传播"取决于返回值正负。返回 0 表示"刚好满足,不再放行",正数表示"还有余量,继续放行"。

返回值正负控制传播深度。0 停止传播,正数继续传播,负数排队。这是共享同步器语义的关键。

#

47. Java 25 虚拟线程生态(JEP 506 Scoped Values)

Java 25 虚拟线程生态(JEP 506 Scoped Values)是什么?

  • Scoped Values
  • 虚拟线程
  • 上下文传递

JEP 506 Scoped Values 是虚拟线程生态的上下文传递方案:把值绑定到"作用域"而非线程,在结构化并发(StructuredTaskScope)中由框架自动传递到子任务,只读、不可变、生命周期明确。它替代 ThreadLocal 在虚拟线程海量场景下的内存膨胀问题,并支持与结构化并发组合:父作用域的值在子任务中可见,任务结束自动释放。适合传递请求 ID、用户、配置等不可变上下文,是虚拟线程时代推荐的方式。

Scoped Values 是虚拟线程的上下文利器,作用域绑定、不可变、自动传递,替代 ThreadLocal。

#

48. Thread.holdsLock(obj) 的真实用途

Thread.holdsLock(obj) 的真实用途是什么?

  • holdsLock 语义
  • 用途
  • 断言

Thread.holdsLock(obj) 判断当前线程是否持有 obj 的监视器锁,返回 boolean。真实用途:1) 断言/校验:在工具方法中要求调用方已持锁(assert Thread.holdsLock(this)),保护不变量;2) 调试:验证锁是否被正确持有;3) 条件化逻辑:仅在持锁时执行某操作。它不用于获取锁,只用于"检查是否持锁"。JDK 内部(如集合的同步包装)用它做一致性校验。

holdsLock 是"检查持锁"的断言工具,用于保护不变量与调试,非锁获取。

#

49. CPU 密集/IO 密集任务的线程数经验公式与上下文切换成本的关系

CPU 密集/IO 密集任务的线程数经验公式与上下文切换成本的关系是什么?

  • 线程数公式
  • 上下文切换
  • 关系

经验公式:CPU 密集任务线程数 ≈ CPU 核数 + 1(避免切换开销);IO 密集任务线程数 ≈ 核数 × (1 + 等待时间/执行时间),即核数 × (1 + IO 时间占比)。核心是"上下文切换成本":线程过多时,切换开销和缓存失效增加,吞吐下降。CPU 密集线程数应接近核数(切换无意义);IO 密集线程数可多(等待时让出 CPU,切换换来并发挥)。虚拟线程出现后,IO 密集无需精确调线程数(虚拟线程廉价),公式仍适用于平台线程池。

线程数公式源于"上下文切换成本与并发的平衡"。CPU 密集≈核数,IO 密集按等待比放大,虚拟线程简化 IO 密集。