一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
CAS
如果说锁是悲观策略(悲观的看待每次操作都会产生冲突,因此需要阻塞其他线程),那么CAS就是乐观策略(乐观认为每次操作都不会产生冲突,即无锁状态,不会阻塞其他线程)。
如果出现冲突,就使用compareAndSwap判断并改变值,一直重试直到成功。
操作过程
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
通过内存地址找到最新值与期望值比较,如果相等,替换成新值。
unsafe可以直接操作内存
通过this(对象)和valueOffset(value在对象中的偏移地址)就可以获得value,如果value和expect相等,那么可以赋值update
// 调用了unsafe的compareAndSwap方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
原子性CAS源码
1.valueOffset通过unsafeo.bjectFieldOffset获得在value变量在主存对象中的偏移地址。
这个value时volatile,保证了多线程之间的可见性。但修改后,会对所有线程可见,保证是最新值
private static final long valueOffset;
private volatile int value;
static {
try {
// 获取value变量在对象内存中的偏移量, 赋值给valueOffset
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
2.那么通过对象和偏移地址即可定位到value,获得最新value-v5
3.compareAndSwapInt操作是如果发现之前获取的v5不是最新值(重写获取了一下最新的value与v5不一样),那么就操作失败,循环重试
//原子类的+1方法
public final int getAndIncrement() {
// 参数1:当前对象,参数2:变量在变量中的偏移地址 参数3:增加1
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 通过对象和偏移地址得到value变量
var5 = this.getIntVolatile(var1, var2);
// 如果在循环期间有其他线程操作导致var5不是最新值了,就不改变,一直重试
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
问题
ABA问题
CAS获取值和赋值期间发生了多次操作,但是多次操作后最新值和获取值一样,也会CAS成功
增加版本号来解决,每当进行一次操作,就会增加版本号。即使值一样,但是版本号不一样也不能赋值成功。 AtomicStampedReference带有时间戳,实现了该思路
自旋时间长
线程修改失败会自旋直到成功,长期会消耗CPU资源。当竞争激烈的时候,多个线程一直自旋会消耗大量CPU资源
JVM支持处理器提供的`pause指令
- 第一它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,
- 第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
只能保证一个变量的原子性操作
目标只能是一个变量,对多个变量就不能保证原子性。(CAS是对cmpxchg执行的封装,一次只能原子性的修改一个变量)
因此如果要操作多个变量,可以将多个变量合并成一个变量来操作。如a =s ,b = 1合并成 ab =s1