问题情景:Java中处理原子操作的Atomic系列类(如AtomicInteger,AtomicDouble等)中,处理同步性问题采用的是CAS算法,看了一下感觉算法貌似有点不对劲,然后就搜了搜网上已有的解决思路汇总如下。
鸣谢程序员囧辉,JupiterMouse和水欣分享博文供笔者参考
CAS是什么
提到同步,第一反应我想到的是synchronized加锁。但是当线程很多并发量很大的时候,这种方式性能会变得极差。于是就有了本文主角:CAS算法。
CAS即Compare and Swap,比较并交换算法,处理同步问题的常见解决思路。需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
比如AtomicInteger的自增操作:
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
自增并返回自增前的值。这个方法又调用了Unsafe类实例的getAndAddInt方法。这个类并不在jdk中,于是我们只能通过IDEA或者Eclipse自带的反编译工具查看,或者下载openJDK一探究竟。反编译结果如下:
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;
}
变量全都用varX代替,又没啥注释说明,不易看懂,但整体思路不难理解:var1是Object型,表示要改变的对象;var2是long型,表示偏移量;var4表示要增加的值。
方法中先从内存中拿到var1的最新值赋值到var5,然后尝试将内存位置的值修改为var5+var4(要增加的量),如果修改失败,则获取该内存位置的新值v,然后继续尝试,直至修改成功。
再深入一层的compareAndSwapInt方法就涉及到JNI的东西了,暂时先不管,如有兴趣可以参考文章开头的两篇鸣谢博文。
CAS的缺陷
那么下面提一提该算法的缺陷。
1.循环开销大
可以看到,方法内部用不断循环的方式实现修改。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
解决方案
- 破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。
- 如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空,从而提高CPU的实行效率。
2.只能保证一个共享变量的原子操作
需要对多个共享变量操作时,循环CAS就无法保证操作的原子性。
解决方案
- 用锁
- 把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ji=2a,然后用CAS来操作ij。
- 封装成对象。注:从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之前的原子性,可以把多个变量放在一个对象里来进行CAS操作。
3.ABA问题
CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS进行检查的时候发现它的值没有发生变化,但是实质上它已经发生了改变 。可能会造成数据的缺失。
解决方案
CAS类似于乐观锁,即每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。因此解决方案也可以跟乐观锁一样:
- 使用版本号机制,如手动增加版本号字段
- Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前的标志是否等于预期标志,如果全部相等,则以原子方式将该应用和该标志的值设置为给定的更新值。