概念
CAS,Compare And Swap的简称,CPU并发原语。
用来判断内存中某个位置的值是否符合期望值,如果是则更新为最新值,这个过程属于原子操作。
CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类(rt.jar包中的类)中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。
代码体现
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
//创建一个原子类,初始化值 5
AtomicInteger atomicInteger = new AtomicInteger(5);
//compareAndSet ==》CAS比较并交换,方法返回结果为boolean类型
atomicInteger.compareAndSet(5, 2020);
atomicInteger.compareAndSet(5, 2021);
}
}
在执行第一个compareAndSet(5,2020)时,期望值和原来值时相等的,因此可以修改成功。
第二次执行时 原始值已被修改为 2020,此时还拿5去与原始值比较,所以更新值失败。
这个就类似于SVN或者Git的版本号,如果没有人更改过,就能够正常提交,否者需要先将代码pull下来,合并代码后,然后提交
CAS底层原理
atomicInteger.getAndIncrement()方法的源码。
unfafe
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务
进一步分析getAndAddInt方法.
- var1:AtomicInteger对象本身
- var2:地址偏移量
- var4:需要更新的值
- var5:用var1和var2找到的内存中的真实值
- 用该对象当前的值与var5比较,如果相同更新为var5 + var4并返回true,如果不同,继续取值然后比较,直到更新完成。
这里没有用synchronized,而用CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的do while循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作。
假设线程A和线程B同时执行getAndInt操作(分别跑在不同的CPU上)
- AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的 value 为3,根据JMM模型,线程A和线程B各自持有一份价值为3的副本,分别存储在各自的工作内存。
- 线程A通过getIntVolatile(var1 , var2) 拿到value值3,这是线程A被挂起(该线程失去CPU执行权)。
- 线程B也通过getIntVolatile(var1, var2)方法获取到value值也是3,此时刚好线程B没有被挂起,并执行了compareAndSwapInt方法,比较内存的值也是3,成功修改内存值为4,线程B打完收工,一切OK
- 这是线程A恢复,执行CAS方法,比较发现自己手里的数字 3和主内存中的数字4不一致,说明该值已经被其它线程抢先一步修改过了,那么A线程本次修改失败,只能够重新读取后在来一遍了,也就是在执行do while
- 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
Unsafe类 + CAS思想: 也就是自旋,自我旋转
CAS缺点
CAS不加锁,保证一次性,但是需要多次比较。
-
循环时间长,开销大(因为执行的是do
while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环) -
只能保证一个共享变量的原子操作
-
ABA问题
- 在某个线程准备进行检测时,如果其间其他线程将一个共享变量的值进行了多次更改后又回到了初始的值,此时该线程通过CAS检测会认为该共享变量未发生更改,这与实际情况不符合。
通过原子类实现线程安全是非阻塞的(对比于synchronized关键字)。其基本的思想是基于冲突检测与循环重试。具体讲就是,需要对共享数据修改时,不加锁先进行目标操作,如果发现有其他线程对同一份共享数据做修改(对应于检测到当前内存位置处的值与旧预期值不等),则放弃本次修改,重写循环再次检测并尝试修改,直到成功为止。
synchronized关键字的时间体现了悲观锁的策略思想,而原子类的实现则体现见了乐观锁的思想。
附:
上文提到的Unsafe类是不能被用户源程序直接加载和实例化的,因为其构造器被限定为Unsafe类私有,Unsafe只提供getUnsafe()接口间接的对外提供Unsafe的实例,但即使是这样,getUnsafe()方法对调用者要求任然颇为严苛:
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
而位于sun.misc.VM类的isSystemDomainLoader(loader)方法只有在参数loader为启动加载器时,才返回true。
/**
* Returns true if the given class loader is in the system domain
* in which all permissions are granted.
*/
public static boolean isSystemDomainLoader(ClassLoader loader) {
return loader == null;
}
结合上述两个方法可知,通常我们用户程序的加载器为应用程序加载器,直接调用Unsafe是会抛异常的。