预告下一本 可能是mysql8+的书籍 或者是AI应用工程的基本岗位所有技能
问题1
什么是 线程安全?在 Java 中如何定义"线程安全"?
线程安全(Thread Safety) 的定义是:
- 当多个线程同时访问某个类的对象时,无论运行环境如何调度这些线程、无论这些线程如何交替执行 ,并且在主调代码中不需要任何额外的同步或协调操作,这个类都能表现出正确的行为。
在 Java 中,线程安全意味着:
- 对外暴露的方法调用都是 原子性 的。
- 并发访问时不会导致 数据不一致 、状态破坏 或 异常行为。
- 典型例子:
java.util.Vector
、Hashtable
、ConcurrentHashMap
等线程安全类。
👉 换句话说:线程安全的本质 = 行为一致性,不随并发环境变化而变化。
问题2
作者将"线程安全"划分为不同的 粒度层次 。请问有哪些分类?并简要说明它们的区别。
你提到的"无锁、偏向锁、轻量级锁、重量级锁" 👍,这其实是 JVM 在实现 synchronized 时的锁状态划分和优化路径 (属于 锁优化 的部分)。
线程安全的分类(语义层次)
作者把"线程安全"从强到弱分为以下几类:
-
不可变(Immutable)
- 对象一旦创建,其状态就不会改变。
- 典型例子:
String
、Integer
、Long
等包装类。 - 天然线程安全。
-
绝对线程安全
- 满足严格定义:类在 任意调用场景下 都是线程安全的。
- 需要在所有方法调用上都进行同步。
- 代价极高,Java 标准库几乎没有完全绝对线程安全的类。
-
相对线程安全(常见的"线程安全"定义)
- Java 大多数线程安全类遵循这一标准。
- 在特定条件下能保证正确行为,比如
Vector
、Hashtable
、Collections.synchronizedXXX()
。 - 通常指 单个方法是原子安全的,但复合调用(如"先检查再更新")仍需额外同步。
-
线程兼容(Thread-Compatible)
- 类本身不是线程安全的,但只要 调用方在外部进行正确同步,就能在多线程环境中安全使用。
- 典型例子:
ArrayList
、HashMap
。
-
线程对立(Thread-Hostile)
- 类在并发环境下完全无法使用,即使调用方进行外部同步也无济于事。
- 很少见,多出现在不规范的第三方代码中。
问题3
互斥同步(Mutual Exclusion Synchronization)的基本原理是什么?在 Java 中有哪些典型实现?
-
基本原理
- 互斥同步的核心是:同一时刻只允许一个线程进入临界区,访问共享资源。
- 实现方式通常是:当一个线程获取锁标记后,其他线程必须 阻塞,直到锁被释放。
- 这种同步机制保证了 互斥性(Mutual Exclusion),从而避免数据竞争。
-
在 Java 中的典型实现
-
synchronized
- Java 内置关键字,编译后会生成
monitorenter
/monitorexit
字节码指令。 - 由 JVM 保证 原子性、可见性、有序性。
- Java 内置关键字,编译后会生成
-
ReentrantLock
(显示锁)- 位于
java.util.concurrent.locks
包。 - 功能更丰富,比如可定时尝试获取锁、公平/非公平策略、支持条件变量。
- 位于
-
其它并发工具
Semaphore
(信号量)、CountDownLatch
、CyclicBarrier
等,也都基于"互斥/同步"思想。
-
-
特点
- 优点:实现简单,语义清晰,保证严格的互斥访问。
- 缺点:可能导致线程 阻塞/唤醒切换开销大,在高并发场景下性能不理想。
👉 总结:
互斥同步的本质是 "抢占标记 → 独占访问 → 释放标记" ,在 Java 中主要通过 synchronized
和 Lock
系列实现。
好的 👍,那我继续自动抛出下一题。
问题4
非阻塞同步(基于 CAS)的基本原理是什么?它的优点和缺点是什么?
-
基本原理
-
CAS(Compare-And-Swap)是一种 非阻塞同步机制。
-
操作过程:
- 比较内存中的值是否与预期值一致;
- 如果一致,就更新为新值;
- 如果不一致,说明有竞争,操作失败,通常会 自旋重试。
-
整个过程由 CPU 提供的原子指令保证,不会出现"更新一半被打断"。
-
-
优点
- 无阻塞:线程不会因为获取锁失败而挂起,而是立即返回/重试。
- 高性能:在竞争不激烈时,性能远优于锁(避免上下文切换)。
- 原子性保证:由硬件指令直接支持。
-
缺点
-
ABA 问题:值从 A 改为 B,再改回 A,CAS 会误认为值没变。
- 解决方法:加版本号(如
AtomicStampedReference
)。
- 解决方法:加版本号(如
-
自旋开销大:在高并发或竞争激烈时,CAS 可能长时间重试,反而比锁更耗性能。
-
只能保证单个变量的原子性:要实现复合操作(如多个共享变量的一致更新)需要额外机制。
-
👉 总结:
CAS 属于 乐观并发控制 ,适合"读多写少、竞争不激烈"的场景。它是 Java 并发包(AtomicInteger
、ConcurrentHashMap
)等的核心实现手段。
问题5
无同步方案 通常适用于哪类场景?请举例说明。
无同步方案 的核心思想是:
- 当多个线程之间 不存在共享数据 ,或即使存在也 不需要严格同步 时,可以完全不使用任何同步手段。
典型适用场景:
-
可重入代码(Reentrant Code)
- 又叫纯代码(Pure Code),方法内部没有引用任何共享数据,输出只依赖输入参数。
- 例如数学函数:
Math.sin()
、Math.sqrt()
。
-
线程本地存储(Thread Local Storage)
- 每个线程维护独立的副本,互不影响。
- Java 提供了
ThreadLocal
工具类。
-
局部变量
- 定义在方法体中的局部变量存放在栈帧上,每个线程有独立的栈,天然线程安全。
-
只读共享数据
- 数据在初始化后不再改变,多个线程只读而不写。
- 比如配置常量、不可变对象(
String
、Integer
)。
👉 总结:
无同步方案适合 数据不会竞争 的情况,它让代码更高效,避免了不必要的锁和 CAS 开销。
问题6
Java 内置的哪些关键字和工具可以用来保证线程安全?请至少列出三种,并简要说明作用。
synchronized
:内置监视器锁,提供互斥 + 可见性 + 有序性(编译为monitorenter/monitorexit
)。volatile
:提供可见性和禁止重排序(不保证复合操作原子性)。java.util.concurrent.locks.Lock
/ReentrantLock
:显式锁,支持可中断/可定时/公平锁、条件变量等高级特性。- 原子类(
AtomicInteger
等) :基于 CAS 的非阻塞同步,保证单变量原子更新。 - 并发容器 :
ConcurrentHashMap
、ConcurrentLinkedQueue
等,封装了内部同步/无锁算法。 - 同步器 :
Semaphore
、CountDownLatch
、CyclicBarrier
、Phaser
、Exchanger
(多样化协作/限流)。 - 读写锁 / 票据锁 :
ReadWriteLock
、StampedLock
(乐观/悲观读、写)。 ThreadLocal
:线程本地存储,绕过共享从而"无同步"。final
(发布语义):构造期的写入有特殊的安全发布语义(配合 JMM)。
问题7
synchronized
的基本语义是什么?它在字节码层面如何体现?(提示:两个字节码指令;保证哪三种内存语义?)
-
基本语义
-
synchronized
是 Java 内置的同步机制,具有 互斥性(排他性):同一时间只能有一个线程进入同步块。 -
在 内存语义上,它保证:
- 原子性:同步块内的操作不可被其他线程打断。
- 可见性:进入同步块前会清空工作内存,从主内存加载最新值;退出时会把更新刷新到主内存。
- 有序性:加锁与解锁之间的代码不会被重排序。
-
-
字节码层面实现
-
synchronized
在编译后会在同步代码块前后插入两条字节码指令:monitorenter
:获取对象的监视器锁。monitorexit
:释放监视器锁。
-
JVM 在执行这些指令时,会触发锁优化机制(偏向锁 → 轻量级锁 → 重量级锁)。
-
-
JVM 层面的优化
- 现代 HotSpot 会对 synchronized 做优化:偏向锁、轻量级锁、自旋锁、锁粗化、锁消除等。
- 在底层实现上,确实会用到 CPU 的
LOCK
前缀指令 + 内存屏障,保证硬件层面的可见性与顺序性。
👉 总结:
- 语义:互斥 + 原子性 + 可见性 + 有序性。
- 字节码 :
monitorenter
/monitorexit
。 - 底层:依赖 CPU 原子指令和内存屏障。
问题8
ReentrantLock 与 synchronized 相比,有哪些额外特性?
ReentrantLock 与 synchronized 的区别/额外特性:
-
相同点
- 都是 可重入锁:同一个线程可以多次获取同一把锁,不会死锁。
-
ReentrantLock 的额外特性:
- 可中断锁 :可以调用
lockInterruptibly()
,在等待锁时能响应中断。 - 可定时锁 :调用
tryLock(long time, TimeUnit unit)
,在超时后自动放弃等待。 - 公平锁 / 非公平锁:可以指定是否采用 FIFO 获取锁,避免线程"饿死"。
- 多个条件变量 :通过
newCondition()
可以为不同等待队列设置不同的唤醒条件,比 synchronized 的wait/notify
更灵活。 - 显式获取/释放 :必须手动调用
lock()
和unlock()
,更灵活,但也更容易出错。
- 可中断锁 :可以调用
-
synchronized 的特点:
- JVM 内置支持,语法层面保证,简单易用。
- 自动释放锁(代码块退出时自动解锁,不易忘记)。
- 锁优化:偏向锁、轻量级锁、自旋锁、重量级锁,升级路径透明给开发者。
👉 总结:
- synchronized:简单、语法级,自动释放锁。
- ReentrantLock:功能更强大,可中断、可定时、公平锁、多条件队列,更灵活,但需手动释放锁。
问题9
什么是 自旋锁(Spin Lock)?它解决了什么问题?
-
自旋锁的定义
- 当线程尝试获取锁失败时,不是立即进入阻塞状态,而是在 用户态循环等待(自旋),不断重试是否能获取锁。
- 如果在一定次数/时间内仍然失败,就会 升级为重量级锁(阻塞/排队)。
-
它解决的问题
- 传统阻塞锁(重量级锁)一旦获取失败,就会进入 内核态阻塞 ,唤醒时需要 用户态 ↔ 内核态切换,开销大。
- 如果锁 只会短暂被持有,那么切换上下文的成本甚至比等锁的时间还高。
- 自旋锁通过"原地等待",避免了短时间内的上下文切换开销。
-
特点
- 优点 :适合 锁占用时间非常短 的场景(比如几十纳秒到几微秒)。
- 缺点:会浪费 CPU,如果自旋时间过长,会导致性能下降。
- HotSpot 的改进 :引入 适应性自旋(Adaptive Spinning),根据线程状态和历史锁竞争情况动态决定是否自旋。
👉 总结:
自旋锁 = 用 CPU 忙等换取避免阻塞/唤醒的开销,是 synchronized 锁优化的一部分。
问题10
什么是 锁消除(Lock Elimination)?它的原理是什么?
-
定义
- 锁消除(Lock Elimination)是 即时编译器(JIT) 在运行时的一项优化。
- 如果代码中出现了同步块,但经过 逃逸分析 发现,这些对象不会被多个线程共享,也就不存在竞争,那么这些同步操作会被直接消除。
-
原理
- 逃逸分析用于判断对象是否会逃出方法/线程。
- 若对象完全不会逃出线程作用域(即线程私有),那么它上的锁就是无意义的,可以直接去掉。
-
示例
javapublic String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
StringBuffer
的方法带有 synchronized,但这里 sb 是局部变量,绝对不会被多线程共享。- JIT 会 锁消除,直接去掉 synchronized,提升性能。
-
效果
- 减少无用的加锁/解锁操作。
- 提升性能,尤其在高频调用场景中。
👉 总结:
锁消除 = 通过逃逸分析发现"无竞争锁" → 删除无意义的同步。
问题11
什么是 锁粗化(Lock Coarsening)?它解决了什么问题?
-
定义
- 锁粗化(Lock Coarsening) 是 JVM 的一种优化。
- 如果 JIT 发现一系列 连续的加锁/解锁操作作用在同一个对象上,就会把这些零碎的锁合并成一个更大范围的锁。
-
为什么需要?
- 按照常规推荐,锁粒度应尽量小,以减少锁持有时间。
- 但如果多个同步块连续执行,每次都要 频繁加锁/解锁,反而带来额外开销。
- 这种情况下,粗化锁的范围可以减少反复申请和释放锁的成本。
-
示例
javafor (int i = 0; i < 100; i++) { synchronized (obj) { // 只有很小的操作 sum++; } }
- 常规写法是 100 次加锁/解锁。
- JIT 会将这 100 次锁 粗化为一次大的加锁/解锁,把整个循环都包进一个锁中。
-
效果
- 减少频繁加锁/解锁的性能损耗。
- 牺牲部分并发性(锁范围变大),换取整体性能提升。
👉 总结:
- 常规建议:同步块要小,减少持有时间。
- 锁粗化:当多次锁操作紧邻时,JIT 会合并它们,减少频繁申请/释放锁的成本。
问题12
偏向锁、轻量级锁、重量级锁三者的区别是什么?它们的 升级路径 是怎样的?
在 HotSpot JVM 中,synchronized 的锁实现有三种形态:
-
偏向锁(Biased Locking)
-
设计目的 :优化 无竞争的场景。
-
特点:
- 第一次获得锁的线程会在对象头记录"偏向线程 ID"。
- 之后该线程再次进入同步块时,直接判断对象头,无需真正加锁。
- 几乎 零开销,所以可以看作"锁=没锁"。
-
触发升级:当 第二个线程尝试获取锁,发现已有偏向线程 ID,就会撤销偏向,升级为轻量级锁。
-
-
轻量级锁(Lightweight Locking)
-
设计目的 :优化 低竞争场景。
-
特点:
- 通过 CAS 操作尝试在对象头设置锁记录。
- 如果成功,线程进入临界区。
- 如果 CAS 失败,表示有竞争 → 线程会 自旋 等待持有锁的线程释放。
-
优点:避免了线程阻塞/唤醒的高开销。
-
缺点:自旋消耗 CPU。
-
触发升级:如果竞争严重,自旋失败,锁会膨胀为重量级锁。
-
-
重量级锁(Heavyweight Locking)
-
设计目的 :处理 高竞争场景。
-
特点:
- 线程获取不到锁时会进入 阻塞状态(操作系统层面),等待唤醒。
- 保证安全性,但有 内核态/用户态切换 的高开销。
-
适合锁竞争非常激烈的场景。
-
升级路径
偏向锁 → 轻量级锁 → 重量级锁
- 锁只能 升级,不会降级。
- 这是为了避免锁频繁转换带来的复杂性和额外开销。
👉 总结:
- 偏向锁:无竞争优化,几乎零成本。
- 轻量级锁:低竞争优化,CAS + 自旋。
- 重量级锁:高竞争场景,阻塞/唤醒。
- 升级路径:偏向 → 轻量级 → 重量级(不可逆)。
下一题(问题13)
什么是 适应性自旋(Adaptive Spinning)?它相比固定自旋有什么优势?
-
自旋的背景
- 轻量级锁获取失败时,线程不会马上阻塞,而是进入 自旋(忙等)状态,尝试再次获取锁。
- 固定自旋:早期 JVM 使用固定次数(例如 10 次)循环尝试获取锁。
-
适应性自旋(Adaptive Spinning)
-
自旋次数 不再固定,而是由 JVM 根据运行时情况动态调整。
-
例如:
- 如果某个锁的持有线程 刚刚释放过锁,说明锁等待时间可能短,可以多自旋几次。
- 如果某个线程在之前的自旋中 几乎没成功过,则减少或放弃自旋,直接阻塞。
-
-
优势
- 节省开销:避免无意义的长时间自旋,降低 CPU 消耗。
- 提升性能:在锁竞争较轻时,减少线程挂起/唤醒的切换开销。
- 更智能:自旋次数动态适配应用场景,而不是一刀切。
👉 总结:
适应性自旋 = JVM 根据历史成功经验和锁的持有情况,动态调整自旋时间。
优点:比固定自旋更智能,能在轻度竞争时提升性能,在重度竞争时减少 CPU 浪费。
问题14
锁优化技术的总体目标是什么?为什么说它们的核心思想是"减少获得锁的代价"?
-
总体目标
-
锁优化技术的根本目标是:
- 减少获取和释放锁的性能开销,提高程序在并发情况下的吞吐量。
-
换句话说:让
synchronized
这种看似"重量级"的操作,在大多数场景下跑得轻快。
-
-
为什么要减少代价?
- 传统重量级锁:竞争失败就会触发线程阻塞/唤醒,需要内核态和用户态切换 → 开销大。
- 优化手段:通过自旋、偏向锁、轻量级锁、锁消除、锁粗化等方式,尽量避免线程进入阻塞状态。
-
核心思想
- 减少上下文切换(用户态 ↔ 内核态)。
- 避免不必要的同步(锁消除)。
- 合并分散锁操作(锁粗化)。
- 利用无竞争场景优化(偏向锁)。
- 低竞争场景用忙等代替阻塞(轻量级锁 + 自旋)。
👉 总结:
锁优化的核心思想 = 减少获得锁的代价,即让锁尽量避免走到"重量级"阻塞/唤醒的路径,而是通过各种手段(偏向、轻量、自旋、消除、粗化)在用户态内完成,大幅降低 CPU 和线程调度的开销。
问题15
请总结 第 13 章《线程安全与锁优化》 的主要知识点:
- 线程安全的定义与分类
- 互斥同步 / 非阻塞同步 / 无同步方案
- Java 提供的关键工具
- JVM 的锁优化技术(自旋、适应性自旋、锁消除、锁粗化、偏向锁、轻量级锁、重量级锁)
按照保证程度,线程安全可以分为:
- 不可变(Immutable) :如
String
,天生线程安全; - 绝对线程安全:完全符合严格定义,但开销极大;
- 相对线程安全 :常见,保证单次方法调用安全,如
Vector
; - 线程兼容 :类本身非线程安全,需要调用方加同步,如
ArrayList
; - 线程对立:不管怎样都无法在并发场景中正确使用。
实现线程安全的手段主要有三类:
- 互斥同步 :最传统的方法,通过锁来保证临界区互斥访问,典型实现是
synchronized
和ReentrantLock
。 - 非阻塞同步:通过 CAS 等硬件原子指令完成更新,失败时自旋重试,避免线程阻塞。
- 无同步方案 :在没有共享数据或无需同步时,直接避免锁,比如局部变量、
ThreadLocal
、不可变对象。
Java 提供了多种工具来实现线程安全,包括:
- 关键字:
synchronized
、volatile
、final
(安全发布语义); - 并发包:
Lock
、Atomic
原子类、并发容器(ConcurrentHashMap
)、同步器(Semaphore
、CountDownLatch
); ThreadLocal
等无同步手段。
在 JVM 内部,HotSpot 对锁做了多种优化以减少性能开销:
- 自旋锁:在短时间内忙等而不阻塞,避免上下文切换。
- 适应性自旋:根据历史和环境动态调整自旋次数。
- 锁消除:利用逃逸分析去掉无意义的同步。
- 锁粗化:合并多个连续的加锁/解锁操作。
- 偏向锁:优化无竞争场景,几乎零成本。
- 轻量级锁:适合低竞争场景,基于 CAS + 自旋。
- 重量级锁:高竞争场景下的最终方案,线程阻塞/唤醒。