ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CAS 深度解析:从硬件指令到源码实现的全面剖析

CAS 深度解析:从硬件指令到源码实现的全面剖析 一、引言并发编程的“原子性”难题在多线程编程中原子性是最基本也最棘手的问题之一。经典的i操作看似一行代码在底层却被拆分为“读取-修改-写入”三个步骤。多个线程同时执行时会出现数据不一致的问题。传统的解决方案是加锁synchronized或ReentrantLock但锁会带来线程上下文切换、阻塞唤醒的开销。在低竞争场景下加锁的性能损失远大于操作本身。有没有一种机制既保证原子性又避免锁的开销CASCompare-And-Swap比较并交换给出了答案。它是一种乐观锁技术允许线程在不加锁的情况下安全地更新共享变量是现代并发编程的基石。CAS的核心思想先读取变量的当前值然后比较当前值是否与预期值一致如果一致则交换为新值否则失败重试。整个过程是硬件级别的原子操作。二、什么是 CAS2.1 定义与操作原语CAS 是一个原子操作包含三个操作数V要操作的内存位置变量E期望的旧值Expected valueN要写入的新值New value执行逻辑读取 V 的当前值与 E 比较如果相等则将 V 更新为 N否则什么都不做。无论成功与否都返回 V 的旧值。// CAS 的伪代码表示实际由硬件原子完成 boolean compareAndSwap(V, E, N) { if (V E) { V N; return true; } return false; }2.2 为什么 CAS 是“无锁”的CAS 操作由CPU 硬件指令直接支持在单条指令周期内完成不会被线程调度中断因此天然线程安全。使用 CAS 实现的同步机制线程不会阻塞也就没有上下文切换的开销被称为非阻塞同步。三、Java 中的 CAS 实现Unsafe 类在 Java 中CAS 操作是通过sun.misc.Unsafe类提供的本地方法实现的。Unsafe是 Java 中用于执行底层、不安全的操作的类它绕过了 Java 的访问控制直接操作内存。3.1 Unsafe 的关键方法// Unsafe 类中的 CAS 方法简化 public final native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update); public final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update); public final native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);obj要操作的对象offset该对象中字段的内存偏移量通过Unsafe.objectFieldOffset()获得expect期望值update新值3.2 获取 Unsafe 实例Unsafe是单例模式构造方法私有只能通过Unsafe.getUnsafe()获取但该方法会检查调用类是否由 Bootstrap ClassLoader 加载普通应用无法直接调用。// 通过反射获取 Unsafe 实例 public static Unsafe getUnsafe() { try { Field field Unsafe.class.getDeclaredField(theUnsafe); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { throw new RuntimeException(e); } }注意官方不建议开发者直接使用Unsafe因为它的操作不安全可能导致 JVM 崩溃。但它为java.util.concurrent包提供了底层支撑我们学习源码即可不鼓励在业务代码中使用。3.3 使用 Unsafe 实现自定义原子计数器import sun.misc.Unsafe; import java.lang.reflect.Field; public class AtomicCounter { private volatile long value; private static final Unsafe UNSAFE; private static final long VALUE_OFFSET; static { try { Field field Unsafe.class.getDeclaredField(theUnsafe); field.setAccessible(true); UNSAFE (Unsafe) field.get(null); VALUE_OFFSET UNSAFE.objectFieldOffset(AtomicCounter.class.getDeclaredField(value)); } catch (Exception e) { throw new RuntimeException(e); } } public AtomicCounter(long initialValue) { this.value initialValue; } /** * 原子性加 1返回旧值 */ public long getAndIncrement() { return UNSAFE.getAndAddLong(this, VALUE_OFFSET, 1L); } /** * 自定义 CAS 更新 */ public boolean compareAndSet(long expect, long update) { return UNSAFE.compareAndSwapLong(this, VALUE_OFFSET, expect, update); } public long get() { return value; } }四、源码阅读AtomicLong 的 CAS 实现AtomicLong是 Java 原子包中最常用的类之一它的底层完全依赖Unsafe进行 CAS 操作。4.1 成员变量与初始化public class AtomicLong extends Number implements java.io.Serializable { private static final Unsafe U Unsafe.getUnsafe(); private static final long VALUE; // 实际存储的值volatile 保证可见性 private volatile long value; static { try { // 获取 value 字段的内存偏移量 VALUE U.objectFieldOffset(AtomicLong.class.getDeclaredField(value)); } catch (ReflectiveOperationException e) { throw new Error(e); } } }4.2 getAndIncrement() 源码public final long getAndIncrement() { // 委托给 Unsafe 的 getAndAddLong return U.getAndAddLong(this, VALUE, 1L); }4.3 Unsafe.getAndAddLong() 源码// sun.misc.Unsafe public final long getAndAddLong(Object obj, long offset, long delta) { long v; do { // 循环读取当前值 v getLongVolatile(obj, offset); // CAS 尝试更新失败则重试 } while (!compareAndSwapLong(obj, offset, v, v delta)); return v; }核心逻辑getLongVolatile读取变量当前值带volatile语义从主内存读取compareAndSwapLong通过 JNI 调用 CPU 指令执行 CAS自旋如果 CAS 失败循环重试直到成功4.4 JNI 层面的实现HotSpot 源码Unsafe.compareAndSwapLong最终调用的是 OpenJDK 中的 JVM 函数// openjdk/hotspot/src/share/vm/prims/unsafe.cpp UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapLong(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jlong e, jlong x)) { oop p JNIHandles::resolve(obj); volatile jlong* addr (volatile jlong*)index_oop_from_field_offset_long(p, offset); // 调用 Atomic::cmpxchg 模板函数最终映射到 CPU 的 CMPXCHG 指令 return Atomic::cmpxchg(addr, e, x) e; } UNSAFE_END对于 x86 架构Atomic::cmpxchg最终会调用LOCK CMPXCHG指令或LOCK CMPXCHGQ对于 long 类型在多核 CPU 中通过总线锁定或缓存锁定保证原子性。五、CAS 的三大问题与解决方案5.1 ABA 问题问题描述线程 T1 读取值 A然后被挂起线程 T2 将 A 改为 B 又改回 AT1 恢复后 CAS 发现值还是 A误以为没有被修改过执行更新。解决方案AtomicStampedReference携带版本号stamp每次修改版本号1AtomicMarkableReference携带布尔标记位import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo { public static void main(String[] args) { AtomicStampedReferenceInteger ref new AtomicStampedReference(100, 0); // 线程1期望值为100版本号为0尝试改为101版本号1 int stamp ref.getStamp(); boolean success ref.compareAndSet(100, 101, stamp, stamp 1); System.out.println(CAS成功 success 新值 ref.getReference()); // 即使别的线程将值改回100版本号已变CAS会失败 } }5.2 自旋开销问题问题描述在高并发下CAS 失败率高大量线程自旋重试消耗 CPU。解决方案自适应自旋JVM 内部优化使用LongAdder替代AtomicLong将竞争分散到多个 Cell失败时让出 CPUThread.yield()或短暂休眠5.3 单变量原子性限制问题描述CAS 只能原子操作一个共享变量。解决方案使用AtomicReference包装多个变量或者使用Lock机制锁可以原子操作多个变量六、性能分析6.1 CAS vs 锁的性能对比场景CAS自旋锁synchronized低竞争极快无上下文切换较慢锁获取/释放开销中竞争较快少量自旋一般高竞争急剧下降大量自旋较优阻塞调度结论CAS 适合低/中竞争场景高竞争下建议使用锁或LongAdder等分散竞争的方案。6.2 JVM 对 CAS 的优化锁粗化将多次 CAS 合并为一次锁消除逃逸分析后确认无竞争时消除 CAS自适应自旋根据历史成功率调整自旋次数七、Java 原子包java.util.concurrent.atomic全景java.util.concurrent.atomic包提供了丰富原子类底层均基于 CAS分类类名说明基础类型AtomicInteger、AtomicLong、AtomicBoolean原子更新基础类型数组AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray原子更新数组元素引用AtomicReference、AtomicMarkableReference、AtomicStampedReference原子更新引用类型字段更新器AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater原子更新对象字段基于反射累加器JDK 8LongAdder、DoubleAdder高并发统计计数分散竞争累积器LongAccumulator、DoubleAccumulator自定义累积操作7.1 AtomicReference原子更新引用public class AtomicReferenceDemo { static class User { String name; int age; User(String name, int age) { this.name name; this.age age; } } public static void main(String[] args) { User oldUser new User(old, 20); AtomicReferenceUser ref new AtomicReference(oldUser); User newUser new User(new, 25); // CAS 更新引用 ref.compareAndSet(oldUser, newUser); System.out.println(ref.get().name); // new } }7.2 AtomicIntegerFieldUpdater轻量级字段更新public class FieldUpdaterDemo { static class Candidate { volatile int score; // 必须 volatile } private static final AtomicIntegerFieldUpdaterCandidate SCORE_UPDATER AtomicIntegerFieldUpdater.newUpdater(Candidate.class, score); public static void main(String[] args) { Candidate candidate new Candidate(); SCORE_UPDATER.incrementAndGet(candidate); // 原子1 System.out.println(candidate.score); // 1 } }八、实战基于 CAS 实现简易限流器package com.example.ratelimiter; import java.util.concurrent.atomic.AtomicLong; /** * 基于 CAS 固定时间窗口的简易限流器 * * 设计目标限制每秒最大请求数固定时间窗口算法 * * 核心原理 * - 使用 AtomicLong 作为计数器保证原子性 * - 使用 volatile 记录窗口起始时间确保多线程可见性 * - 使用双重检查锁DCL确保重置逻辑只执行一次 * - 使用 CAS 自旋实现无锁递增 * * 注意这是固定时间窗口每1秒重置不是真正的滑动窗口。 * 固定窗口存在临界突发问题如窗口边界处允许双倍流量 * 适合对流量均匀性要求不高的场景。 * * author YourName */ public class FixedWindowRateLimiter { /** * 每秒允许的最大请求数限流阈值 */ private final long maxRequestsPerSecond; /** * 计数器统计当前窗口内已通过的请求数 * 使用 AtomicLong 保证原子性避免多线程竞争 */ private final AtomicLong counter new AtomicLong(0); /** * 当前窗口的起始时间戳毫秒 * volatile 保证多线程间的可见性 */ private volatile long lastResetTime System.currentTimeMillis(); /** * 构造限流器 * param maxRequestsPerSecond 每秒最大请求数 */ public FixedWindowRateLimiter(long maxRequestsPerSecond) { this.maxRequestsPerSecond maxRequestsPerSecond; } /** * 尝试获取令牌是否允许通过 * * 核心逻辑 * 1. 检查是否进入下一个时间窗口距离上次重置 ≥ 1000ms * 2. 如果是使用双重检查锁重置计数器 * 3. 使用 CAS 自旋尝试递增计数器 * 4. 如果递增成功且未超过阈值返回 true允许通过 * 5. 否则返回 false拒绝 * * return true 允许通过false 被限流拒绝 */ public boolean tryAcquire() { long now System.currentTimeMillis(); // 阶段1检查是否需要重置窗口双重检查锁 // 为什么用 DCL既保证线程安全又避免每次都要加锁带来的性能损耗 if (now - lastResetTime 1000) { synchronized (this) { // 二次检查防止多个线程同时进入同步块后重复重置 if (now - lastResetTime 1000) { counter.set(0); // 重置计数器 lastResetTime now; // 更新窗口起始时间 } } } // 阶段2CAS 自旋递增计数器 while (true) { long current counter.get(); // 如果当前计数已超过阈值直接拒绝不占用计数 if (current maxRequestsPerSecond) { return false; } // CAS 尝试将计数 1 if (counter.compareAndSet(current, current 1)) { return true; // 成功获取令牌 } // CAS 失败说明有其他线程抢先修改了计数器重试 } } /** * 获取当前窗口内的请求计数用于监控 */ public long getCurrentCount() { return counter.get(); } /** * 获取当前窗口剩余时间毫秒 */ public long getRemainingMillis() { long now System.currentTimeMillis(); long elapsed now - lastResetTime; return elapsed 1000 ? 0 : 1000 - elapsed; } // 测试入口 public static void main(String[] args) throws InterruptedException { // 创建限流器每秒最多 5 个请求 FixedWindowRateLimiter limiter new FixedWindowRateLimiter(5); System.out.println( 第1轮连续请求10次预期前5次通过后5次拒绝 ); for (int i 0; i 10; i) { boolean allowed limiter.tryAcquire(); System.out.println(请求 i : (allowed ? ✅ 允许 : ❌ 拒绝)); } System.out.println(\n 等待1秒窗口重置 ); Thread.sleep(1000); System.out.println( 第2轮重置后再请求预期允许 ); boolean allowed limiter.tryAcquire(); System.out.println(请求: (allowed ? ✅ 允许 : ❌ 拒绝)); System.out.println(\n当前计数: limiter.getCurrentCount() 剩余重置时间: limiter.getRemainingMillis() ms); } }输出请求 0: 允许 请求 1: 允许 ... 请求 5: 拒绝 请求 6: 拒绝 1秒后再次请求: 允许九、注意事项与最佳实践✅ 推荐做法优先使用java.util.concurrent.atomic包提供的原子类不要直接使用Unsafe在低/中竞争场景下使用 CAS高竞争场景考虑LongAdder或锁合理设置自旋上限避免无限循环消耗 CPU使用AtomicStampedReference解决 ABA 问题利用AtomicReference实现无锁栈、无锁队列十、总结CAS 是 Java 并发包java.util.concurrent的基石。从AtomicInteger到ConcurrentHashMap从ReentrantLock的 AQS 到LongAdder的分段计数处处都有 CAS 的身影。维度要点操作原语Compare-And-Swap比较并交换硬件级原子指令CMPXCHGJava 实现Unsafe类提供本地方法Atomic*类封装使用核心优势无锁、非阻塞、低开销无上下文切换主要问题ABA 问题、自旋开销、单变量限制解决方案AtomicStampedReference、LongAdder、AtomicReference适用场景计数器、状态标志、队列/栈实现、轻量级同步一句话总结CAS 用“失败重试”替代了“阻塞等待”在无锁状态下实现了线程安全的原子更新是 Java 高并发性能的底层密码。理解 CAS就等于拿到了理解 JUC 包的金钥匙。
返回列表