CAS原理
前言:
今天在看Ribbon底层是如何实现负载轮询算法的时候看到了这一段,其中有这句代码nextServerCyclicCounter.compareAndSet(current, next),这个nextServerCyclicCounter就是AtomicInteger,那么这个AtomicInteger是个什么东西呢?
AtomicInteger原子类:
我们知道java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。synchronized关键字可以保证可见性和有序性却无法保证原子性。而这个AtomicInteger的作用就是为了保证原子性。在多线程使用i++的时候很容易产生问题,当一个线程要操作一个数据的时候其实分为三步,第一从主内存中拿到数据,第二自己对数据进行修改,第三把修改后的数据写到主内存中,这种方式就可能会出现,两个或者多个线程同时拿到数据,修改完成之后直接写到内存就会产生与我们预期不符的结果。比如十个线程分别执行十次i++,最后结果一般都会小于100。这时就需要AtomicInteger原子类。AtomicInteger类中有许多方法,比如我们熟悉的compareAndSet(int expect, int update)、getAndIncrement()、 incrementAndGet()
unsafe类:
从源码可以发现getAndIncrement()、 incrementAndGet()都是使用的unsafe.getAndAddInt()方法
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
/** * Atomically increments by one the current value. * * @return the updated value */ public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要用过本地方法(native)来访问,Unsafe相当于一个后门,基于类可以直接操作特定内存的数据。Unsafe类位于sun.misc包中,其内部方法操作可以像C的指针一样操作内存,因为CAS操作是执行依赖于Unsafe类的方法。Unsafe的所有方法都是Native修饰就是直接调用操作系统底层资源执行相应的任务
getAndAddInt()方法:
再往底层翻源码就会看到getAndAddInt()方法其实用的就是CAS(compareAndSwap)
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
从incrementAndGet()方法中可以看出getAndAddInt()方法中
var1就是当前数据,
var2就是valueOffset就是内存偏移量,就是当前数据在内存地址。
var4就是固定值1,因为这是i++操作。
从getAndAddInt()方法源码开一看到用的是do while,首先就是获取当前数据在内存中的实际值var5,然后在while中又从内存中拿到数据,把这个数据和在do中拿到的var5比较是否相等
如果相等执行var5+var4也就是i++,同时相等的话this.compareAndSwapInt(var1,var2,var5,var5+var4)是true,前面取反,整个while也就是false,跳出循环return var5
如果不等,继续do while,从内存中拿值,比较直到相等,这也就是自旋,所以CAS也叫自旋锁。
getAndIncrement()、 incrementAndGet()区别:
这个很容易发现上面的源码截图中也很明显,incrementAndGet()是+1之后返回,getAndIncrement()是返回原来的值。
AtomicInteger atomicInteger = new AtomicInteger(0); System.out.println(atomicInteger.getAndIncrement()); System.out.println(atomicInteger.incrementAndGet()); 结果: 0 2 Process finished with exit code 0
可以看出初始值是0,getAndIncrement()方法虽然+1了,但是返回的值是原来的值0,但是现在值现在就是1,incrementAndGet()方法+1之后返回的是2,也就是说返回的是操作之后的结果值。
compareAndSet()方法:
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
这个方法也很好理解,我们在使用的时候传了expect和update两个值就是期望值和更新值,然后compareAndSwapInt()方法底层原理也讲过了,意思就是去主存里面看看这个值和快照的expect我们的期望值是不是一样,当然如果不一样肯定是因为这个值被动了,我们期望他是一样的,这样就能把expect值写入到主存中。
CAS缺点:
1、cpu开销大,在高并发下,许多线程,更新一变量,多次更新不成功,循环反复,给cpu带来大量压力。
2、只是一个变量的原子性操作,不能保证代码块的原子性。
3、ABA问题,就算这个值和我们期望的值一样,就一定能说明这个值没有被动过吗,显然不一定,有可能是从A改到B然后又改回A了。
举个ABA问题例子:内存值V=100;
threadA 将100,改为50;
threadB 将100,改为50;
threadC 将50,改为100;
场景:取款,由于机器不太好使,多点了几次取款操作。后台threadA和threadB工作,
此时threadA操作成功(100->50),threadB阻塞。正好有人给打款50元(50->100),
threadC执行成功,之后threadB运行了,又改为(100->50)。很明显这个逻辑不对。如何解决aba问题:
对内存中的值加个版本号,在比较的时候除了比较值还的比较版本号。
AtomicStampedReference就是用版本号实现cas机制。