本文主要简介一下CAS原理及其引发的三个问题:
1.ABA问题; 2.自旋锁开销及jdk8解决方案; 3.单对象操作及解决。
1. CAS简介
在多线程编程的时候,为了保证多个线程对一个对象可以安全进行访问时,我们需要加同步锁synchronized,保证对象的在使用时的正确性,synchronized就是一种独占锁,它会导致所有需要此锁的线程挂起,等待锁的释放。
加锁会导致一下问题:
加多线程竞争下,加锁和释放锁会导致较多的上下文切换,引起性能问题。
多线程可以导致死锁的问题。
多线程持有的锁会导致其他需要此锁的线程挂起。
而乐观锁却是不加锁,那不加锁如何确保某一变量的操作没有被其他线程修改过?
这里就需要CAS操作(Compare And Swap)来实现。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
在JVM中的CAS操作就是基于处理器的CMPXCHG汇编指令实现的,因此,JVM中的CAS的原子性是处理器保障的。
这里我们可以看一下JAVA的原子类AtomicLong.getAndIncrement()的实现,来理解一下CAS这一乐观锁(JDK 1.8)。
<span><span><span>public</span> final <span>long</span> <span>getAndIncrement</span>(<span></span>)</span> {</span>
<span> <span>return</span> <span>unsafe</span>.getAndAddLong(<span>this</span>, valueOffset, <span>1L</span>);</span>
<span>}</span>
<span><br /></span>
接着看一下 Unsafe.getAndAddLong()的实现:
<span><span><span>public</span> <span>final</span> <span>long</span> <span>getAndAddLong(Object var1, <span>long</span> var2, <span>long</span> var4)</span> </span>{</span>
<span> <span>long</span> var6;</span>
<span> <span>do</span> {</span>
<span> var6 = <span>this</span>.getLongVolatile(var1, var2);</span>
<span> } <span>while</span>(!<span>this</span>.compareAndSwapLong(var1, var2, var6, var6 + var4));</span>
<span> <span>return</span> var6;</span>
<span>}</span>
<span><br /></span>
这里我们可以看到AtomicLong.getAndIncrement()的实现就是通过CAS循环操作的实现,只有期望值与真实值相同情况下,CAS操作才会成功执行,退出循环,如果失败则继续自旋,直到成功。
ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。ABA问题的解决思路是,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。在JDK的java.util.concurrent.atomic包中提供了AtomicStampedReference来解决ABA问题,该类的compareAndSet是该类的核心方法,实现如下:
<span><span>public</span> <span>boolean compareAndSet(V expectedReference,</span></span>
<span> <span>V</span> <span>newReference,</span></span>
<span> <span>int</span> <span>expectedStamp,</span></span>
<span> <span>int</span> <span>newStamp) {</span></span>
<span> <span>Pair<V></span> <span>current = pair;</span></span>
<span> <span>return</span></span>
<span> <span>expectedReference</span> =<span>= current.reference &&</span></span>
<span> <span>expectedStamp</span> =<span>= current.stamp &&</span></span>
<span> <span>((newReference</span> =<span>= current.reference &&</span></span>
<span> <span>newStamp</span> =<span>= current.stamp) ||</span></span>
<span> <span>casPair(current,</span> <span>Pair.of(newReference, newStamp)));</span></span>
<span style="">}</span>
我们可以发现,该类检查了当前引用与当前标志是否与预期相同,如果全部相等,才会以原子方式将该引用和该标志的值设为新的更新值,这样CAS操作中的比较就不依赖于变量的值了。
3. CAS导致自旋消耗
多个线程争夺同一个资源时,如果自旋一直不成功,将会一直占用CPU。
解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。
虽然base和cells都是volatile修饰的,但感觉这个sum操作没有加锁,可能sum的结果不是那么精确。
<span><span><span>public</span> <span>long</span> <span>sum</span>(<span></span>)</span> {</span>
<span> Cell[] <span>as</span> = cells; Cell a;</span>
<span> <span>long</span> sum = <span>base</span>;</span>
<span> <span>if</span> (<span>as</span> != <span>null</span>) {</span>
<span> <span>for</span> (<span>int</span> i = <span>0</span>; i < <span>as</span>.length; ++i) {</span>
<span> <span>if</span> ((a = <span>as</span>[i]) != <span>null</span>)</span>
<span> sum += a.<span>value</span>;</span>
<span> }</span>
<span> }</span>
<span> <span>return</span> sum;</span>
<span> }</span>
4. CAS只能单变量
方法:CAS操作是针对一个变量的,如果对多个变量操作,1. 可以加锁来解决。2 .封装成对象类解决。