最近通过一些文章学习了CAS算法底层原理,在此把学习和理解做一次梳理和延伸。希望对各位同学有所启发,如有错漏不足之处,也望能予以指正,在此敬谢。
CAS,Compare And Swap,即比较与交换。CAS是一种无阻塞算法的实现,是CPU硬件同步原语。CAS算法包含了三个操作数:实际内存值V,预期值A,和修改后的新值B。当且仅当内存值V和预期值A相同时,将内存值修改为B并返回true,否则不执行任何操作返回false。整个比较和交换的过程是原子操作。
CAS有三个缺点:1、循环时间长,开销大;2、只能保证一个共享变量的原子操作;3、ABA问题。
关于CAS的介绍大多以AtomicInteger为例,说到底就是如下一段源码。
// 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); } } // 变量值 private volatile int value; // unsafe调用底层指令自增加1 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } // openJdk 8 源码 Unsafe类中的方法 public final int getAndAddInt(Object o, long offset, int delta) { int v; //预期值A do { v = getIntVolatile(o, offset); //从内存中读取A } while (!compareAndSwapInt(o, offset, v, v + delta)); // 通过当前对象o的物理地址和偏移量offset获取主存中的实际值V,与预期值参数v比较,(v+delta)是新值B return v; }
对我来说,上段代码有以下几个难以理解的地方:
1.Unsafe类是干嘛的?
Java不能直接访问操作系统底层,需要通过本地方法(Native关键字)调用,Unsafe类提供了硬件级别的原子操作。就是通过这个类调用上面的方法访问底层资源。对于开发人员来说,能不用就不要用了
2.volatile关键字有什么作用?
保证不同线程对变量进行操作的可见性,当一个线程修改了volatile修饰的变量后,其他线程工作内存中该缓存变量的缓存行失效,其他线程的读取自己的工作内存发现缓存行失效后,会等待主存更新完成,然后重新到主存读取最新的值。
要读懂这句话,我们先要了解一下java的内存模型:
jvm将所管理的内存分为以下几个运行时数据区域:
其中堆和方法区属于主存,工作内存包含栈(虚拟机栈、本地方法栈)和寄存器(程序计数器和CPU工作的高速缓存区)。主内存对所有线程共享,工作内存属于线程私有,其他线程不能读取。主存中存放共享变量,共享变量在堆中。工作内存存放拷贝的缓存变量,缓存变量在CPU的高速缓存区中。
线程在执行方法时,会先从主存(堆)中读取共享变量,把读取到的变量加载到工作内存(CPU工作的高速缓存区)中,在工作内存中对缓存变量进行修改操作,最终把修改后的结果写入主存中。
因此,对每个线程而言,先在工作内存中对共享变量进行缓存,再在工作内存中对缓存变量进行修改,修改完成后把最终结果成功再写入主存,这一过程并行线程之间是相互不可见的,因为并行线程已经加载过缓存变量,不会重新同步修改后的共享变量,只有再有新线程进入时才能读取到修改后的结果。而volatile的作用就是,在线程修改了缓存变量之后,其他并行线程中该变量的缓存变量将会失效,必须等结果写入主存后重新读取修改后的最新结果。
详细介绍可参考:Java并发编程:volatile关键字解析,主内存和工作内存,jvm-volatile之缓存行,Java中的伪共享以及应对方案,深入理解Java虚拟机
3.预期值A是怎么从主存中读取的?
要弄明白程序是怎么从主存中读取数据的,首先要搞明白数据在存储器中是怎么存储的,所以要先理解以下几个概念。
关于地址:
段地址:电脑内的存储器地址可被分为若干逻辑段。每个逻辑段的起始地址称为段地址。
单元地址:存储器中每个存储单元的唯一地址编号,
偏移地址:就是偏移量,计算机里的内存分段后,在段内某一地址相对于段首地址(段地址)的偏移量,也称相对地址。
物理地址:又叫实际地址、绝对地址、实地址、二进制地址。在存储器里以字节为单位存储信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址。物理地址的计算是段地址乘以16加上偏移量得到的。
关于单位:
位(Bit):计算机中最小的信息单位是二进制位,即bit,一个bit可以包含0或1两种状态
字节(Byte):为了表示数据中的所有字符(字母、数字及各种专用符号,大约256个),需要用哪个7位或8位二进制数。因此,人们选定8位作为一个位元组,即字节,通常用B表示。一个字节由8位二进制数位表示,字节是计算机中用来表示存储空间大小的最基本容量单位。
字(Word):一串数码作为一个整体来运算或处理的,称为一个计算机字,由若干个字节组成(通常取字节的整数倍)。字是计算机进行数据存储和数据处理的基本运算单位。
字长:计算机的每个字所包含的位数称为字长,计算机的字长是指它一次性可处理的二进制数字的数目,是计算机性能的重要标识。按照字长可将计算机划分为8位机(微型计算机)、16位机(微/小型计算机)、32位机(小/大型计算机)、64位机(大型计算机)。
通过以上可以看出,一个存储器划分了若干存储段,一个存储段内有连续的若干存储单元,一个存储单元存放一个或多个字节。当需要读取数据时,处理器从存储器地址寄存器中读取物理地址,根据物理地址和偏移量定位到主存的段地址和单元地址,继而将存储单元中的状态转换为二进制电信号输出到存储器数据寄存器中。以上就是预期值获取的原理,根据对象的物理地址和偏移量可得到主存中的预期值。
4.比较与交换是怎么实现原子操作的?
请看这一篇文章:CAS你以为你真的懂?
// Unsafe类中本地方法 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//上面方法在C++中的实现 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); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) { int mp = os::is_MP(); // LOCK_IF_MP 如果是多核CPU 加lock // CPU指令 cpmxchg指令 应该是通过CPU中的电路实现的吧,在查就该从模电 数电开始学起了 __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" : "=a" (exchange_value) : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) : "cc", "memory"); return exchange_value; }