本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
什么是CAS
CAS又叫比较并交换,是一种无锁算法,日常开发中,基本不会直接用到CAS,都是通过一些JDK封装好的并发工具类来使用的,在JUC包下。
CAS包含三个值,内存地址(V),预期值(A),新值(B)。先比较内存地址的值和预期的值是否相等,如果相等,就将新值赋在内存地址上,否则,不做任何处理。步骤如下:
1.获得字段的期望值(oldValue)。
2.计算出需要替换的新值(newValue)。
3.通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败则重复第1步到第2步,一直到CAS成功,这种重复也就是CAS自旋。
当CAS进行内存地址的值与预期值比较时,如果相等,则证明内存地址的值没有被修改,可以替换成新值,然后继续往下运行;如果不相等,说明明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少,CAS性能会很高;当并发修改的线程多,冲突出现的机会高时,自旋的次数也会很多,CAS性能会大大降低。所以,提升CAS无锁编程的效率,关键在于减少冲突的机会。
CAS原理剖析
以AtomicInteger原子整型类为例,看一下CAS底层实现机制。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
//这里主要就是获取AtomicInteger类value这个这个字段在地址的偏
//移量,也就是地址,这个value字段是使用的volatile 进行修饰的,保证了字段的可见性
static {
try {
//获取value值的内存偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//AtomicInteger初始化就是对value进行赋值
public AtomicInteger(int initialValue) {
value = initialValue;
}
//实际对数据操作的是unsafe的类的getAndAddInt方法
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 提供自增易用的方法,返回增加1后的值
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 额外提供的compareAndSet方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// Unsafe类的提供的方法
public final int getAndAddInt (Object o,long offset, int delta){
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
AtomicInteger 内部方法都是基于Unsafe类实现的,Unsafe类是个跟底层硬件CPU指令通讯的复制工具类。 再看看unsafe.getAndAddInt方法具体内容:
//CAS自旋,通过getIntVolatile方法通过内存偏移量获取对象最新的值,再调用cas方法,如果失败了就不断的重
//试获取对象新值然后CAS,直到成功
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//先去内存中获取内存地址所指向的值,这个将这个内存值赋值给var5,这个方法是native方法
//var5是预期值
var5 = this.getIntVolatile(var1, var2);
//在修改前先比较一次内存的值还是否是预期值var5,将偏移量var2、对应的unsafe对象var1
//和var5带进compareAndSwapInt去获取此时内存中偏移量所指向的数值然后
//和预期值var5进行比较,如果是相等的就将偏移量var2指向的主内存地址中的值修改为
//var5 + var4,失败就进行重试,每次重试都会去内存中重新获取值赋值给var5,然后修改时再比较一下
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
再看看如何获得valueOffset的:
// Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 获得value在AtomicInteger中的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 实际变量的值
private volatile int value;
value实际的变量,是由volatile关键字修饰的,为了保证在多线程下的内存可见性。
CAS的问题
ABA问题
CAS操作是先比较A的预期值和内存地址中的值是否相同,如果相同就认为此时没有其他线程修改A值。但是,此时假如一个线程读取到A值,此时有另外一个线程将A值改成了B,然后又将B改回了A,这时比较A和预期值是相同的,就认为A值没有被改变过。为了解决ABA的问题,可以使用版本号,每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被别人偷偷改过了。
解决方法:AtomicReference原子引用。
性能问题
如果自旋长时间不成功,会给CPU带来非常大的执行开销。
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
可以看到源码中的自旋就是当CAS成功时,才会return。因此CAS带来的性能问题也是需要考虑的。自旋也是CAS的特点,自旋算是一种非阻塞算法,相对于其他阻塞算法而已,非阻塞是不需要cpu切换时间片保存上下文的,节省了大量性能消耗。CAS相对于同步锁的优点:如果在并发量不是很高时CAS机制会提高效率,但是在竞争激烈并发量大的情况下效率是非常低,因为自旋时间过长,失败次数过多造成重试次数过多。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
ABA问题解决办法
加版本号
每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。参考乐观锁的版本号,这种做法可以给数据带上了一种实效性的检验。
AtomicStampReference的compareAndSet方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。
使用AtomicMarkableReference
AtomicMarkableReference不关心修改过几次,仅仅关心是否修改过。其标记属性mark是boolean类型,而不是数字类型,标记属性mark仅记录值是否有过修改。 AtomicMarkableReference适用只要知道对象是否有被修改过,而不适用于对象被反复修改的场景。
CAS的使用场景
CAS在JUC包中的原子类、AQS以及CurrentHashMap等重要并发容器类的实现上都有应用。再看一下AQS的例子:
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
对state变量进行的CAS操作,很多同步类都是通过这个变量来实现线程安全的,所以在AQS中,首先要保证对state的赋值是线程安全的。
在java.util.concurrent.atomic包的原子类如AtomicXXX 中,都使用了CAS保障对数字成员进行操作的原子性。 JUC的大多数类(包括显示锁、并发容器)都基于AQS和AtomicXXX实现,而AQS通过CAS保障其内部双向队列头部、尾部操作的原子性。
抽奖说明
1.本活动由掘金官方支持 详情可见juejin.cn/post/701221…
2.通过评论和文章有关的内容即可参加,要和文章内容有关哦!
3.本月的文章都会参与抽奖活动,欢迎大家多多互动!
4.除掘金官方抽奖外本人也将送出周边礼物(马克杯一个和掘金徽章若干,马克杯将送给走心评论,徽章随机抽取,数量视评论人数增加)。