文章目录
- [2. Java多线程进阶:深入synchronized与CAS](#2. Java多线程进阶:深入synchronized与CAS)
-
- 一、无锁编程------CAS (Compare-and-Swap)
-
- [1. 什么是 CAS?一个原子的"比较并交换"操作](#1. 什么是 CAS?一个原子的“比较并交换”操作)
- [2. CAS 的应用](#2. CAS 的应用)
- [3. 深度剖析:CAS 的 ABA 问题及其解决方案](#3. 深度剖析:CAS 的 ABA 问题及其解决方案)
- [二、`synchronized` ------从偏向锁到重量级锁的演进](#二、
synchronized
——从偏向锁到重量级锁的演进) -
- [1. `synchronized` 的多重身份回顾](#1.
synchronized
的多重身份回顾) - [2. 锁升级之路:偏向锁 -> 轻量级锁 -> 重量级锁](#2. 锁升级之路:偏向锁 -> 轻量级锁 -> 重量级锁)
- [3. JVM 的智能优化:锁消除与锁粗化](#3. JVM 的智能优化:锁消除与锁粗化)
- [1. `synchronized` 的多重身份回顾](#1.
- 本篇核心要点总结 (Key Takeaways)
2025-8-13优化记录:
- 补充了CAS的硬件实现原理,重新整理了关于
getAndIncrement
的描述- 增加了ABA问题解决方案的"场景回顾",解释了如何避免ABA问题
- 补充了ABA知识点相关的面试题内容
2. Java多线程进阶:深入synchronized与CAS
在上一篇Java多线程初阶-线程协作与实战案例笔记中,我们从宏观上探讨了各种锁策略的思想。今天,我们将深入到更底层的实现,去探寻两个在现代并发编程中至关重要的知识内容:synchronized
的锁升级机制 和 无锁编程CAS。
CAS (Compare-and-Swap) 是实现"乐观锁"的原子操作,也是JUC(java.util.concurrent
)包中许多高性能类的幕后英雄。而 synchronized
,这个我们最熟悉的关键字,其内部为了追求极致性能而进行的优化和演进,恰恰就是对CAS等思想的应用。可以说,理解了它们,我们才能真正看懂Java并发性能优化的精髓所在。
一、无锁编程------CAS (Compare-and-Swap)
在实际开发中,我们很少直接使用CAS,但它的思想和应用无处不在,是理解JUC并发包许多工具类的关键。
1. 什么是 CAS?一个原子的"比较并交换"操作
CAS,全称 Compare and Swap,顾名思义,就是"比较并交换"。它是一种特殊的原子操作,涉及到三个核心操作数:
- V (Value):内存中要被修改的那个变量的当前值。
- A (Expected):我们"期望"内存中变量的当前值应该是多少。
- B (New):如果期望成真,需要将变量更新成的新值。
整个操作逻辑可以概括为一句话:"我认为 V 应该是 A,如果是,那就把它改成 B。"
它的执行流程如下:
- 比较 :从内存地址
V
中读取当前值,判断它是否与我们的预期值A
相等。 - 交换 :如果相等,说明在我们准备修改的这段时间里,没有其他线程动过这个变量。此时,就放心地将新值
B
写入内存地址V
。 - 返回:无论成功与否,都返回操作结果。
为了更直观地理解,我们可以用一段伪代码来描述这个过程:
java
// CAS伪代码,仅用于理解流程
// 重点:真实的CAS是由一个原子的硬件指令完成的!
boolean CAS(address, expectValue, swapValue) {
// --- 以下所有操作,在硬件层面是一个不可分割的整体 ---
if (内存中address处的值 == expectValue) {
将内存中address处的值更新为swapValue;
return true; // 成功
}
return false; // 失败
// --- 原子操作结束 ---
}
小思考:CAS的原子性到底来自哪里?
看到上面的伪代码,我们很容易产生一个疑问:这个
if
判断再加一个赋值操作,这不就是典型的"Check-and-Set"嘛,它怎么可能是原子的?没错,在软件层面,
if { ... }
这样的结构确实不是原子的,它完全可能在if
判断成功后、赋值操作执行前被其他线程打断。这正是我们在多线程基础部分反复强调的i++
(Read-and-Update)那样的线程不安全操作。CAS的牛逼在于,它并非由软件实现,而是由CPU硬件层面的一条或几条特殊指令直接支持的。CPU保证了这个"读取-比较-写入"的复合操作在执行期间不被任何其他操作打断,从而实现了真正的原子性。
简单来说,Java中的CAS调用链大致是这样的:
- Java代码(如
AtomicInteger
)调用Unsafe
类的CAS方法。Unsafe
类的方法是native
的,它会调用到JVM的C++代码。- JVM会根据不同的操作系统,生成对应的汇编指令,例如在x86架构下就是带有
lock
前缀的cmpxchg
指令。- 最终,由CPU硬件来保障这个操作的原子性。
所以,正是因为有硬件层面的支持,我们才能在软件层面享受到CAS带来的高效并发控制。
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但它并不会阻塞其他线程,其他线程只会收到操作失败的信号。因此,CAS 可以视为是一种乐观锁的实现方式。
2. CAS 的应用
1) 实现原子类
Java标准库 java.util.concurrent.atomic
包下的所有原子类,如 AtomicInteger
、AtomicLong
等,都是基于CAS实现的。它们可以在不使用锁的情况下,保证基本数据类型的操作是线程安全的,性能远高于加锁。

我们知道,像 count++
这样的复合操作在并发环境下是线程不安全的,常规的解决方案就是加 synchronized
锁。但加锁的性能开销较大,而基于CAS的原子类,如 AtomicInteger
,则提供了一种更轻量、更高效的替代方案。
java
// 使用原子类代替int,实现线程安全的计数器
private static AtomicInteger count = new AtomicInteger(0);
// 通过这个原子类就可以简单的解决我们之前多线程部分count++的线程安全问题
public void threadSafeIncrement() {
count.getAndIncrement(); // 内部通过CAS循环实现原子自增
}
getAndIncrement
的内部实现,就是一个典型的"CAS自旋"循环。让我们通过一个多线程场景,来详细拆解它的执行过程:
java
// AtomicInteger.getAndIncrement() 伪代码
public final int getAndIncrement() {
int oldValue;
do {
// 步骤1: 读取主内存中的当前值
oldValue = this.get();
} while (!this.compareAndSet(oldValue, oldValue + 1)); // 步骤2: 循环尝试CAS更新
return oldValue;
}
这个 do-while
循环就是所谓的 "CAS自旋" 。为了彻底搞懂它,让我们来情景重现一下两个线程(线程A、线程B)同时执行 getAndIncrement()
的全过程。
场景设定: count
初始值为 0
。
-
各自读取,心中有数
- 线程A和线程B几乎同时启动,它们都执行
get()
方法,从主内存中读取到count
的值为0
。 - 现在,在它们各自的线程栈里,都有一个局部变量
oldValue
,值都是0
。它们各自的目标都是想把主内存的count
更新为1
。
- 线程A和线程B几乎同时启动,它们都执行

-
线程A"抢跑"成功
- 假设线程A的CPU时间片先到,它执行
compareAndSet(0, 1)
。 - 它发起一次原子操作:"请检查主内存的
count
是不是0
?如果是,就把它改成1
。" - 检查通过!主内存中的值确实是
0
。于是,CAS操作成功 ,count
的值被更新为1
。compareAndSet
返回true
,循环结束,线程A完成任务。
- 假设线程A的CPU时间片先到,它执行
-
线程B的"意外"与"自旋"
- 现在,轮到线程B执行
compareAndSet(0, 1)
。 - 它也发起原子请求:"请检查主内存的
count
是不是0
?" - 然而,此时主内存中的
count
已经是1
了!线程B的预期值0
与主内存的当前值1
不匹配。因此,这次CAS操作失败了。 compareAndSet
返回false
,while
循环的条件!false
变成了true
。线程B意识到:"看来在我发呆的时候,有人已经把值改了。我得重新来过。"- 线程B进入下一次循环 (这就是"自旋")。它重新执行
get()
,这次从主内存读到的是最新值1
,并更新了自己的oldValue
。
- 现在,轮到线程B执行
-
线程B的第二次尝试
- 线程B更新了自己的小算盘:"好的,现在值是
1
了,那我的目标就是把它更新成2
。" - 它再次发起
compareAndSet(1, 2)
请求。这一次,没有其他线程来捣乱,它的预期值1
和主内存的值1
完美匹配。 - CAS操作成功 ,主内存的
count
值被更新为2
。while
循环条件为false
,线程B也成功退出。
- 线程B更新了自己的小算盘:"好的,现在值是
通过这个过程,我们可以看到,CAS的核心就是一种"乐观"的尝试。每个线程都乐观地认为自己可以修改成功,如果失败了,也不会立刻阻塞,而是像一个执着的程序员一样,不断地重试(自旋),直到成功为止。这种机制在并发度不高、锁占用时间很短的场景下,性能远超于需要操作系统介入的重量级锁。
2) 实现自旋锁
基于 CAS 也可以实现一个更灵活的自旋锁。
java
public class SpinLock {
// owner为null表示锁空闲,否则表示被某个线程持有
private volatile Thread owner = null;
public void lock(){
// 如果锁是null(空闲),就尝试通过CAS把它设置为当前线程
// CAS失败说明锁被别人占了,进入循环忙等(自旋)
while(!CAS(this.owner, null, Thread.currentThread())){
// a busy-wait loop
}
}
public void unlock (){
// 直接将owner置为null即可释放锁
this.owner = null;
}
}
3. 深度剖析:CAS 的 ABA 问题及其解决方案
CAS的核心逻辑是:只要内存值等于预期旧值,就认为数据没有被其他线程修改过。但这里存在一个逻辑漏洞:如果数据被其他线程从 A 改为 B,然后又改回 A,会发生什么?
这就是 ABA 问题。对于执行CAS的线程来说,它无法区分数据是"从未变过",还是"曾经变过又变回来了"。

在大部分情况下,ABA问题可能无害。但某些对数据变化过程敏感的场景,它会引发严重BUG。
一个经典的ABA问题场景:
假设滑稽老哥账户有 100 元存款。他想从 ATM 取 50 块钱。
- 取款线程1 获取到当前存款值为 100, 期望更新为 50。
- 在线程1执行CAS前,CPU切换到另一个高优先级任务:滑稽的朋友正好给他转账 50,账户余额先变成150,然后他又消费了50,账户余额最终又变回 100。
- 轮到线程1 执行了, 它发现当前存款为 100, 和它之前读到的 100 相同, 于是CAS操作成功,执行扣款!
尽管中间发生了多次交易,但线程1的CAS操作依然成功了,这在某些金融场景下是不可接受的。
解决方案:版本号机制
要解决ABA问题,核心思路很简单:不要只关心值是否变了,还要关心它变过几次。于是,**版本号(Version)**机制应运而生。
- 在更新变量时,除了更新值,还要给版本号
+1
。 - 执行CAS操作时,不再是
CAS(V, A, B)
,而是CAS(V, (A, version), (B, version+1))
。 - 现在,只有当内存中的 值 和 版本号 与预期完全一致时,操作才会成功。
在 Java 标准库中,AtomicStampedReference<E>
类就是这一思想的完美实现。它内部维护了一个"时间戳"(stamp),这个时间戳其实就是版本号。
场景回顾:带版本号的取款操作
让我们用版本号机制来重演一遍上面的取款故事:
- 初始状态 :账户余额为
(100, version=1)
。 - 取款线程1 读取数据,得知当前余额为 100,版本号为 1。它的期望是:如果余额还是
(100, version=1)
,就将其更新为(50, version=2)
。 - 中间交易 :
- 朋友转账50元,账户余额变为
(150, version=2)
。 - 滑稽老哥消费50元,账户余额变为
(100, version=3)
。
- 朋友转账50元,账户余额变为
- 取款线程1执行 :它发起CAS请求:"请检查账户余额是不是
(100, version=1)
?" - 检查结果 :CPU发现,当前账户的实际状态是
(100, version=3)
。虽然值都是100,但版本号对不上了! - 操作失败:CAS操作失败,取款线程1的扣款操作被阻止。这就完美规避了ABA问题。
相关面试题
面试官:谈谈你对CAS的理解?
参考回答:
CAS,全称是Compare and Swap,也就是"比较并交换"。它是一种无锁化的、基于乐观锁思想的并发控制技术。
它的核心是CAS(V, A, B)
这样一个原子操作,包含三个操作数:内存地址V,预期值A,和新值B。执行时,它会原子地完成三件事:读取V处的值,与A比较,如果相等,就用B更新V的值。
它的原子性不是由软件代码保证的,而是由CPU的硬件指令(如lock cmpxchg
)直接支持的,所以效率很高。像Java里的AtomicInteger
等原子类,底层就是通过CAS的自旋操作来实现线程安全的,避免了使用重量级锁带来的线程挂起和上下文切换开销。
面试官:那CAS有什么缺点吗?比如ABA问题,你知道怎么解决吗?
参考回答:
是的,CAS有一个经典的ABA问题。就是说,一个值从A变成了B,又变回了A。CAS检查的时候会发现它的值没有变,但实际上这个值已经被其他线程修改过了。
在大多数情况下这可能没问题,但在一些需要严格追踪数据变更过程的场景(比如金融领域的账户余额操作),就会导致逻辑错误。
解决ABA问题的标准方法是引入版本号机制 。在每次修改数据时,除了修改数据本身,也递增版本号。这样,CAS操作就需要同时检查"数据值"和"版本号"是否都和预期一致。即使数据被改回了原样,版本号也已经变了,CAS操作就会失败,从而避免了ABA问题。在Java中,我们可以使用 AtomicStampedReference
这个类来实现带版本号的CAS操作。
二、synchronized
------从偏向锁到重量级锁的演进
理解了CAS和自旋锁,我们就能更好地揭开 synchronized
的神秘面纱。现代JVM为了极致的性能,赋予了 synchronized
多重身份和一套智能的锁升级机制。
1. synchronized
的多重身份回顾
结合上一篇的内容, 我们可以总结出, synchronized
具有以下特性(以现代JDK版本为例):
- 乐观与悲观的结合体:开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁。
- 轻量与重量的动态切换:开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁。
- 自旋锁的应用:实现轻量级锁的时候用到了自旋锁策略。
- 非公平锁。
- 可重入锁。
- 非读写锁。
2. 锁升级之路:偏向锁 -> 轻量级锁 -> 重量级锁
JVM 为了极致的性能,将 synchronized
锁分为 无锁、偏向锁、轻量级锁、重量级锁 四种状态,并会根据竞争情况,进行依次升级,且此过程通常是不可逆的。

1) 偏向锁
当第一个线程尝试加锁时,JVM并不会立刻加锁,而是优先进入偏向锁状态。
偏向锁不是真的 "加锁", 只是在对象头中做一个 "偏向锁的标记", 记录下这个锁"偏爱"哪个线程。如果后续没有其他线程来竞争该锁, 那么持有偏向锁的线程在进出同步块时,就无需再进行任何同步操作了,极大地避免了加锁解锁的开销。
偏向锁本质上相当于 "延迟加锁"。能不加锁就不加锁, 尽量来避免不必要的加锁开销。
如果后续有其他线程来竞争该锁, 那就取消原来的偏向锁状态, 升级为轻量级锁状态。
2) 轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态。
此处的轻量级锁就是通过 CAS 来实现的。线程会尝试通过CAS将锁对象的对象头指向自己的线程栈中的记录。
- 如果更新成功, 则认为加锁成功。
- 如果更新失败, 则认为锁已被占用, 线程会进行自旋式的等待(并不放弃 CPU)。
自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源。因此此处的自旋不会一直持续进行, 而是达到一定的时间或重试次数后(即所谓的"自适应自旋"),如果仍未获取到锁,就会再次升级。
3) 重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 锁就会"膨胀"为重量级锁。
此处的重量级锁就是指动用操作系统内核提供的 mutex
。
- 执行加锁操作, 线程会从用户态切换到内核态。
- 在内核态判定当前锁是否已经被占用。
- 如果该锁没有占用, 则加锁成功, 并切换回用户态。
- 如果该锁被占用, 则加锁失败。此时线程会进入锁的等待队列, 挂起并放弃CPU, 等待未来被操作系统唤醒。
3. JVM 的智能优化:锁消除与锁粗化
除了锁升级,JVM在即时编译(JIT)阶段还会进行一些智能的锁优化。
锁消除 (Lock Elision)
编译器和JVM足够智能,能够判断出某些代码中的 synchronized
锁是否是多余的。如果发现一个锁不可能存在竞争(例如在单线程中使用 StringBuffer
),就会直接将这个锁消除掉。
java
// 在单线程环境中,这些append的锁都是不必要的
public void createString() {
StringBuffer sb = new StringBuffer();
sb.append("a"); // append是同步方法,有锁
sb.append("b");
sb.append("c");
}
// JIT编译器会优化为:
public void createString() {
StringBuffer sb = new StringBuffer();
// 锁被消除
sb.append("a");
sb.append("b");
sb.append("c");
}
锁粗化 (Lock Coarsening)
如果一段逻辑中出现对同一个锁对象的多次、连续的加锁和解锁,编译器和JVM会自动将这些锁操作合并成一个更大范围的锁,以减少锁操作的次数。这被称为锁粗化。

一个类比:
领导给下属交代工作任务:
- 方式一 (锁粒度细): 打电话, 交代任务1, 挂电话。再打电话, 交代任务2, 挂电话。再打电话, 交代任务3, 挂电话。
- 方式二 (锁粒度粗): 打电话, 一次性交代完任务1, 任务2, 任务3, 然后挂电话。
显然, 方式二是更高效的方案。JVM的锁粗化就是这个道理。
本篇核心要点总结 (Key Takeaways)
- CAS是无锁编程 :CAS(Compare-and-Swap)是一种CPU原子指令,它可以在不使用锁的情况下实现线程安全的操作。它是
AtomicInteger
等原子类和JUC中许多并发工具的实现基础。 synchronized
的智能进化 :synchronized
并非一个简单的重量级锁,而是拥有一个从偏向锁 -> 轻量级锁 -> 重量级锁的智能升级过程,旨在尽可能降低无竞争或低竞争场景下的性能开销。- CAS与
synchronized
的内在联系 :synchronized
的轻量级锁阶段,就是通过"CAS + 自旋"来实现的,这避免了线程直接进入阻塞状态,提高了性能。 - ABA问题的警惕 :在使用CAS时,需要注意ABA问题(值被改回原样),对于需要严格保证操作过程的场景,应使用
AtomicStampedReference
等带有版本号机制的工具来解决。
-> 轻量级锁 -> 重量级锁**的智能升级过程,旨在尽可能降低无竞争或低竞争场景下的性能开销。 - CAS与
synchronized
的内在联系 :synchronized
的轻量级锁阶段,就是通过"CAS + 自旋"来实现的,这避免了线程直接进入阻塞状态,提高了性能。 - ABA问题的警惕 :在使用CAS时,需要注意ABA问题(值被改回原样),对于需要严格保证操作过程的场景,应使用
AtomicStampedReference
等带有版本号机制的工具来解决。