CAS

8 阅读26分钟

CAS(Compare-And-Swap,比较并交换)  是一种硬件级别的原子操作,用于实现多线程环境下的无锁同步。它通过比较内存中的值是否等于预期值(expected),如果相等,则将内存中的值更新为新值(newValue);否则,不做任何操作。

CAS 是现代并发编程中非常重要的基础技术,广泛应用于 Java 的 java.util.concurrent 包(如 Atomic 类)和其他高性能并发框架中。


CAS 的工作原理

1. 基本步骤

CAS 操作的核心逻辑如下:

  1. 比较内存中的值(currentValue)是否等于预期值(expectedValue)。
  2. 如果相等,则将内存中的值更新为新值(newValue)。
  3. 如果不相等,则不更新,并返回失败。
2. 伪代码

以下是 CAS 的伪代码实现:

boolean compareAndSwap(memoryAddress, expectedValue, newValue) {
    if (memoryAddress.value == expectedValue) {
        memoryAddress.value = newValue;
        return true; // 更新成功
    } else {
        return false; // 更新失败
    }
}
3. 硬件支持

CAS 操作依赖于 CPU 的指令集支持,例如:

  • x86 架构CMPXCHG 指令。
  • ARM 架构LDREX 和 STREX 指令。
  • 其他架构:类似的原子指令。

这些指令能够在硬件层面保证比较和更新操作是不可分割的(原子的)。


CAS 的优点

  1. 无锁同步

    • CAS 是一种无锁算法,避免了传统锁(如 synchronized 或 ReentrantLock)带来的线程阻塞和上下文切换开销。
  2. 高性能

    • CAS 操作直接由硬件支持,性能非常高,适合高并发场景。
  3. 简单易用

    • CAS 提供了简单的原子操作接口,适合实现线程安全的数据结构(如 AtomicIntegerConcurrentHashMap)。

CAS 的缺点

  1. ABA 问题

    • 如果内存中的值从 A 变为 B,然后又变回 A,CAS 操作会认为值没有变化,从而导致错误。

    • 解决方法

      • 使用版本号(如 AtomicStampedReference)来标记值的变化。
      • 每次更新时同时更新版本号,避免 ABA 问题。
  2. 自旋开销

    • 如果 CAS 操作失败,通常会采用自旋重试的方式(不断尝试更新值),在高竞争场景下可能导致 CPU 资源浪费。
  3. 只能操作单个变量

    • CAS 只能保证单个变量的原子性,无法直接操作多个变量。

    • 解决方法

      • 使用 AtomicReference 或其他高级工具(如 StampedLock)。
  4. 硬件依赖

    • CAS 操作依赖于底层硬件指令集支持,不同架构的实现可能有所不同。

CAS 的应用场景

  1. Java 并发包中的 Atomic 类
  • Java 提供了 java.util.concurrent.atomic 包,其中的类(如 AtomicIntegerAtomicBooleanAtomicReference)都基于 CAS 实现。
  • 示例:AtomicInteger 的 compareAndSet 方法。
AtomicInteger atomicInteger = new AtomicInteger(0);
boolean success = atomicInteger.compareAndSet(0, 1); // 如果当前值是 0,则更新为 1
System.out.println(success); // 输出 true
  1. 高性能队列

    • 无锁队列(如 ConcurrentLinkedQueue)使用 CAS 来实现线程安全的入队和出队操作。
  2. 线程池

    • Java 的 ThreadPoolExecutor 使用 CAS 来更新线程池的状态。
  3. 计数器

    • 高并发场景下的计数器(如 AtomicLong)使用 CAS 实现线程安全的递增和递减。

CAS 的实现细节(以 Java 为例)

1. Unsafe 类
  • Java 的 Atomic 类底层依赖于 sun.misc.Unsafe 类中的 CAS 方法。
  • 示例:compareAndSwapInt 方法。
public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int newValue);
    • obj:要操作的对象。
    • offset:对象中变量的内存偏移量。
    • expected:预期值。
    • newValue:要更新的新值。
2. 内存偏移量
  • Unsafe 类通过内存偏移量直接操作对象的字段。
  • 示例:获取字段的内存偏移量。
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) {
        throw new Error(ex);
    }
}
3. CAS 操作
  • AtomicInteger 的 compareAndSet 方法实现:
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

CAS 的优缺点对比

特性优点缺点
线程安全性无锁同步,避免线程阻塞和上下文切换。在高竞争场景下可能导致自旋开销。
性能硬件级别的原子操作,性能非常高。依赖硬件指令集,不同架构的实现可能有所不同。
适用场景适合单变量的原子操作,如计数器、标志位等。无法直接操作多个变量,需借助其他工具(如 AtomicReference)。
问题简单高效,适合高并发场景。存在 ABA 问题,需要额外处理(如版本号)。

CAS 的扩展:ABA 问题

什么是 ABA 问题?
  • ABA 问题是指:如果内存中的值从 A 变为 B,然后又变回 A,CAS 操作会认为值没有变化,从而导致错误。
解决 ABA 问题的方法
  1. 版本号机制

    • 在值的基础上附加一个版本号,每次更新值时同时更新版本号。
    • 示例:AtomicStampedReference
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 1);

int[] stampHolder = new int[1];
int value = atomicStampedRef.get(stampHolder);
System.out.println("Value: " + value + ", Stamp: " + stampHolder[0]);

atomicStampedRef.compareAndSet(100, 200, stampHolder[0], stampHolder[0] + 1);
  1. 时间戳机制

    • 使用时间戳代替版本号,确保每次更新的时间戳唯一。

总结

  • CAS 是什么

    • CAS 是一种硬件级别的原子操作,用于实现无锁同步。
    • 它通过比较和更新内存中的值来保证线程安全。
  • 优点

    • 高效、无锁、线程安全,适合高并发场景。
  • 缺点

    • 存在 ABA 问题。
    • 在高竞争场景下可能导致自旋开销。
  • 应用场景

    • Java 的 Atomic 类、无锁队列、线程池等。

CAS 是现代并发编程的核心技术之一,通过合理使用 CAS,可以显著提升系统的并发性能。

CAS 的自旋

CAS(Compare-And-Swap,比较并交换)  是一种硬件级别的原子操作,用于实现多线程环境下的无锁同步。在 CAS 操作中,如果当前线程的更新失败(即内存中的值与预期值不一致),线程会不断重试,直到更新成功。这种不断重试的行为被称为 CAS 的自旋


CAS 自旋的工作原理

1. 基本流程

CAS 的自旋逻辑如下:

  1. 线程读取共享变量的当前值。
  2. 比较当前值是否等于预期值(expectedValue)。
  3. 如果相等,则将共享变量更新为新值(newValue),操作成功,退出自旋。
  4. 如果不相等,则重新读取共享变量的值,重复上述步骤,直到更新成功。
2. 伪代码

以下是 CAS 自旋的伪代码:

while (true) {
    // 获取当前值
    int currentValue = memoryAddress.value;

    // 比较并尝试更新
    if (compareAndSwap(memoryAddress, currentValue, newValue)) {
        break; // 更新成功,退出自旋
    }
    // 更新失败,继续自旋
}
3. 自旋的特点
  • 无锁:线程不会阻塞,而是通过不断重试来完成操作。
  • 高效:避免了线程上下文切换的开销(如加锁和解锁)。
  • 适合低竞争场景:在锁竞争较少的情况下,自旋通常能快速完成操作。

CAS 自旋的优点

  1. 避免线程阻塞

    • 自旋操作不会导致线程进入阻塞状态,避免了线程上下文切换的开销。
    • 适合短时间的锁竞争场景。
  2. 高性能

    • CAS 自旋依赖硬件级别的原子操作(如 CMPXCHG 指令),性能非常高。
  3. 无锁同步

    • 自旋是无锁算法的核心,避免了传统锁机制(如 synchronized 或 ReentrantLock)带来的性能瓶颈。

CAS 自旋的缺点

  1. 高竞争场景下的性能问题

    • 如果多个线程同时竞争同一个资源,自旋可能会导致大量的 CPU 资源浪费,因为线程会不断重试。
  2. 自旋时间过长

    • 如果共享变量长时间无法更新,线程会一直自旋,可能导致 CPU 占用率过高。
  3. 无法解决复杂同步问题

    • CAS 自旋只能保证单个变量的原子性,无法直接处理多个变量的同步问题。
  4. ABA 问题

    • 自旋无法解决 CAS 的 ABA 问题(即值从 A 变为 B,然后又变回 A),需要额外的机制(如版本号)来解决。

CAS 自旋的实现

1. Java 中的 CAS 自旋

Java 的 Atomic 类(如 AtomicIntegerAtomicBoolean)底层依赖 CAS 操作,并通过自旋实现线程安全。

示例:AtomicInteger 的自旋实现

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
  • compareAndSwapInt

    • 这是一个本地方法,调用底层的硬件指令(如 CMPXCHG)实现 CAS 操作。
    • 如果 CAS 操作失败,compareAndSet 会不断重试,直到成功。

2. 自旋锁的实现

自旋锁是一种基于 CAS 的锁实现,线程在获取锁失败时会自旋等待,而不是进入阻塞状态。

示例:自旋锁的简单实现

import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {
    private final AtomicBoolean lock = new AtomicBoolean(false);

    public void lock() {
        while (!lock.compareAndSet(false, true)) {
            // 自旋等待
        }
    }

    public void unlock() {
        lock.set(false);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();

        Runnable task = () -> {
            spinLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " acquired the lock");
                Thread.sleep(1000); // 模拟任务
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLock.unlock();
                System.out.println(Thread.currentThread().getName() + " released the lock");
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
    }
}

输出示例

Thread-0 acquired the lock
Thread-0 released the lock
Thread-1 acquired the lock
Thread-1 released the lock

CAS 自旋的适用场景

  1. 低竞争场景

    • 当锁竞争较少时,自旋通常能快速完成操作,避免线程阻塞。
  2. 短时间的临界区

    • 如果临界区的操作非常短(如简单的计数器递增),自旋的性能优于传统锁。
  3. 高性能场景

    • 在高性能并发框架中(如 ConcurrentHashMapThreadPoolExecutor),CAS 自旋被广泛使用。

CAS 自旋的优化

  1. 限制自旋次数

    • 为了避免线程长时间自旋,可以设置最大自旋次数。如果超过次数仍未成功,则线程进入阻塞状态。
    • 示例:
public void lock() {
    int maxSpin = 1000; // 最大自旋次数
    int spins = 0;
    while (!lock.compareAndSet(false, true)) {
        if (++spins > maxSpin) {
            // 超过自旋次数,进入阻塞状态
            Thread.yield();
        }
    }
}
  1. 自适应自旋

    • 根据锁竞争的历史记录动态调整自旋次数。例如,JVM 的 ReentrantLock 使用了自适应自旋策略。
  2. 结合其他同步机制

    • 在高竞争场景下,可以结合传统锁或其他同步机制(如 Semaphore)来减少自旋的开销。

CAS 自旋的优缺点对比

特性优点缺点
线程安全性基于硬件级别的 CAS 操作,线程安全。在高竞争场景下可能导致大量 CPU 资源浪费。
性能避免线程阻塞和上下文切换,性能高。自旋时间过长可能导致性能下降。
适用场景适合低竞争、短时间的临界区操作。不适合高竞争或长时间的临界区操作。
复杂性实现简单,依赖 CAS 操作即可。无法直接解决 ABA 问题,需要额外机制(如版本号)。

总结

  1. CAS 自旋的定义

    • CAS 自旋是指线程在 CAS 操作失败时不断重试,直到操作成功。
  2. 优点

    • 避免线程阻塞,性能高,适合低竞争场景。
  3. 缺点

    • 在高竞争场景下可能导致 CPU 资源浪费,自旋时间过长会影响性能。
  4. 适用场景

    • 低竞争、短时间的临界区操作。
  5. 优化策略

    • 限制自旋次数、自适应自旋、结合其他同步机制。

CAS 自旋是现代并发编程中非常重要的技术,通过合理使用和优化,可以显著提升系统的并发性能。

Java CAS 自旋实现

Java 的 CAS 自旋实现依赖于底层硬件指令,而这些硬件指令通过操作系统的支持被调用。具体来说,Java 的 CAS 自旋是通过 CPU 提供的原子指令(如 x86 架构的 CMPXCHG 或 ARM 架构的 LDREX/STREX)实现的,而这些指令由操作系统和 JVM 的本地代码(Native Code)桥接到 Java 层。


Java CAS 自旋的实现流程

  1. Java 层的自旋逻辑

    • Java 的 CAS 自旋逻辑是通过 Unsafe 类或 Atomic 类(如 AtomicInteger)实现的。
    • 当 CAS 操作失败时,线程会进入自旋状态,不断重试,直到 CAS 操作成功。
  2. JVM 层的 CAS 实现

    • JVM 使用 Unsafe 类的 compareAndSwapXXX 方法来实现 CAS 操作。
    • 这些方法是 native 方法,通过 JNI 调用本地代码。
  3. 本地代码层

    • 本地代码(通常是 C/C++ 编写的)会调用 CPU 提供的原子指令(如 CMPXCHG 或 LDREX/STREX)。
    • 这些指令直接在硬件层面执行,保证了比较和交换操作的原子性。
  4. 硬件层的原子指令

    • CPU 提供的原子指令(如 CMPXCHG)能够在一个时钟周期内完成比较和交换操作,避免了中断或其他线程的干扰。
    • 如果 CAS 操作失败,Java 层会进入自旋状态,继续调用底层指令重试。

自旋的核心:硬件指令的支持

1. x86 架构:CMPXCHG 指令
  • 作用:比较内存中的值和寄存器中的值,如果相等,则更新内存中的值。
  • 原子性:通过缓存一致性协议(如 MESI 协议)或总线锁保证操作的原子性。
2. ARM 架构:LDREX 和 STREX 指令
  • 作用LDREX 加载内存值并标记为独占访问,STREX 尝试更新内存值,如果期间有其他线程修改了该值,则更新失败。
  • 原子性:通过独占访问标记和缓存一致性协议实现。
3. 硬件级别的原子性保证
  • 总线锁:在单核 CPU 中,CAS 操作通过锁定总线实现原子性。
  • 缓存一致性协议:在多核 CPU 中,CAS 操作通过锁定缓存行实现原子性。

Java CAS 自旋的实现示例

以下是一个基于 AtomicInteger 的 CAS 自旋示例:

import java.util.concurrent.atomic.AtomicInteger;

public class CASSpinExample {
    private static final AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            while (!counter.compareAndSet(0, 1)) {
                // 自旋等待
            }
            System.out.println("Thread 1 updated counter to 1");
        });

        Thread thread2 = new Thread(() -> {
            while (!counter.compareAndSet(1, 2)) {
                // 自旋等待
            }
            System.out.println("Thread 2 updated counter to 2");
        });

        thread1.start();
        thread2.start();
    }
}
执行流程
  1. counter.compareAndSet(0, 1) 调用 Unsafe.compareAndSwapInt 方法。
  2. JVM 通过 JNI 调用本地代码,执行硬件指令(如 CMPXCHG)。
  3. 如果 CAS 操作失败,线程会进入自旋状态,继续重试。

自旋的优缺点

优点
  1. 避免线程阻塞

    • 自旋不会导致线程进入阻塞状态,避免了线程上下文切换的开销。
  2. 高性能

    • 在低竞争场景下,自旋通常能快速完成操作,性能优于传统锁。
缺点
  1. 高竞争场景下的性能问题

    • 如果多个线程同时竞争同一个资源,自旋可能导致大量的 CPU 资源浪费。
  2. 自旋时间过长

    • 如果共享变量长时间无法更新,线程会一直自旋,可能导致 CPU 占用率过高。

自旋的优化策略

  1. 限制自旋次数

    • 设置最大自旋次数,如果超过次数仍未成功,则线程进入阻塞状态。
    • 示例:
int maxSpin = 1000;
int spins = 0;
while (!counter.compareAndSet(0, 1)) {
    if (++spins > maxSpin) {
        Thread.yield(); // 让出 CPU
    }
}
  1. 自适应自旋

    • 根据锁竞争的历史记录动态调整自旋次数。
    • JVM 的 ReentrantLock 使用了自适应自旋策略。
  2. 结合其他同步机制

    • 在高竞争场景下,可以结合传统锁或其他同步机制(如 Semaphore)来减少自旋的开销。

总结

  • Java CAS 自旋的实现依赖于底层硬件指令

    • Java 层通过 Unsafe 类调用 CAS 操作。
    • JVM 使用 JNI 调用本地代码,最终执行硬件指令(如 CMPXCHG 或 LDREX/STREX)。
  • 自旋的核心是硬件指令的原子性

    • 硬件指令通过总线锁、缓存一致性协议或独占访问标记实现原子性。
  • 自旋的优缺点

    • 自旋适合低竞争场景,但在高竞争场景下可能导致性能问题。

通过硬件指令的支持,Java 的 CAS 自旋能够高效地实现无锁同步,是现代并发编程的重要基础。

Unsafe.compareAndSetInt 方法本身没有自旋等待的操作

Unsafe.compareAndSetInt 方法本身没有自旋等待的操作,它只是一个单次的 CAS(Compare-And-Swap)操作。如果调用 compareAndSetInt 时 CAS 操作失败,它会直接返回 false,而不会自动重试或自旋。

因此,自旋等待的逻辑需要由调用者在 Java 层显式实现,例如通过 while 循环不断调用 compareAndSetInt 来实现自旋。


1. compareAndSetInt 的行为

方法定义

compareAndSetInt 是 Unsafe 类中的一个方法,定义如下:

public final native boolean compareAndSetInt(Object obj, long offset, int expected, int newValue);
行为
  • 单次 CAS 操作

    • 它会尝试将对象 obj 的某个字段(通过 offset 指定)从 expected 值更新为 newValue
    • 如果内存中的值等于 expected,更新成功,返回 true
    • 如果内存中的值不等于 expected,更新失败,返回 false
  • 无自旋逻辑

    • 如果更新失败,compareAndSetInt 不会自动重试或等待,而是直接返回。

2. 自旋等待的实现

为什么需要自旋?
  • 如果 CAS 操作失败,通常是因为其他线程修改了目标值。
  • 在某些场景下(如高并发环境),我们希望线程不断重试,直到 CAS 操作成功。这种重试逻辑需要通过显式的自旋等待实现。
如何实现自旋等待?

自旋等待的逻辑需要在 Java 层通过循环调用 compareAndSetInt 来实现。例如:

while (!unsafe.compareAndSetInt(obj, offset, expected, newValue)) {
    // 自旋等待,直到 CAS 操作成功
}
完整示例

以下是一个基于 Unsafe 类的自旋等待示例:

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class CASExample {
    private static final Unsafe unsafe;
    private static final long valueOffset;

    private volatile int value = 0;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            valueOffset = unsafe.objectFieldOffset(CASExample.class.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public void increment() {
        int expected;
        int newValue;
        do {
            expected = unsafe.getIntVolatile(this, valueOffset); // 获取当前值
            newValue = expected + 1; // 计算新值
        } while (!unsafe.compareAndSetInt(this, valueOffset, expected, newValue)); // 自旋等待
    }

    public int getValue() {
        return value;
    }

    public static void main(String[] args) {
        CASExample example = new CASExample();

        // 启动多个线程并发执行
        for (int i = 0; i < 10; i++) {
            new Thread(example::increment).start();
        }

        try {
            Thread.sleep(1000); // 等待线程执行完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final value: " + example.getValue());
    }
}
输出示例
Final value: 10

3. 为什么 compareAndSetInt 不自带自旋?

1. 灵活性
  • 如果 compareAndSetInt 内部实现了自旋等待,调用者就无法控制自旋的逻辑(如自旋次数、超时策略等)。
  • 通过将自旋逻辑交给调用者,提供了更大的灵活性。
2. 性能考虑
  • 自旋等待可能会导致 CPU 资源的浪费,尤其是在高竞争场景下。
  • 有些场景下,开发者可能希望在 CAS 失败后直接放弃操作,而不是自旋等待。
3. 适用场景
  • 自旋适合短时间的锁竞争场景。如果竞争时间较长,自旋会浪费大量 CPU 资源,通常需要结合其他机制(如 Thread.yield 或阻塞队列)来避免性能问题。

4. 自旋等待的优化策略

1. 限制自旋次数
  • 设置最大自旋次数,避免线程长时间占用 CPU。
  • 示例:
int maxSpin = 1000;
int spins = 0;
while (!unsafe.compareAndSetInt(obj, offset, expected, newValue)) {
    if (++spins > maxSpin) {
        Thread.yield(); // 让出 CPU
    }
}
2. 自适应自旋
  • 根据锁竞争的历史记录动态调整自旋次数。
  • 示例:
int adaptiveSpin = calculateAdaptiveSpin(); // 动态计算自旋次数
while (!unsafe.compareAndSetInt(obj, offset, expected, newValue) && adaptiveSpin-- > 0) {
    // 自旋等待
}
3. 结合阻塞机制
  • 在高竞争场景下,可以结合阻塞机制(如 LockSupport.park)来减少 CPU 资源浪费。
  • 示例:
while (!unsafe.compareAndSetInt(obj, offset, expected, newValue)) {
    LockSupport.parkNanos(100); // 等待一段时间后重试
}

5. 总结

  1. compareAndSetInt 本身没有自旋等待逻辑

    • 它是一个单次 CAS 操作,失败后直接返回 false
  2. 自旋等待需要调用者显式实现

    • 通过 while 循环不断调用 compareAndSetInt 来实现自旋等待。
  3. 为什么不内置自旋?

    • 提供灵活性,允许调用者根据具体场景控制自旋逻辑。
    • 避免在高竞争场景下浪费 CPU 资源。
  4. 优化策略

    • 限制自旋次数、自适应自旋、结合阻塞机制等。

通过这种设计,Java 的 CAS 操作既能满足高性能场景的需求,又能提供足够的灵活性,让开发者根据具体场景选择合适的实现方式。

CAS 的操作系统指令

CAS(Compare-And-Swap,比较并交换)操作依赖于 CPU 提供的硬件指令来实现原子性。这些指令通过硬件机制确保比较和更新操作是不可分割的(原子的),即使在多核、多线程环境下也能保证数据一致性。

以下是常见 CPU 架构中支持 CAS 操作的指令及其工作原理:


1. x86 架构:CMPXCHG 指令

指令简介
  • CMPXCHG(Compare and Exchange)  是 x86 架构中用于实现 CAS 的指令。
  • 它比较寄存器中的值与内存中的值,如果相等,则将寄存器中的新值写入内存;否则,不更新内存,并将内存中的值加载到寄存器中。
工作流程
  1. 比较内存地址中的值(memory)与寄存器中的预期值(EAX)。
  2. 如果相等,则将寄存器中的新值写入内存。
  3. 如果不相等,则将内存中的值加载到寄存器中。
伪代码
CMPXCHG [memory], newValue

等价于以下伪代码:

if (memory == EAX) {
    memory = newValue;
} else {
    EAX = memory;
}
原子性保证
  • 总线锁(Bus Locking)

    • 在单核 CPU 中,LOCK 前缀可以锁定总线,确保指令执行期间其他处理器无法访问内存。
  • 缓存一致性协议(MESI)

    • 在多核 CPU 中,CMPXCHG 使用缓存一致性协议(如 MESI 协议)来确保内存操作的原子性。
    • 当一个核心执行 CMPXCHG 时,会锁定目标内存地址所在的缓存行,其他核心无法同时修改该地址。
示例

以下是使用 CMPXCHG 指令实现 CAS 的示例:

mov eax, expectedValue   ; 将预期值加载到 EAX 寄存器
mov ebx, newValue        ; 将新值加载到 EBX 寄存器
lock cmpxchg [memory], ebx ; 比较并交换内存中的值

2. ARM 架构:LDREX 和 STREX 指令

指令简介
  • ARM 架构中没有直接的 CAS 指令,但通过 LDREX(Load Exclusive)  和 STREX(Store Exclusive)  指令组合实现类似的功能。
  • 这些指令用于实现乐观锁,即先加载值,再尝试更新值,如果期间值被其他线程修改,则更新失败。
工作流程
  1. LDREX

    • 加载内存地址中的值到寄存器,并标记该地址为“独占访问”。
  2. STREX

    • 尝试将新值写入内存地址。
    • 如果期间该地址被其他线程修改,则写入失败,并返回一个失败标志。
伪代码
LDREX R1, [memory]    ; 加载内存中的值到 R1
CMP R1, expectedValue ; 比较 R1 和预期值
BNE fail              ; 如果不相等,跳转到失败分支
STREX R2, newValue, [memory] ; 尝试将新值写入内存
CMP R2, #0            ; 检查 STREX 是否成功
BNE retry             ; 如果失败,重新尝试
原子性保证
  • 独占访问标记(Exclusive Access Tag)

    • ARM 处理器在执行 LDREX 时,会标记目标内存地址为“独占访问”。
    • 如果在 LDREX 和 STREX 之间,其他线程修改了该地址,STREX 会检测到冲突并返回失败。
  • 缓存一致性协议

    • ARM 处理器使用缓存一致性协议(如 MOESI 协议)来确保内存操作的原子性。
示例

以下是 ARM 架构中使用 LDREX 和 STREX 实现 CAS 的示例:

retry:
    LDREX R1, [memory]       ; 加载内存中的值到 R1
    CMP R1, expectedValue    ; 比较 R1 和预期值
    BNE fail                 ; 如果不相等,跳转到失败分支
    STREX R2, newValue, [memory] ; 尝试将新值写入内存
    CMP R2, #0               ; 检查 STREX 是否成功
    BNE retry                ; 如果失败,重新尝试
fail:

3. RISC-V 架构:LR/SC 指令

指令简介
  • RISC-V 架构使用 LR(Load-Reserved)  和 SC(Store-Conditional)  指令组合实现 CAS 操作。
  • 类似于 ARM 的 LDREX 和 STREXLR 和 SC 也基于乐观锁的思想。
工作流程
  1. LR

    • 加载内存地址中的值到寄存器,并标记该地址为“保留”。
  2. SC

    • 尝试将新值写入内存地址。
    • 如果期间该地址被其他线程修改,则写入失败,并返回失败标志。
伪代码
LR R1, [memory]       ; 加载内存中的值到 R1
CMP R1, expectedValue ; 比较 R1 和预期值
BNE fail              ; 如果不相等,跳转到失败分支
SC R2, newValue, [memory] ; 尝试将新值写入内存
BEQ success           ; 如果成功,跳转到成功分支
B retry               ; 如果失败,重新尝试
原子性保证
  • 保留标记(Reservation Tag)

    • 在执行 LR 时,处理器会标记目标内存地址为“保留”。
    • 如果在 LR 和 SC 之间,其他线程修改了该地址,SC 会检测到冲突并返回失败。
  • 缓存一致性协议

    • RISC-V 处理器使用缓存一致性协议来确保内存操作的原子性。

4. 原子性保证的硬件机制

1. 总线锁(Bus Locking)
  • 在单核处理器中,CAS 操作通过锁定总线来实现原子性。
  • 当一个处理器执行 CAS 操作时,会锁定目标内存地址所在的总线,其他处理器无法访问该地址。
2. 缓存一致性协议
  • 在多核处理器中,CAS 操作通过缓存一致性协议(如 MESI、MOESI)来实现原子性。
  • 当一个核心执行 CAS 操作时,会锁定目标内存地址所在的缓存行,其他核心无法同时修改该地址。
3. 独占访问标记
  • ARM 和 RISC-V 架构通过独占访问标记(Exclusive Access Tag 或 Reservation Tag)来实现原子性。
  • 在加载和存储之间,如果其他线程修改了目标地址,存储操作会失败。

总结

架构指令原子性机制特点
x86CMPXCHG总线锁、缓存一致性协议(MESI)直接支持 CAS 操作,性能高,广泛应用于多核处理器。
ARMLDREX 和 STREX独占访问标记、缓存一致性协议基于乐观锁的实现,适合低功耗设备和嵌入式系统。
RISC-VLR 和 SC保留标记、缓存一致性协议类似 ARM 的实现,支持灵活的原子操作。

通过这些硬件指令,CAS 操作能够在多线程环境下高效地实现原子性,避免了传统锁机制的开销,是现代并发编程的核心技术之一。

Java 的 Unsafe 类的 compareAndSwapInt 方法,是怎么访问到操作系统指令的?

在 Java 中,Unsafe 类的 compareAndSwapInt 方法是通过 JNI(Java Native Interface)  调用底层的本地代码(Native Code)实现的,而这些本地代码最终会调用操作系统提供的硬件指令(如 x86 架构的 CMPXCHG 指令或 ARM 架构的 LDREX/STREX 指令)。以下是详细的工作原理和流程:


1. Unsafe 类简介

  • Unsafe 类 是 Java 中一个位于 sun.misc 包下的类,提供了一些底层操作的方法,比如直接操作内存、线程调度、CAS 操作等。
  • 它是 Java 中实现高性能并发工具(如 AtomicIntegerConcurrentHashMap)的核心工具。
  • compareAndSwapInt 方法 是 Unsafe 类中用于实现 CAS 操作的一个方法。

2. compareAndSwapInt 方法的定义

在 Unsafe 类中,compareAndSwapInt 方法的定义如下:

public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int newValue);
参数说明
  • obj:要操作的对象。
  • offset:对象中字段的内存偏移量(通过 Unsafe.objectFieldOffset 获取)。
  • expected:预期值。
  • newValue:要更新的新值。
关键点
  • native 关键字:表示该方法是一个本地方法,其实现由本地代码(C/C++)提供,而不是用 Java 编写。
  • 返回值true 表示 CAS 操作成功,false 表示失败。

3. compareAndSwapInt 的实现流程

1. Java 层调用 Unsafe.compareAndSwapInt
  • 当 Java 程序调用 compareAndSwapInt 方法时,JVM 会通过 JNI 跳转到本地代码的实现。
2. JNI 调用本地代码
  • Unsafe 类的本地方法由 JVM 的 C/C++ 实现提供。
  • 在 OpenJDK 中,compareAndSwapInt 的实现位于 unsafe.cpp 文件中。
3. 本地代码调用硬件指令
  • 本地代码通过调用操作系统提供的底层 API 或直接使用汇编指令(如 CMPXCHG)来实现 CAS 操作。
  • 这些硬件指令由 CPU 提供,能够保证比较和交换操作的原子性。

4. OpenJDK 中的实现细节

1. Unsafe 类的本地方法注册
  • 在 JVM 启动时,Unsafe 类的本地方法会通过 JNI 注册到 JVM 中。
  • 在 OpenJDK 的 unsafe.cpp 文件中,可以看到 compareAndSwapInt 的实现。
2. compareAndSwapInt 的核心实现

以下是 OpenJDK 中 compareAndSwapInt 的核心代码(简化版):

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint expected, jint newValue)) {
    oop p = JNIHandles::resolve(obj); // 获取对象的内存地址
    jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // 计算字段的内存地址
    return (jint)(Atomic::cmpxchg(newValue, addr, expected) == expected); // 调用底层 CAS 实现
} UNSAFE_END
3. Atomic::cmpxchg 的实现
  • Atomic::cmpxchg 是一个封装了硬件指令的函数,用于执行 CAS 操作。
  • 在 x86 架构中,它会调用 CMPXCHG 指令:
inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
    jint old_value;
    __asm__ volatile ("lock; cmpxchgl %1, %2"
                      : "=a" (old_value)
                      : "r" (exchange_value), "m" (*dest), "0" (compare_value)
                      : "memory");
    return old_value;
}
4. 硬件指令的执行
  • 在 x86 架构中,CMPXCHG 指令会比较内存中的值和寄存器中的值,如果相等,则更新内存中的值。
  • 在 ARM 架构中,类似的操作由 LDREX 和 STREX 指令完成。

5. 硬件级别的原子性保证

1. 总线锁(Bus Locking)
  • 在单核 CPU 中,CAS 操作通过锁定总线来实现原子性。
  • 当一个处理器执行 CAS 操作时,会锁定目标内存地址所在的总线,其他处理器无法访问该地址。
2. 缓存一致性协议
  • 在多核 CPU 中,CAS 操作通过缓存一致性协议(如 MESI 协议)来实现原子性。
  • 当一个核心执行 CAS 操作时,会锁定目标内存地址所在的缓存行,其他核心无法同时修改该地址。
3. 指令级别的原子性
  • 硬件指令(如 CMPXCHG 或 LDREX/STREX)由 CPU 提供,能够保证比较和交换操作在一个时钟周期内完成,避免了中断或其他线程的干扰。

6. 总结:从 Java 到硬件的完整流程

  1. Java 层

    • Java 程序调用 Unsafe.compareAndSwapInt 方法。
  2. JNI 层

    • JVM 通过 JNI 跳转到本地代码(C/C++)的实现。
  3. 本地代码层

    • 本地代码调用 Atomic::cmpxchg 函数,封装了硬件指令。
  4. 硬件层

    • CPU 执行硬件指令(如 CMPXCHG 或 LDREX/STREX),保证 CAS 操作的原子性。

7. 为什么 Unsafe 能直接调用硬件指令?

  • JNI 的作用

    • Unsafe 类通过 JNI 调用本地代码,而本地代码可以直接使用汇编指令或操作系统提供的 API。
    • 这种机制使得 Java 能够突破 JVM 的限制,直接访问底层硬件。
  • 硬件指令的支持

    • CPU 提供了专门的指令(如 CMPXCHG)来实现原子操作,操作系统和编译器会封装这些指令供开发者使用。
  • JVM 的优化

    • JVM 对 Unsafe 类的调用进行了高度优化,确保其性能接近于直接调用硬件指令。

8. 关键点总结

  • Unsafe.compareAndSwapInt 的实现

    • 通过 JNI 调用本地代码,最终使用硬件指令(如 CMPXCHG 或 LDREX/STREX)实现 CAS 操作。
  • 硬件级别的原子性

    • 硬件指令通过总线锁、缓存一致性协议和指令级别的原子性保证 CAS 操作的正确性。
  • 性能优势

    • 这种机制避免了传统锁的开销,是实现高性能并发工具的基础。

通过这种设计,Java 的 Unsafe 类能够高效地利用底层硬件的能力,为并发编程提供强大的支持。