深入理解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 + 自旋。
  • 重量级锁:高竞争场景下的最终方案,线程阻塞/唤醒。

相关推荐
Java进阶笔记2 小时前
JVM默认栈大小
java·jvm·后端
shan&cen2 小时前
Day04 前缀和&差分 1109. 航班预订统计 、304. 二维区域和检索 - 矩阵不可变
java·数据结构·算法
在线教学养猪2 小时前
Spring Task
java·后端·spring
_hermit:2 小时前
【从零开始java学习|小结】记录学习和编程中的问题
java·学习
小柯J桑_2 小时前
C++之特殊类设计
java·开发语言·c++
菜鸟plus+3 小时前
Java NIO
java·nio
君不见,青丝成雪3 小时前
Java中IntStream的详细用法及典型案例
java
QiZhang | UESTC3 小时前
JAVA算法练习题day11
java·开发语言·python·算法·hot100
FenceRain3 小时前
spring boot 拦截器增加语言信息
java·spring boot·后端