深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)第十三章知识点问答(15题)

预告下一本 可能是mysql8+的书籍 或者是AI应用工程的基本岗位所有技能

问题1

什么是 线程安全?在 Java 中如何定义"线程安全"?


线程安全(Thread Safety) 的定义是:

  • 当多个线程同时访问某个类的对象时,无论运行环境如何调度这些线程、无论这些线程如何交替执行 ,并且在主调代码中不需要任何额外的同步或协调操作,这个类都能表现出正确的行为。

Java 中,线程安全意味着:

  • 对外暴露的方法调用都是 原子性 的。
  • 并发访问时不会导致 数据不一致状态破坏异常行为
  • 典型例子:java.util.VectorHashtableConcurrentHashMap 等线程安全类。

👉 换句话说:线程安全的本质 = 行为一致性,不随并发环境变化而变化。


问题2

作者将"线程安全"划分为不同的 粒度层次 。请问有哪些分类?并简要说明它们的区别。

你提到的"无锁、偏向锁、轻量级锁、重量级锁" 👍,这其实是 JVM 在实现 synchronized 时的锁状态划分和优化路径 (属于 锁优化 的部分)。


线程安全的分类(语义层次)

作者把"线程安全"从强到弱分为以下几类:

  1. 不可变(Immutable)

    • 对象一旦创建,其状态就不会改变。
    • 典型例子:StringIntegerLong 等包装类。
    • 天然线程安全。
  2. 绝对线程安全

    • 满足严格定义:类在 任意调用场景下 都是线程安全的。
    • 需要在所有方法调用上都进行同步。
    • 代价极高,Java 标准库几乎没有完全绝对线程安全的类。
  3. 相对线程安全(常见的"线程安全"定义)

    • Java 大多数线程安全类遵循这一标准。
    • 在特定条件下能保证正确行为,比如 VectorHashtableCollections.synchronizedXXX()
    • 通常指 单个方法是原子安全的,但复合调用(如"先检查再更新")仍需额外同步。
  4. 线程兼容(Thread-Compatible)

    • 类本身不是线程安全的,但只要 调用方在外部进行正确同步,就能在多线程环境中安全使用。
    • 典型例子:ArrayListHashMap
  5. 线程对立(Thread-Hostile)

    • 类在并发环境下完全无法使用,即使调用方进行外部同步也无济于事。
    • 很少见,多出现在不规范的第三方代码中。

问题3

互斥同步(Mutual Exclusion Synchronization)的基本原理是什么?在 Java 中有哪些典型实现?


  1. 基本原理

    • 互斥同步的核心是:同一时刻只允许一个线程进入临界区,访问共享资源。
    • 实现方式通常是:当一个线程获取锁标记后,其他线程必须 阻塞,直到锁被释放。
    • 这种同步机制保证了 互斥性(Mutual Exclusion),从而避免数据竞争。
  2. 在 Java 中的典型实现

    • synchronized

      • Java 内置关键字,编译后会生成 monitorenter / monitorexit 字节码指令。
      • 由 JVM 保证 原子性、可见性、有序性
    • ReentrantLock(显示锁)

      • 位于 java.util.concurrent.locks 包。
      • 功能更丰富,比如可定时尝试获取锁、公平/非公平策略、支持条件变量。
    • 其它并发工具

      • Semaphore(信号量)、CountDownLatchCyclicBarrier 等,也都基于"互斥/同步"思想。
  3. 特点

    • 优点:实现简单,语义清晰,保证严格的互斥访问。
    • 缺点:可能导致线程 阻塞/唤醒切换开销大,在高并发场景下性能不理想。

👉 总结:

互斥同步的本质是 "抢占标记 → 独占访问 → 释放标记" ,在 Java 中主要通过 synchronizedLock 系列实现。


好的 👍,那我继续自动抛出下一题。


问题4

非阻塞同步(基于 CAS)的基本原理是什么?它的优点和缺点是什么?


  1. 基本原理

    • CAS(Compare-And-Swap)是一种 非阻塞同步机制。

    • 操作过程:

      1. 比较内存中的值是否与预期值一致;
      2. 如果一致,就更新为新值;
      3. 如果不一致,说明有竞争,操作失败,通常会 自旋重试
    • 整个过程由 CPU 提供的原子指令保证,不会出现"更新一半被打断"。

  2. 优点

    • 无阻塞:线程不会因为获取锁失败而挂起,而是立即返回/重试。
    • 高性能:在竞争不激烈时,性能远优于锁(避免上下文切换)。
    • 原子性保证:由硬件指令直接支持。
  3. 缺点

    • ABA 问题:值从 A 改为 B,再改回 A,CAS 会误认为值没变。

      • 解决方法:加版本号(如 AtomicStampedReference)。
    • 自旋开销大:在高并发或竞争激烈时,CAS 可能长时间重试,反而比锁更耗性能。

    • 只能保证单个变量的原子性:要实现复合操作(如多个共享变量的一致更新)需要额外机制。

👉 总结:

CAS 属于 乐观并发控制 ,适合"读多写少、竞争不激烈"的场景。它是 Java 并发包(AtomicIntegerConcurrentHashMap)等的核心实现手段。


问题5

无同步方案 通常适用于哪类场景?请举例说明。


无同步方案 的核心思想是:

  • 当多个线程之间 不存在共享数据 ,或即使存在也 不需要严格同步 时,可以完全不使用任何同步手段。

典型适用场景:

  1. 可重入代码(Reentrant Code)

    • 又叫纯代码(Pure Code),方法内部没有引用任何共享数据,输出只依赖输入参数。
    • 例如数学函数:Math.sin()Math.sqrt()
  2. 线程本地存储(Thread Local Storage)

    • 每个线程维护独立的副本,互不影响。
    • Java 提供了 ThreadLocal 工具类。
  3. 局部变量

    • 定义在方法体中的局部变量存放在栈帧上,每个线程有独立的栈,天然线程安全。
  4. 只读共享数据

    • 数据在初始化后不再改变,多个线程只读而不写。
    • 比如配置常量、不可变对象(StringInteger)。

👉 总结:

无同步方案适合 数据不会竞争 的情况,它让代码更高效,避免了不必要的锁和 CAS 开销。


问题6

Java 内置的哪些关键字和工具可以用来保证线程安全?请至少列出三种,并简要说明作用。


  1. synchronized :内置监视器锁,提供互斥 + 可见性 + 有序性(编译为 monitorenter/monitorexit)。
  2. volatile:提供可见性和禁止重排序(不保证复合操作原子性)。
  3. java.util.concurrent.locks.Lock / ReentrantLock:显式锁,支持可中断/可定时/公平锁、条件变量等高级特性。
  4. 原子类(AtomicInteger 等) :基于 CAS 的非阻塞同步,保证单变量原子更新。
  5. 并发容器ConcurrentHashMapConcurrentLinkedQueue 等,封装了内部同步/无锁算法。
  6. 同步器SemaphoreCountDownLatchCyclicBarrierPhaserExchanger(多样化协作/限流)。
  7. 读写锁 / 票据锁ReadWriteLockStampedLock(乐观/悲观读、写)。
  8. ThreadLocal:线程本地存储,绕过共享从而"无同步"。
  9. final(发布语义):构造期的写入有特殊的安全发布语义(配合 JMM)。

问题7

synchronized 的基本语义是什么?它在字节码层面如何体现?(提示:两个字节码指令;保证哪三种内存语义?)


  1. 基本语义

    • synchronized 是 Java 内置的同步机制,具有 互斥性(排他性):同一时间只能有一个线程进入同步块。

    • 内存语义上,它保证:

      • 原子性:同步块内的操作不可被其他线程打断。
      • 可见性:进入同步块前会清空工作内存,从主内存加载最新值;退出时会把更新刷新到主内存。
      • 有序性:加锁与解锁之间的代码不会被重排序。
  2. 字节码层面实现

    • synchronized 在编译后会在同步代码块前后插入两条字节码指令:

      • monitorenter:获取对象的监视器锁。
      • monitorexit:释放监视器锁。
    • JVM 在执行这些指令时,会触发锁优化机制(偏向锁 → 轻量级锁 → 重量级锁)。

  3. JVM 层面的优化

    • 现代 HotSpot 会对 synchronized 做优化:偏向锁、轻量级锁、自旋锁、锁粗化、锁消除等。
    • 在底层实现上,确实会用到 CPU 的 LOCK 前缀指令 + 内存屏障,保证硬件层面的可见性与顺序性。

👉 总结:

  • 语义:互斥 + 原子性 + 可见性 + 有序性。
  • 字节码monitorenter / monitorexit
  • 底层:依赖 CPU 原子指令和内存屏障。

问题8

ReentrantLock 与 synchronized 相比,有哪些额外特性?


ReentrantLock 与 synchronized 的区别/额外特性:

  1. 相同点

    • 都是 可重入锁:同一个线程可以多次获取同一把锁,不会死锁。
  2. ReentrantLock 的额外特性:

    • 可中断锁 :可以调用 lockInterruptibly(),在等待锁时能响应中断。
    • 可定时锁 :调用 tryLock(long time, TimeUnit unit),在超时后自动放弃等待。
    • 公平锁 / 非公平锁:可以指定是否采用 FIFO 获取锁,避免线程"饿死"。
    • 多个条件变量 :通过 newCondition() 可以为不同等待队列设置不同的唤醒条件,比 synchronized 的 wait/notify 更灵活。
    • 显式获取/释放 :必须手动调用 lock()unlock(),更灵活,但也更容易出错。
  3. synchronized 的特点:

    • JVM 内置支持,语法层面保证,简单易用。
    • 自动释放锁(代码块退出时自动解锁,不易忘记)。
    • 锁优化:偏向锁、轻量级锁、自旋锁、重量级锁,升级路径透明给开发者。

👉 总结:

  • synchronized:简单、语法级,自动释放锁。
  • ReentrantLock:功能更强大,可中断、可定时、公平锁、多条件队列,更灵活,但需手动释放锁。

问题9

什么是 自旋锁(Spin Lock)?它解决了什么问题?


  1. 自旋锁的定义

    • 当线程尝试获取锁失败时,不是立即进入阻塞状态,而是在 用户态循环等待(自旋),不断重试是否能获取锁。
    • 如果在一定次数/时间内仍然失败,就会 升级为重量级锁(阻塞/排队)。
  2. 它解决的问题

    • 传统阻塞锁(重量级锁)一旦获取失败,就会进入 内核态阻塞 ,唤醒时需要 用户态 ↔ 内核态切换,开销大。
    • 如果锁 只会短暂被持有,那么切换上下文的成本甚至比等锁的时间还高。
    • 自旋锁通过"原地等待",避免了短时间内的上下文切换开销。
  3. 特点

    • 优点 :适合 锁占用时间非常短 的场景(比如几十纳秒到几微秒)。
    • 缺点:会浪费 CPU,如果自旋时间过长,会导致性能下降。
    • HotSpot 的改进 :引入 适应性自旋(Adaptive Spinning),根据线程状态和历史锁竞争情况动态决定是否自旋。

👉 总结:
自旋锁 = 用 CPU 忙等换取避免阻塞/唤醒的开销,是 synchronized 锁优化的一部分。


问题10

什么是 锁消除(Lock Elimination)?它的原理是什么?


  1. 定义

    • 锁消除(Lock Elimination)是 即时编译器(JIT) 在运行时的一项优化。
    • 如果代码中出现了同步块,但经过 逃逸分析 发现,这些对象不会被多个线程共享,也就不存在竞争,那么这些同步操作会被直接消除。
  2. 原理

    • 逃逸分析用于判断对象是否会逃出方法/线程。
    • 若对象完全不会逃出线程作用域(即线程私有),那么它上的锁就是无意义的,可以直接去掉。
  3. 示例

    java 复制代码
    public String concat(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }
    • StringBuffer 的方法带有 synchronized,但这里 sb 是局部变量,绝对不会被多线程共享。
    • JIT 会 锁消除,直接去掉 synchronized,提升性能。
  4. 效果

    • 减少无用的加锁/解锁操作。
    • 提升性能,尤其在高频调用场景中。

👉 总结:
锁消除 = 通过逃逸分析发现"无竞争锁" → 删除无意义的同步


问题11

什么是 锁粗化(Lock Coarsening)?它解决了什么问题?


  1. 定义

    • 锁粗化(Lock Coarsening) 是 JVM 的一种优化。
    • 如果 JIT 发现一系列 连续的加锁/解锁操作作用在同一个对象上,就会把这些零碎的锁合并成一个更大范围的锁。
  2. 为什么需要?

    • 按照常规推荐,锁粒度应尽量小,以减少锁持有时间。
    • 但如果多个同步块连续执行,每次都要 频繁加锁/解锁,反而带来额外开销。
    • 这种情况下,粗化锁的范围可以减少反复申请和释放锁的成本。
  3. 示例

    java 复制代码
    for (int i = 0; i < 100; i++) {
        synchronized (obj) {
            // 只有很小的操作
            sum++;
        }
    }
    • 常规写法是 100 次加锁/解锁。
    • JIT 会将这 100 次锁 粗化为一次大的加锁/解锁,把整个循环都包进一个锁中。
  4. 效果

    • 减少频繁加锁/解锁的性能损耗。
    • 牺牲部分并发性(锁范围变大),换取整体性能提升。

👉 总结:

  • 常规建议:同步块要小,减少持有时间。
  • 锁粗化:当多次锁操作紧邻时,JIT 会合并它们,减少频繁申请/释放锁的成本。

问题12

偏向锁、轻量级锁、重量级锁三者的区别是什么?它们的 升级路径 是怎样的?


HotSpot JVM 中,synchronized 的锁实现有三种形态:

  1. 偏向锁(Biased Locking)

    • 设计目的 :优化 无竞争的场景

    • 特点:

      • 第一次获得锁的线程会在对象头记录"偏向线程 ID"。
      • 之后该线程再次进入同步块时,直接判断对象头,无需真正加锁。
      • 几乎 零开销,所以可以看作"锁=没锁"。
    • 触发升级:当 第二个线程尝试获取锁,发现已有偏向线程 ID,就会撤销偏向,升级为轻量级锁。

  2. 轻量级锁(Lightweight Locking)

    • 设计目的 :优化 低竞争场景

    • 特点:

      • 通过 CAS 操作尝试在对象头设置锁记录。
      • 如果成功,线程进入临界区。
      • 如果 CAS 失败,表示有竞争 → 线程会 自旋 等待持有锁的线程释放。
    • 优点:避免了线程阻塞/唤醒的高开销。

    • 缺点:自旋消耗 CPU。

    • 触发升级:如果竞争严重,自旋失败,锁会膨胀为重量级锁。

  3. 重量级锁(Heavyweight Locking)

    • 设计目的 :处理 高竞争场景

    • 特点:

      • 线程获取不到锁时会进入 阻塞状态(操作系统层面),等待唤醒。
      • 保证安全性,但有 内核态/用户态切换 的高开销。
    • 适合锁竞争非常激烈的场景。


升级路径

偏向锁 → 轻量级锁 → 重量级锁

  • 锁只能 升级,不会降级
  • 这是为了避免锁频繁转换带来的复杂性和额外开销。

👉 总结:

  • 偏向锁:无竞争优化,几乎零成本。
  • 轻量级锁:低竞争优化,CAS + 自旋。
  • 重量级锁:高竞争场景,阻塞/唤醒。
  • 升级路径:偏向 → 轻量级 → 重量级(不可逆)。

下一题(问题13)

什么是 适应性自旋(Adaptive Spinning)?它相比固定自旋有什么优势?


  1. 自旋的背景

    • 轻量级锁获取失败时,线程不会马上阻塞,而是进入 自旋(忙等)状态,尝试再次获取锁。
    • 固定自旋:早期 JVM 使用固定次数(例如 10 次)循环尝试获取锁。
  2. 适应性自旋(Adaptive Spinning)

    • 自旋次数 不再固定,而是由 JVM 根据运行时情况动态调整。

    • 例如:

      • 如果某个锁的持有线程 刚刚释放过锁,说明锁等待时间可能短,可以多自旋几次。
      • 如果某个线程在之前的自旋中 几乎没成功过,则减少或放弃自旋,直接阻塞。
  3. 优势

    • 节省开销:避免无意义的长时间自旋,降低 CPU 消耗。
    • 提升性能:在锁竞争较轻时,减少线程挂起/唤醒的切换开销。
    • 更智能:自旋次数动态适配应用场景,而不是一刀切。

👉 总结:
适应性自旋 = JVM 根据历史成功经验和锁的持有情况,动态调整自旋时间。
优点:比固定自旋更智能,能在轻度竞争时提升性能,在重度竞争时减少 CPU 浪费。


问题14

锁优化技术的总体目标是什么?为什么说它们的核心思想是"减少获得锁的代价"?


  1. 总体目标

    • 锁优化技术的根本目标是:

      • 减少获取和释放锁的性能开销,提高程序在并发情况下的吞吐量。
    • 换句话说:让 synchronized 这种看似"重量级"的操作,在大多数场景下跑得轻快。

  2. 为什么要减少代价?

    • 传统重量级锁:竞争失败就会触发线程阻塞/唤醒,需要内核态和用户态切换 → 开销大。
    • 优化手段:通过自旋、偏向锁、轻量级锁、锁消除、锁粗化等方式,尽量避免线程进入阻塞状态。
  3. 核心思想

    • 减少上下文切换(用户态 ↔ 内核态)。
    • 避免不必要的同步(锁消除)。
    • 合并分散锁操作(锁粗化)。
    • 利用无竞争场景优化(偏向锁)。
    • 低竞争场景用忙等代替阻塞(轻量级锁 + 自旋)。

👉 总结:
锁优化的核心思想 = 减少获得锁的代价,即让锁尽量避免走到"重量级"阻塞/唤醒的路径,而是通过各种手段(偏向、轻量、自旋、消除、粗化)在用户态内完成,大幅降低 CPU 和线程调度的开销。


问题15

请总结 第 13 章《线程安全与锁优化》 的主要知识点:

  • 线程安全的定义与分类
  • 互斥同步 / 非阻塞同步 / 无同步方案
  • Java 提供的关键工具
  • JVM 的锁优化技术(自旋、适应性自旋、锁消除、锁粗化、偏向锁、轻量级锁、重量级锁)

按照保证程度,线程安全可以分为:

  • 不可变(Immutable) :如 String,天生线程安全;
  • 绝对线程安全:完全符合严格定义,但开销极大;
  • 相对线程安全 :常见,保证单次方法调用安全,如 Vector
  • 线程兼容 :类本身非线程安全,需要调用方加同步,如 ArrayList
  • 线程对立:不管怎样都无法在并发场景中正确使用。

实现线程安全的手段主要有三类:

  1. 互斥同步 :最传统的方法,通过锁来保证临界区互斥访问,典型实现是 synchronizedReentrantLock
  2. 非阻塞同步:通过 CAS 等硬件原子指令完成更新,失败时自旋重试,避免线程阻塞。
  3. 无同步方案 :在没有共享数据或无需同步时,直接避免锁,比如局部变量、ThreadLocal、不可变对象。

Java 提供了多种工具来实现线程安全,包括:

  • 关键字:synchronizedvolatilefinal(安全发布语义);
  • 并发包:LockAtomic 原子类、并发容器(ConcurrentHashMap)、同步器(SemaphoreCountDownLatch);
  • ThreadLocal 等无同步手段。

在 JVM 内部,HotSpot 对锁做了多种优化以减少性能开销:

  • 自旋锁:在短时间内忙等而不阻塞,避免上下文切换。
  • 适应性自旋:根据历史和环境动态调整自旋次数。
  • 锁消除:利用逃逸分析去掉无意义的同步。
  • 锁粗化:合并多个连续的加锁/解锁操作。
  • 偏向锁:优化无竞争场景,几乎零成本。
  • 轻量级锁:适合低竞争场景,基于 CAS + 自旋。
  • 重量级锁:高竞争场景下的最终方案,线程阻塞/唤醒。

相关推荐
Scott9999HH2 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔2 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社3 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海3 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖4 小时前
JDK 26 新特性详解
java·开发语言
马优晨5 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区7 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大7 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json