Skip to content

JUC 并发编程 2——Java 内存模型与底层同步机制

· 48 min

【跳转到上一篇:JUC并发编程——多线程基础】

六、Java 内存模型(Java Memory Model,JMM)#

jmm-20230823200720

JMM 定义了 Java 程序中的变量、线程如何和主存以及工作内存进行交互的规则。

并发编程的线程之间存在两个问题:

有两种并发模型可以解决这两个问题:

这两种模型之间的区别如下图所示:

image-20260108024420583

Java 使用的是共享内存并发模型

Java 共享内存并发模型#

image-20260108024553656

在栈中的变量(局部变量、方法定义的参数、异常处理的参数)不会在线程之间共享,也就不会有内存可见性的问题,也不受内存模型的影响。而在堆中的变量是共享的,一般称之为共享变量

所以,内存可见性针对的是堆中的共享变量。

jmm-f02219aa-e762-4df0-ac08-6f4cceb535c2

内存模型规定如下:

那么如何保证内存可见性?怎么知道这个共享变量的被其他线程更新了呢?JMM 通过控制主存与每个线程的本地内存之间的交互,来提供内存可见性保证。

Java 中的 volatile 关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized 关键字不仅保证可见性,同时也保证了原子性(互斥性)。

在更底层,JMM 通过内存屏障来实现内存的可见性以及禁止重排序。为了程序员更方便地理解,设计者提出了 happens-before 的概念(下文会细讲),它更加简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则,以及这些规则的具体实现方法。

重排序#

在编译或执行时,为了优化程序的执行效率,编译器或处理器常常会对指令进行重排序,有以下情况:

指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。

顺序一致性模型与JMM的保证#

一、数据竞争(Data Race)与正确同步

JMM 承诺:如果程序是正确同步的(即无数据竞争),那么其执行结果将与在顺序一致性模型中的某个执行结果一致。

二、什么是顺序一致性模型?

顺序一致性(Lamport, 1979)是一个理想化的内存模型,具有两大特性:

  1. 程序顺序性(Program Order): 每个线程内的操作必须按照代码顺序执行。
  2. 全局单一执行顺序(Global Total Order): 所有线程看到的操作执行顺序是一致的,且每个操作原子地、立即对所有线程可见

举例: 线程 A:A1 → A2 → A3 线程 B:B1 → B2 → B3 在 SC 模型中,存在一个全局序列(如 A1 → B1 → A2 → B2 → A3 → B3),所有线程都观察到这个顺序。

三、JMM 为什么不直接实现顺序一致性?

因为顺序一致性会严重限制硬件和编译器的优化能力,导致性能大幅下降。

JMM 的设计哲学是在性能与正确性之间取得平衡:

在保证正确同步程序语义不变的前提下,允许尽可能多的重排序和优化。

具体差异如下:

特性顺序一致性模型Java 内存模型(JMM)
单线程内操作顺序严格按程序顺序允许重排序,但保证 as-if-serial(单线程结果不变)
多线程可见性所有操作立即全局可见仅通过同步建立 happens-before 才保证可见性;否则,写操作可能滞留在本地缓存
操作原子性所有内存操作原子long/double 的非 volatile 写操作可能不是原子的(JVM 规范允许拆分为两个 32 位写)
执行结果一致性所有执行都符合某个全局顺序仅对无数据竞争的程序保证与 SC 某一执行等价

JMM要求编译器和处理器禁止会改变程序执行结果的重排序。

happens-before#

JMM使用happens-before的概念来定制两个操作之间的执行顺序。这两个操作可以在一个线程以内,也可以是不同的线程之间。

happens-before关系的定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;
  2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。

程序员只要遵循happens-before规则,那他写的程序就能保证在JMM中具有强的内存可见性。

在Java中,有以下天然的happens-before关系:

七、管程模型—synchronized 关键字与锁#

管程模型(Monitor) 是一种程序结构,用于管理对共享资源的并发访问。它的核心思想是:同一时刻,最多只有一个线程能执行管程中的代码。

一个完整的管程通常包含以下三部分:

组件说明
1. 共享数据(Shared Data)被多个线程访问的变量或资源(如队列、计数器等)
2. 互斥锁(Mutex / Lock)确保同一时间只有一个线程能进入管程(即执行管程内的方法)
3. 条件变量(Condition Variables)用于线程间的等待/通知机制

在 Java 中:

  • 互斥锁 由对象的 Monitor(监视器) 实现;
  • 条件变量 通过 Object.wait() / notify() / notifyAll() 实现。

在Java多线程中,锁的概念都是基于对象的,因此也称它为对象锁。它采用互斥的方式让同一时刻至多有一个线程能持有对象锁。

还有一点需要注意的是,我们常听到的类锁其实也是对象锁。

Java类只有一个Class对象(可以有多个实例对象,多个实例共享这个Class对象),而Class对象也是特殊的Java对象。所以我们常说的类锁,其实就是Class对象的锁。

synchronized 关键字#

通常使用 synchronized 关键字来给一段代码或一个方法上锁。有以下三种形式:

// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号里面的对象/类
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}
// 当然除此之外,我们还可以使用 this 对象(代表当前实例)或者当前类的 Class 对象作为锁

如果 synchronized 关键字在方法上,那临界区就是整个方法内部;如果使用 synchronized 代码块,那临界区就是代码块内部的区域。

当一个线程进入到同步代码块时,会获取到当前的锁,而这时如果其他使用同样的锁的同步代码块也想执行内容,就必须等待当前同步代码块的内容执行完毕自动释放这把锁,其他的线程才能拿到这把锁并开始执行同步代码块里面的内容(实际上synchronized是一种悲观锁,随时都认为有其他线程在对数据进行修改,后面会讲到乐观锁,如CAS算法)。

synchronized 属于可重入锁,可以一定程度避免死锁:

public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this,当前实例对象锁
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}

Java对象头#

一个对象的“锁”的信息存放在什么地方呢?

每个Java对象都有对象头。如果是非数组类型,则用2个字宽(1个字宽为32/64bit)来存储对象头;如果是数组类型,则用3个字宽。

对象头的内容:

主要来看Mark Word的格式:

image-20260110012835833 w5kq4gbBHcCMv1L

可以看到,当对象状态为偏向锁时, Mark Word 存储的是偏向的线程ID;当状态为轻量级锁时, Mark Word 存储的是指向线程栈中 Lock Record 的指针;当状态为重量级锁时, Mark Word 为指向堆中的monitor对象的指针。

对象监视器 monitor#

拿同步块来举例:

static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}

经过 javap -v 编译后的指令如下:

public static void main(java.lang.String[]);
// 方法签名
descriptor: ([Ljava/lang/String;)V
// 访问修饰符
flags: ACC_PUBLIC, ACC_STATIC
Code:
// 操作数栈深度和本地变量表容量
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object; // 获取静态字段 lock 的值并将其推送到操作数栈顶部
3: dup // 复制栈顶的数值(即 lock 引用)并将副本推送到操作数栈顶部
4: astore_1 // 将栈顶的数值(即 lock 引用的副本)存储到本地变量 1(args 参数)
5: monitorenter // 进入监视器(锁)保护的同步块
6: getstatic #3 // Field counter:I 执行counter++
9: iconst_1
10: iadd
11: putstatic #3
14: aload_1
15: monitorexit // 自增操作完毕,退出监视器(锁)保护的同步块
16: goto 24 // 无条件跳转到指令位置 24,继续执行下面的指令
19: astore_2 // 这里是异常处理:将栈顶的数值(即异常对象引用)存储到本地变量 2(ex 异常)
20: aload_1 // 将本地变量 1(args 参数)加载到操作数栈顶部
21: monitorexit // 释放锁:在异常处理块中,退出监视器(锁)保护的同步块
22: aload_2 // 重试异常:将本地变量 2(ex 异常)加载到操作数栈顶部
23: athrow // 抛出异常
24: return // 方法返回
Exception table:
from to target type
6 16 19 any // 如果6~16行出现了异常,跳转到19行继续执行
19 22 19 any // 如果19~22行出现了异常,跳转到19行继续执行
bYUPEDwoBdkSxi3

每个对象都可以关联一个Monitor对象,使用synchronized给对象上锁(重量级)之后,该对象头的MarkWord就被指向Monitor对象的指针。

注意:不加synchronized的对象不会关联监视器

pC1iNxH
class ObjectMonitor {
void* _owner; // 当前持有者
int _recursions; // 重入次数
int _count; // (部分版本保留)
ObjectWaiter* _cxq; // contention queue/list,并发竞争栈(LIFO)
ObjectWaiter* _EntryList; // 阻塞等待链表
ObjectWaiter* _WaitSet; // wait() 等待链表
Thread* _succ; // 下一个候选继承者
Thread* _Responsible; // 自旋线程
oop _object; // 关联的 Java 对象
ObjectMonitor* _FreeNext; // 空闲链表指针
volatile int _flags; // 状态标志
};

Monitor 核心字段包含:

几种锁#

Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“。

在Java 6 以前,所有的锁都是”重量级“锁。所以在Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

无锁就是没有对资源进行锁定,任何线程都可以尝试去修改它。

几种锁会随着竞争情况逐渐升级,重量级锁不能撤销或降级回偏向锁或轻量级锁

各种锁的优缺点对比(来自《Java 并发编程的艺术》):

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块场景。
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗 CPU。追求响应时间。同步块执行速度非常快。
重量级锁线程竞争不使用自旋,不会消耗 CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行时间较长。

轻量级锁#

针对多个线程在不同时段获取同一把锁,即不存在锁竞争、没有线程阻塞的情况,JVM采用轻量级锁来避免线程的阻塞与唤醒

1、轻量级锁的加锁

自旋:不断尝试去获取锁,一般用循环来实现。

自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状 态,浪费CPU资源。可以指定自旋的次数,但是HotSpot JVM 采用 适应性自旋(Adaptive Spinning) 机制:

2、锁膨胀

如果自旋到一定程度依然没有获取到锁,称为自旋失败,那么这个线程会阻塞,同时这个锁会升级成重量级锁

目的是:避免长时间自旋浪费 CPU,转而使用 OS 提供的阻塞/唤醒机制,提高系统整体效率

  1. 创建重量级 Monitor 对象
    • JVM 在堆外(C++ 层)分配一个 ObjectMonitor 实例;
  2. 将对象头指向 Monitor
    • 使用 CAS 将对象头的 Mark Word 替换为指向 ObjectMonitor 的指针
    • 此后,所有新来的线程发现对象头指向 Monitor,synchronized 操作都走重量级锁逻辑。
  3. 挂起竞争线程
    • 触发膨胀的线程(如线程 B)不再自旋;
    • 调用 OS 的阻塞原语(如 Linux 的 pthread_mutex_lock / futex);
    • 被加入 Monitor 的 _cxq(Contention List),进入 OS 睡眠状态(S
    • 等待锁持有者释放锁后被唤醒。

3、轻量级锁的释放

退出synchronized代码块(解锁)时,

image-20260110013257842

重量级锁#

重量级锁依赖于操作系统的互斥量(mutex) 实现,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。

注意:一旦对象真正进入重量级锁状态(Mark Word 指向 ObjectMonitor),后续线程不再自旋,而是直接阻塞。

加锁:
┌───────────────┐
│ 尝试 CAS 抢锁 │ ← 新线程 or 自旋成功
└───────┬───────┘
↓ 否
┌───────────────┐
│ 是否重入? │
└───────┬───────┘
↓ 否
┌───────────────┐
│ 插入 _cxq │ ← CAS 入栈
└───────┬───────┘
┌───────────────┐
│ park() 挂起 │
└───────────────┘
释放锁:
┌───────────────┐
│ 重入?→ 递减 │
└───────┬───────┘
↓ 否
┌───────────────┐
│ 合并 _cxq → _EntryList │
└───────┬───────┘
┌───────────────┐
│ 选 succ(假定继承人)│
└───────┬───────┘
┌───────────────┐
│ unpark(succ) │
└───────────────┘
succ 线程回到“加锁”流程顶部,重新抢锁

点击跳转到前文加锁、释放锁的具体场景

一、重量级锁加锁流程(ObjectMonitor::enter

步骤 1:快速尝试直接获取锁

if (monitor->_owner == NULL) {
// CAS 尝试直接抢锁
if (Atomic::cmpxchg(&monitor->_owner, NULL, current_thread) == NULL) {
return; // 成功!
}
}

步骤 2:检查是否重入(当前线程已持有)

if (monitor->_owner == current_thread) {
monitor->_recursions++; // 重入计数 +1
return
}

步骤 3:进入竞争队列(_cxq

步骤 4:挂起线程(park

二、重量级锁释放流程(ObjectMonitor::exit

步骤 1:处理重入

if (monitor->_recursions > 0) {
monitor->_recursions--;
return; // 未完全释放
}

步骤 2:准备唤醒候选线程

JVM 会按优先级检查以下位置是否有等待线程:

  1. _EntryList(主等待队列)
  2. _cxq(并发竞争栈)
  3. _succ(假定继承人,若有)

合并 _cxq _EntryList

步骤 3:选择 “Heir Presumptive”(假定继承人)

步骤 4:尝试“锁交接优化”

步骤 5:唤醒线程

⚠️ 非公平性的体现

  • succ 被唤醒之前,可能有新来的线程通过自旋或 CAS 抢先获得锁
  • 因此,_EntryList 中的线程不能保证先到先得synchronized 是非公平锁

三、wait() / notify() 与 Monitor 的交互

Object类有三个方法,分别是 wait()notify() 以及 notifyAll(),他们其实是需要配合synchronized来使用的(实际上锁就是依附于对象存在的,每个对象都应该有针对于锁的一些操作,所以这样设计),只有在同步代码块中才能使用这些方法,正常情况下会报错。

调用 obj.wait()

  1. 必须已持有锁
  2. 将当前线程加入 _WaitSet
  3. 释放锁(调用 exit);
  4. 调用 park() 挂起(状态变为 WAITING)。

调用 obj.notify()

  1. _WaitSet 移出一个 WAITING 状态的线程
  2. 将其加入 _cxq
  3. 不立即唤醒,等下次 exit 时统一处理。

所以:notify 唤醒的线程,和其他竞争线程一样,要走 _cxq _EntryList → 抢锁 的流程。

notifyAll其实和notify一样,也是用于唤醒,但是前者是唤醒所有调用 wait() 后处于等待的线程,而后者是随机选择一个。

注意区别:

  • BLOCKED 状态的线程:是因为尝试进入 synchronized 块但发现锁已被占用,于是被加入 _cxq,处于阻塞等待状态。
  • WAITING 状态的线程:是主动调用 wait() 放弃锁并进入 _WaitSet,只有被 notify/notifyAll 唤醒后才会被移到 _cxq,之后和其他竞争者一样走抢锁流程。

偏向锁#

轻量级锁在没有竞争时,每次重入仍需要进行CAS操作。Java6 中引入了偏向锁来减少无竞争场景下的同步开销只有第一次使用CAS将线程ID设置到对象头的MarkWord头,之后发现这个线程ID是自己的,那么就表示没有竞争,不用任何CAS或原子操作。以后只要不发生竞争,这个对象就归该线程所有。

值得注意的是,如果对象通过调用 hashCode() 方法计算过对象的一致性哈希值,那么它是不支持偏向锁的,会直接进入到轻量级锁状态,因为Hash是需要被保存的,而偏向锁的Mark Word数据结构,无法保存Hash值;如果对象已经是偏向锁状态,再去调用 hashCode() 方法,那么会直接将锁升级为重量级锁,并将哈希值存放在 monitor(有预留位置保存)中。

一、偏向锁加锁流程(biased_locking_enter

场景:线程 A 第一次进入 synchronized(obj)

场景:线程 A 再次进入 synchronized(obj)

步骤:

  1. 读取对象头 Mark Word;
  2. 检查 thread ID 是否等于当前线程 ID
  3. 如果是 → 直接进入临界区,无任何 CAS、无任何原子操作
  4. 如果不是 → 触发偏向撤销

二、当其他线程(B)尝试竞争时:偏向撤销(Bias Revocation)

  1. 线程 B 发现对象偏向线程 A(Mark Word 中 thread ID ≠ B);
  2. 线程 B 触发“偏向撤销”
    • 向 JVM 提交一个 Revocation Task
    • JVM 在安全点(Safepoint) 暂停所有线程;
    • 检查线程 A 的状态:
      • 如果线程 A 已退出所有同步块(逻辑上不持有该锁,但物理上仍持有):
        • 将对象重新偏向线程 B(更新 thread ID);
        • 锁仍为偏向锁
      • 如果线程 A 仍在同步块中(仍逻辑持有该锁):
        • 撤销偏向:清除 thread ID,设置 Mark Word 为 [hashCode | age | 001](无锁);
        • 后续加锁走轻量级锁路径
        • 不会立即升级为轻量级锁,而是下次加锁时按无锁处理。

三、偏向锁没有“释放”操作,而是使用了一种等到竞争出现才释放锁的机制

线程 A 首次 synchronized(obj)
CAS 设置 Mark Word = [epoch | A | age | 101] → 偏向锁建立
线程 A 再次进入
检查 thread ID == A → 直接进入(零开销)
线程 B 尝试 synchronized(obj)
发现 thread ID ≠ B → 触发 Bias Revocation
JVM 在 Safepoint 暂停所有线程
检查线程 A 是否仍在同步块?
├─ 否 → 重新偏向 B(仍为偏向锁)
└─ 是 → 撤销偏向,Mark Word = [hash | age | 001]
下次加锁走轻量级锁流程

八、乐观锁与CAS(Compare-And-Swap)#

CAS(Compare-And-Swap,比较并交换)是一种乐观锁的实现方式,用于在硬件层面上提供无锁的原子性操作。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

乐观锁与悲观锁#

由于乐观锁假想操作中没有锁的存在,因此不太可能出现死锁的情况,换句话说,乐观锁天生免疫死锁

CAS 原理#

在 CAS 中,有这样三个值:

比较并交换的过程:判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。

有没有可能在判断了V为E之后,正准备更新它为N时,被其它线程更改了V的值呢?

不会的。因为 CAS 是一种原子操作,它是一种系统原语,是一条 CPU 的原子指令,从 CPU 层面已经保证它的原子性。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

在 Java 中,如果一个方法是 native 的,那 Java 就不负责具体实现它,而是交给底层的 JVM 使用 C 或者 C++ 去实现。

在 Java 中有一个 Unsafe 类,在 sun.misc 包中,里面都是 native 方法,其中就有几个是关于 CAS 的:

boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
boolean compareAndSwapInt(Object o, long offset,int expected,int x);
boolean compareAndSwapLong(Object o, long offset,long expected,long x);

Linux 的 X86 下主要是通过 cmpxchgl 这个指令在 CPU 上完成 CAS 操作,但在多处理器情况下,必须使用 lock 指令加锁来完成。当然,不同的操作系统和处理器在实现方式上肯定会有所不同。

CMPXCHG是“Compare and Exchange”的缩写,它是一种原子指令,用于在多核/多线程环境中安全地修改共享数据。CMPXCHG在很多现代微处理器体系结构中都有,例如Intel x86/x64体系。对于32位操作数,这个指令通常写作CMPXCHG,而在64位操作数中,它被称为CMPXCHG8B或CMPXCHG16B。

CAS 如何实现原子操作#

上面介绍了 Unsafe 类的支持 CAS 的方法。那 Java 具体是如何通过这几个方法来实现原子操作的呢?

JDK 提供了一些用于原子操作的类,在java.util.concurrent.atomic包下面。

这里以 AtomicInteger 类的 getAndAdd(int delta) 方法为例。

public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}

这里的 unsafe 其实就是一个 Unsafe 对象:

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

所以,AtomicInteger 类的 getAndAdd() 方法是通过调用 Unsafe 类的方法实现的:

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}

方法执行的过程:

这里使用的是do-while 循环,目的是保证循环体内的语句至少会被执行一遍

CAS 的三大问题#

ABA 问题#

ABA 问题是指在基于 CAS(Compare-And-Swap)的无锁算法中,一个变量的值从 A → B → A 变化后,CAS 操作仍会认为“值未变”,从而错误地认为期间没有发生任何修改。 但实际上,该变量已经被其他线程修改过两次,可能已导致逻辑不一致(例如链表节点被删除又复用)。

📌 典型场景: 在无锁栈(Lock-Free Stack)中,线程1准备弹出节点 A;此时线程2弹出 A,再压入新节点(恰好地址复用为 A),线程1的 CAS 成功,但实际弹出了“另一个 A”,造成数据错乱。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了 AtomicStampedReference 类来解决引用类型的 ABA 问题。(注:对于基本类型,可使用 AtomicMarkableReference 或自定义带版本字段的包装类)

这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用 CAS 设置为新的值和标志。

public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}

执行流程:

1、Pair<V> current = pair;

2、双重校验 expectedReference == current.reference && expectedStamp == current.stamp(使用 == 比较引用)

3、短路优化:无需更新 (newReference == current.reference && newStamp == current.stamp)

4、执行 CAS 更新 casPair(current, Pair.of(newReference, newStamp))

长时间自旋#

CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。

解决思路是让 JVM 支持处理器提供的pause 指令

pause 指令能让自旋失败时 cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率降低很多,为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多。

多个共享变量的原子操作#

当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法:

  1. 使用 AtomicReference 类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作;
  2. 使用锁,锁内的临界区代码可以保证只有当前线程能操作。