- 导语
上一篇提到了在多核CPU环境下怎么实现CAS的原子性操作。本节将列举CAS在Java中的具体应用。
CAS实现锁的原理
CAS的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。
若不相等,则不做任何操作。
基于这个理念,试想,在并发场景中,如果我们在做CAS操作的时候不成功,那么可以一直循环执行,直到最终成功返回为止。
这样没有加锁执行,线程不会阻塞影响性能,只会耗费更多的CPU资源。
AtomicLong为列
AtomicLong.java
// 获取Unsafe,后面在详细分析该类
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 偏移量
private static final long valueOffset;
// 具体的值
private volatile long value;
static {
try {
// 初始化偏移量,调用的本地方法
valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 列举自增的场景
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
Unsafe.java
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
// 采用volatile读可以保证可见性和有序性。
var6 = this.getLongVolatile(var1, var2);
// 循环处理,比较当前值与预期值是否相等,相等则+1,本地方法调用
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
CAS缺陷
- ABA问题
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。
尽管线程one的CAS操作成功,但可能存在潜藏的问题。
- 循环时间长开销大
自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
- 只能保证一个共享变量的原子操作
CAS与Synchronized对比
-
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
-
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充:synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
扫码关注了解更多