java中cas是原子的,但仅限单变量单次操作;映射为cmpxchg或ldxr/stxr等硬件指令,不可中断;多步逻辑、aba问题、自旋写法错误等均使其失去原子性保障。

Java里CAS到底是不是“原子”的?
是,但仅限于单个变量的单次操作——Unsafe.compareAndSwapInt 这类调用在CPU指令层面映射为 cmpxchg(x86)或 ldxr/stxr(ARM),硬件保证其不可中断。可一旦你把它当“事务”用,比如先读A再CAS B,中间穿插了别的逻辑,那就不是原子的了。
常见错误现象:ABA问题、自旋空转导致CPU飙升、误以为 AtomicInteger.incrementAndGet() 能保护整个业务流程。
- 使用场景:计数器、无锁队列节点链接、状态机切换(如从
INIT到RUNNING) -
Unsafe.compareAndSwapObject和compareAndSwapInt的参数顺序一致,但第三个参数是期望值,第四个是新值;传错会静默失败 - 注意JDK 9+后
Unsafe被限制,反射获取实例可能抛IllegalAccessException,得用jdk.internal.misc.Unsafe+ JVM参数--add-opens
为什么直接用Unsafe比AtomicInteger更危险?
因为 AtomicInteger 封装了内存屏障(如 getAndIncrement 底层插入 lock xadd)、volatile语义、以及对 Unsafe 的安全封装;而裸用 Unsafe 时,你得自己决定在哪加 Unsafe.storeFence()、Unsafe.loadFence(),稍有遗漏就会出现可见性问题。
典型坑:Unsafe.putInt(obj, offset, 1) 不带任何屏障,其他线程可能永远看不到这个写入;而 AtomicInteger.set(1) 自动保证后续读能看见。
立即学习“Java免费学习笔记(深入)”;
- 性能影响:裸
Unsafe确实略快(少一层方法调用和检查),但差异在纳秒级,不值得用正确性换 - 兼容性风险:不同JVM实现对
Unsafe的支持细节有差异,比如某些嵌入式JVM压根没实现compareAndSwapLong - 示例:想手动实现一个无锁栈,别直接用
Unsafe.compareAndSwapObject(head, offset, expect, update)就完事——得确保head字段本身是volatile或用Unsafe.getObjectVolatile读取
“自旋等待”卡住不动?不是CAS没生效,是条件写错了
CAS失败只返回 false,它不阻塞也不重试;所谓“自旋”,是你自己写的 while (!cas(...)) { }。如果循环体里没更新期望值,就会无限失败。
错误现象:while (unsafe.compareAndSwapInt(obj, offset, expected, newValue)) ——这里 expected 没变,第二次就必然失败,死循环。
- 正确做法:每次失败后,重新读取当前值作为新的
expected,例如int cur = unsafe.getIntVolatile(obj, offset); while (!unsafe.compareAndSwapInt(obj, offset, cur, cur + 1)) { cur = unsafe.getIntVolatile(obj, offset); } - 注意
getIntVolatile和getInt区别:后者不保证可见性,可能读到过期缓存值 - 别在自旋里做耗时操作(如IO、锁、日志),否则不仅卡住自己,还拖垮整个CPU核
Unsafe的objectFieldOffset怎么算?别硬记偏移量
偏移量不是固定值,取决于JVM对象布局(OOP layout)、字段顺序、是否开启压缩指针(-XX:+UseCompressedOops)。手算容易错,也难移植。
常见错误:把 static final long OFFSET = 16 写死在代码里,换个JVM版本或GC策略就崩,报 IllegalArgumentException: field offset not aligned。
- 必须用
Unsafe.objectFieldOffset(Field)动态获取,哪怕多一次反射调用 - 字段必须是
public或通过setAccessible(true)绕过访问控制,否则getDeclaredField找不到 - 示例:
Field f = MyCounter.class.getDeclaredField("value"); f.setAccessible(true); long offset = unsafe.objectFieldOffset(f);
真正麻烦的从来不是理解CAS原理,而是记住哪些地方要加屏障、哪些字段必须volatile、哪些offset不能硬编码——这些细节一漏,bug就藏在高并发偶现的角落里,很难复现。










