CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。

CSA 原理

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的。
java.util.concurrent 下面的源码中,Atomic, ReentrantLock 都使用了Unsafe类中的方法来保证并发的安全性。

CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁,JDK中大量使用了CAS来更新数据而防止加锁来保持原子更新。

CAS 操作包含三个操作数 :内存偏移量位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

源码分析

下面来看一下 java.util.concurrent.atomic.AtomicInteger.javagetAndIncrement()getAndDecrement()是如何利用CAS实现原子性操作的。

AtomicInteger 源码解析

// 使用 unsafe 类的原子操作方式
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;static {try {//计算变量 value 在类对象中的偏移量valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }
}

valueOffset 字段表示"value" 内存位置,在compareAndSwap 方法 ,第二个参数会用到.

关于偏移量

Unsafe 调用C 语言可以通过偏移量对变量进行操作

//volatile变量value
private volatile int value;/*** 创建具有给定初始值的新 AtomicInteger** @param initialValue 初始值*/
public AtomicInteger(int initialValue) {value = initialValue;
}//返回当前的值
public final int get() {return value;
}
//原子更新为新值并返回旧值
public final int getAndSet(int newValue) {return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//最终会设置成新值
public final void lazySet(int newValue) {unsafe.putOrderedInt(this, valueOffset, newValue);
}
//如果输入的值等于预期值,则以原子方式更新为新值
public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//方法相当于原子性的 ++i
public final int getAndIncrement() {//三个参数,1、当前的实例 2、value实例变量的偏移量 3、递增的值。return unsafe.getAndAddInt(this, valueOffset, 1);
}
//方法相当于原子性的 --i
public final int getAndDecrement() {//三个参数,1、当前的实例 2、value实例变量的偏移量 3、递减的值。return unsafe.getAndAddInt(this, valueOffset, -1);
}

实现逻辑封装在 Unsafe 中 getAndAddInt 方法,继续往下看,Unsafe 源码解析

Unsafe 源码解析

在JDK8中追踪可见sun.misc.Unsafe这个类是无法看见源码的,打开openjdk8源码看

文件:openjdk-8-src-b132-03_mar_2014.zip

目录:openjdk\jdk\src\share\classes\sun\misc\Unsafe.java

通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。要想使用Unsafe类需要用一些比较tricky的办法。Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。

下面是sun.misc.Unsafe.java类源码


//获取Unsafe实例静态方法
@CallerSensitive
public static Unsafe getUnsafe() {Class<?> caller = Reflection.getCallerClass();if (!VM.isSystemDomainLoader(caller.getClassLoader()))throw new SecurityException("Unsafe");return theUnsafe;
}

网上也有一些办法来用主类加载器加载用户代码,最简单方法是利用Java反射,方法如下:

private static Unsafe unsafe;static {try {//通过反射获取rt.jar下的Unsafe类Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);unsafe = (Unsafe) field.get(null);} catch (Exception e) {System.out.println("Get Unsafe instance occur error" + e);}
}

获取到Unsafe实例之后,我们就可以为所欲为了。Unsafe类提供了以下这些功能:

https://www.cnblogs.com/pkufork/p/java_unsafe.html

    //native硬件级别的原子操作//类似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);//内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)public final int getAndAddInt(Object o, long offset, int delta) {int v;do {//获取对象内存地址偏移量上的数值vv = getIntVolatile(o, offset);//如果现在还是v,设置为 v + delta,否则返回false,继续循环再次重试.} while (!compareAndSwapInt(o, offset, v, v + delta));return v;}

利用 Unsafe 类的 JNI compareAndSwapInt 方法实现,使用CAS实现一个原子操作更新,

compareAndSwapInt 四个参数

1、当前的实例
2、实例变量的内存地址偏移量
3、预期的旧值
4、要更新的值

unsafe.cpp 深层次解析

// unsafe.cpp
/** 这个看起来好像不像一个函数,不过不用担心,不是重点。UNSAFE_ENTRY 和 UNSAFE_END 都是宏,* 在预编译期间会被替换成真正的代码。下面的 jboolean、jlong 和 jint 等是一些类型定义(typedef):** 省略部分内容*/
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt");oop p = JNIHandles::resolve(obj);// 根据偏移量,计算 value 的地址。这里的 offset 就是 AtomaicInteger 中的 valueOffsetjint* addr = (jint *) index_oop_from_field_offset_long(p, offset);// 调用 Atomic 中的函数 cmpxchg,该函数声明于 Atomic.hpp 中return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END// atomic.cpp
unsigned Atomic::cmpxchg(unsigned int exchange_value, volatile unsigned int* dest, unsigned int compare_value) {assert(sizeof(unsigned int) == sizeof(jint), "more work to do");/** 根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载* 函数。相关的预编译逻辑如下:** atomic.inline.hpp:*    #include "runtime/atomic.hpp"*  *    // Linux*    #ifdef TARGET_OS_ARCH_linux_x86*    # include "atomic_linux_x86.inline.hpp"*    #endif* *    // 省略部分代码*  *    // Windows*    #ifdef TARGET_OS_ARCH_windows_x86*    # include "atomic_windows_x86.inline.hpp"*    #endif*  *    // BSD*    #ifdef TARGET_OS_ARCH_bsd_x86*    # include "atomic_bsd_x86.inline.hpp"*    #endif** 接下来分析 atomic_windows_x86.inline.hpp 中的 cmpxchg 函数实现*/return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,(jint)compare_value);
}

上面的分析看起来比较多,不过主流程并不复杂。如果不纠结于代码细节,还是比较容易看懂的。接下来,我会分析 Windows 平台下的 Atomic::cmpxchg 函数。继续往下看吧。

// atomic_windows_x86.inline.hpp
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \__asm je L0      \__asm _emit 0xF0 \__asm L0:inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {// alternative for InterlockedCompareExchangeint mp = os::is_MP();__asm {mov edx, destmov ecx, exchange_valuemov eax, compare_valueLOCK_IF_MP(mp)cmpxchg dword ptr [edx], ecx}
}

上面的代码由 LOCK_IF_MP 预编译标识符和 cmpxchg 函数组成。为了看到更清楚一些,我们将 cmpxchg 函数中的 LOCK_IF_MP 替换为实际内容。如下:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {// 判断是否是多核 CPUint mp = os::is_MP();__asm {// 将参数值放入寄存器中mov edx, dest    // 注意: dest 是指针类型,这里是把内存地址存入 edx 寄存器中mov ecx, exchange_valuemov eax, compare_value// LOCK_IF_MPcmp mp, 0/** 如果 mp = 0,表明是线程运行在单核 CPU 环境下。此时 je 会跳转到 L0 标记处,* 也就是越过 _emit 0xF0 指令,直接执行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令* 前加 lock 前缀。*/je L0/** 0xF0 是 lock 前缀的机器码,这里没有使用 lock,而是直接使用了机器码的形式。至于这样做的* 原因可以参考知乎的一个回答:*     https://www.zhihu.com/question/50878124/answer/123099923*/_emit 0xF0
L0:/** 比较并交换。简单解释一下下面这条指令,熟悉汇编的朋友可以略过下面的解释:*   cmpxchg: 即“比较并交换”指令*   dword: 全称是 double word,在 x86/x64 体系中,一个*          word = 2 byte,dword = 4 byte = 32 bit*   ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元*   [edx]: [...] 表示一个内存单元,edx 是寄存器,dest 指针值存放在 edx 中。*          那么 [edx] 表示内存地址为 dest 的内存单元*        * 这一条指令的意思就是,将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值* 进行对比,如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中。*/cmpxchg dword ptr [edx], ecx}
}

到这里 CAS 的实现过程就讲了,CAS 的实现离不开处理器的支持。以上这么多代码,其实核心代码就是一条带 lock 前缀的 cmpxchg 指令,即lock cmpxchg dword ptr [edx], ecx

通过上述的分析,可以发现AtomicInteger原子类的内部几乎是基于前面分析过Unsafe类中的CAS相关操作的方法实现的,这也同时证明AtomicInteger getAndIncrement自增操作实现过程,是基于无锁实现的。

CAS的ABA问题及其解决方案

假设这样一种场景,当第一个线程执行CAS(V,E,U)操作。在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图:

这就是典型的CAS的ABA问题,一般情况这种情况发现的概率比较小,可能发生了也不会造成什么问题,比如说我们对某个做加减法,不关心数字的过程,那么发生ABA问题也没啥关系。但是在某些情况下还是需要防止的,那么该如何解决呢?在Java中解决ABA问题,我们可以使用以下原子类

AtomicStampedReference类

AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境

底层实现为: 通过Pair私有内部类存储数据和时间戳, 并构造volatile修饰的私有实例

接着看 java.util.concurrent.atomic.AtomicStampedReference类的compareAndSet()方法的实现:

private static class Pair<T> {final T reference;final int stamp;//最好不要重复的一个数据,决定数据是否能设置成功,时间戳会重复private Pair(T reference, int stamp) {this.reference = reference;this.stamp = stamp;}static <T> Pair<T> of(T reference, int stamp) {return new Pair<T>(reference, stamp);}
}

同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,

单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe类中的compareAndSwapObject方法

到这我们就很清晰AtomicStampedReference的内部实现思想了,

通过一个键值对Pair存储数据和时间戳,在更新时对数据和时间戳进行比较,

只有两者都符合预期才会调用UnsafecompareAndSwapObject方法执行数值和时间戳替换,也就避免了ABA的问题。

/*** 原子更新带有版本号的引用类型。* 该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号。* 可以解决使用CAS进行原子更新时,可能出现的ABA问题。*/
public class AtomicStampedReference<V> {//静态内部类Pair将对应的引用类型和版本号stamp作为它的成员private static class Pair<T> {//最好不要重复的一个数据,决定数据是否能设置成功,建议时间戳final T reference;final int stamp;private Pair(T reference, int stamp) {this.reference = reference;this.stamp = stamp;}//根据reference和stamp来生成一个Pair的实例static <T> Pair<T> of(T reference, int stamp) {return new Pair<T>(reference, stamp);}}//作为一个整体的pair变量被volatile修饰private volatile Pair<V> pair;//构造方法,参数分别是初始引用变量的值和初始版本号public AtomicStampedReference(V initialRef, int initialStamp) {pair = Pair.of(initialRef, initialStamp);}....private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);//获取pair成员的偏移地址static long objectFieldOffset(sun.misc.Unsafe UNSAFE,String field, Class<?> klazz) {try {return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));} catch (NoSuchFieldException e) {NoSuchFieldError error = new NoSuchFieldError(field);error.initCause(e);throw error;}}
}
/*** @param 期望(老的)引用* @param       (新的)引用数据* @param 期望(老的)标志stamp(时间戳)值* @param       (新的)标志stamp(时间戳)值* @return 是否成功*/
public boolean compareAndSet(V expectedReference,V   newReference,int expectedStamp,int newStamp) {Pair<V> current = pair;return// 期望(老的)引用 == 当前引用expectedReference == current.reference &&// 期望(老的)标志stamp(时间戳)值 == 当前标志stamp(时间戳)值expectedStamp == current.stamp &&// (新的)引用数据 == 当前引用数据 并且 (新的)标志stamp(时间戳)值 ==当前标志stamp(时间戳)值((newReference == current.reference && newStamp == current.stamp) ||#原子更新值casPair(current, Pair.of(newReference, newStamp)));}//当引用类型的值与期望的一致的时候,原子的更改版本号为新的值。该方法只修改版本号,不修改引用变量的值,成功返回true
public boolean attemptStamp(V expectedReference, int newStamp) {Pair<V> current = pair;returnexpectedReference == current.reference &&(newStamp == current.stamp ||casPair(current, Pair.of(expectedReference, newStamp)));
}/*** CAS真正实现方法*/
private boolean casPair(Pair<V> cmp, Pair<V> val) {return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

期望 Pair cmp(A) == 当前内存存偏移量位置 Pair(V),就更新值 Pair val(B)成功返回true 否则 false

public static void main(String[] args) {AtomicStampedReference<Integer> num = new AtomicStampedReference<Integer>(1, 0);Integer i = num.getReference();int stamped = num.getStamp();if (num.compareAndSet(i, i + 1, stamped, stamped + 1)) {System.out.println("测试成功");}
}

通过以上原子更新方法,可见 AtomicStampedReference就是利用了Unsafe的CAS方法+Volatile关键字对存储实际的引用变量和int的版本号的Pair实例进行更新。

参考:
https://www.cnblogs.com/nullllun/p/9039049.html
https://blog.csdn.net/a67474506/article/details/48310515

Java并发基础:了解无锁CAS就从源码分析相关推荐

  1. Java并发基础:了解无锁CAS就从源码分析 1

    CAS的全称为Compare And Swap,直译就是比较交换.是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在i ...

  2. Java并发编程,无锁CAS与Unsafe类及其并发包Atomic

    为什么80%的码农都做不了架构师?>>>    我们曾经详谈过有锁并发的典型代表synchronized关键字,通过该关键字可以控制并发执行过程中有且只有一个线程可以访问共享资源,其 ...

  3. ReentrantLock 公平锁和非公平锁加锁和解锁源码分析(简述)

    - title: ReentrantLock 公平锁和非公平锁加锁和解锁源码分析(简述) - date: 2021/8/16 文章目录 一.ReentrantLock 1. 构造函数 二.Reentr ...

  4. java并发编程——线程池的工作原理与源码解读

    2019独角兽企业重金招聘Python工程师标准>>> 线程池的简单介绍 基于多核CPU的发展,使得多线程开发日趋流行.然而线程的创建和销毁,都涉及到系统调用,比较消耗系统资源,所以 ...

  5. Java并发包中Semaphore的工作原理、源码分析及使用示例

    简介: 在多线程程序设计中有三个同步工具需要我们掌握,分别是Semaphore(信号量),countDownLatch(倒计数门闸锁),CyclicBarrier(可重用栅栏) 欢迎探讨,如有错误敬请 ...

  6. java 向上取整方法 Math.ceil() 用法、源码分析

    刷题用到了,正好好好看看源码. 用法 Math.ceil() 返回值.参数均为double类型, 如果参数为int类型,idea不会报错,但是方法同时不会向上取整. 参数为int类型时,Math.ce ...

  7. 学习笔记:Java 并发编程④_无锁

    若文章内容或图片失效,请留言反馈. 部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 视频链接:https://www.bilibili.com/video/av81461839 配套资料: ...

  8. 【java并发编程】无锁并发框架disruptor

    一.简介 Disruptor是一个高性能队列,研发的初衷是解决内部的内存队列的延迟问题,而不是分布式队列.基于Disruptor开发的系统单线程能支撑每秒600万订单. 使用场景:对延时要求很高的场景 ...

  9. JAVA并发容器-ConcurrentHashMap 1.7和1.8 源码解析

    HashMap是一个线程不安全的类,在并发情况下会产生很多问题,详情可以参考HashMap 源码解析:HashTable是线程安全的类,但是它使用的是synchronized来保证线程安全,线程竞争激 ...

最新文章

  1. 机器学习:基于关联规则的多标签分类器
  2. 【逆向工具】IDA使用5-( string、图形化与视图的切换、图形化显示反汇编地址、自动注释、标签使用)...
  3. 停止对互联网的意淫吧,它不过是个信息技术啊
  4. Linux宏定义实现类成员函数,全面解析Linux内核的同步与互斥机制
  5. Leedcode4-sort listnode 归并排序
  6. C/C++信息隐写术(一)之认识文件结构
  7. 宝马纯电动i4原型车谍照曝光 预计2021年上市
  8. profibus dp协议_轻松搞定PROFIBUS故障诊断与排除
  9. [AIR] 获取U盘,打开U盘
  10. 基于Cocos2d-x开发guardCarrot--4 《保卫萝卜2》主页面动画
  11. Debug的使用方法
  12. 自动泊车停车位检测算法
  13. 打印的时候显示域服务器不可用,Win10打印时active directory域服务不可用解决方法...
  14. Word 模板渲染引擎-Poi-tl
  15. CCF201903-1小中大(C语言)
  16. 该虚拟机似乎正在使用中。如果该虚拟机未在使用,请按“获取所有权(T)”按钮获取它的所有权
  17. 再掀融资潮 团购网仍后劲不足(团购现状分析)
  18. 华东理工某ACMer总结
  19. #Reading Paper#Improving Graph Collaborative Filtering with Neighborhood-enriched Contrastive Learni
  20. 【Java中级】8.5 SSH之Hibernate框架(五)——关于Criteria(QBC)过时的补充

热门文章

  1. 必读的 Java 学习资料分享!
  2. 获取在线APP的素材图片
  3. Lync与Exchange 2013 UM集成:Exchange 配置
  4. [分享]五种提高 SQL 性能的方法
  5. 组策略分发软件全攻略
  6. bff v2ex_语音备忘录的BFF-如何通过Machine Learning简化Speech2Text
  7. javascript 堆栈_JavaScript调用堆栈-它是什么以及为什么它是必需的
  8. ai css 线条粗细_如何训练AI将您的设计模型转换为HTML和CSS
  9. 完成工作表-使用Google Spreadsheets作为数据后端
  10. 【廖雪峰Python学习笔记】函数式编程