CAS原理

195 阅读5分钟

CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:

  • 主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
  • 工作内存中共享变量的副本值,也叫预期值:A
  • 需要将共享变量更新到的最新值:B

如下图所示假设20个线程需要对一个变量不停的累加1,最终的结果是不等于20的。

初步解决:synchornized

改造后的代码如下图所示

此时线程是安全的,加了锁,每个线程进入到方法之前都要尝试加锁,同一时间只有一个线程能加锁,其他线程都要等待。

但是很简单的操作用一个sync锁来解决线程并发问题,有点大材小用。而且,在这个场景下,你要是用synchronized,不就相当于让各个线程串行化了么?一个接一个的排队,加锁,处理数据,释放锁,下一个再进来。

更高效的方案:Atomic原子类及其底层原理

使用AtomicInteger可以保证线程并发安全的情况下,高性能的并发更新一个数值。

上面的代码就是把data的值累积加1,接着返回累加后的值。就没有加锁和释放锁一说了。

实际上,Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性

他的全称是:Compare and Set,也就是先比较再设置的意思。

如上图所示,假如说有3个线程并发的要修改一个AtomicInteger的值,他们底层的机制如下:

  • 首先,每个线程都会先获取当前的值,接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。
  • 然后CAS操作里,会比较一下,当前值是不是刚才获取的值,如果是 就累加。
  • 如果值不一样,cas失败,之后进入一个无线循环,再次获取值,继续执行cas操作。

整个过程,就是所谓Atomic原子类的原理,没有基于加锁机制串行化,而是基于CAS机制:先获取一个值,然后发起CAS,比较这个值被人改过没?如果没有,就更改值!这个CAS是原子的,别人不会打断你!

通过这个机制,不需要加锁这么重量级的机制,也可以用轻量级的方式实现多个线程安全的并发的修改某个数值。

Java 8对CAS机制的优化

问题:

比如说大量的线程同时并发修改一个AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。

这些线程不停地获取值,然后发起CAS操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起CAS操作又失败了,再次进入下一个循环。

在大量线程高并发更新AtomicInteger的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。

Java 8推出了一个新的类,LongAdder,他就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能!


在LongAdder的底层实现中,首先有一个base值,刚开始多线程来不停的累加数值,都是对base进行累加的,比如刚开始累加成了base = 5。

接着如果发现并发更新的线程数量过多,就会开始施行分段CAS的机制,也就是内部会搞一个Cell数组,每个数组是一个数值分段。

这时,让大量的线程分别去对不同Cell内部的value值进行CAS累加操作,这样就把CAS计算压力分散到了不同的Cell分段数值中了!

这样就可以大幅度的降低多线程并发更新同一个数值时出现的无限循环的问题,大幅度提升了多线程并发更新数值的性能和效率!

而且他内部实现了自动分段迁移的机制,也就是如果某个Cell的value执行CAS失败了,那么就会自动去找另外一个Cell分段内的value值进行CAS操作。

这样也解决了线程空旋转、自旋不停等待执行CAS操作的问题,让一个线程过来执行CAS时可以尽快的完成这个操作。

最后,如果你要从LongAdder中获取当前累加的总值,就会把base值和所有Cell分段数值加起来返回给你。

CAS优缺点

缺点:

1、ABA问题

2、可能会消费更高的CPU

3、不能保证代码块的原子性

优点:

1、可以保证变量操作的原子性

2、并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;

3、在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。