CAS
概念
CAS全称Compare And Swap
(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent
包中的原子类
就是通过CAS来实现了乐观锁。
在Java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg
指令。随着Java不断的发展,Java本地方法(JNI—Java Native Interface)的出现,使得java程序越过JVM直接调用本地方法
提供了一种便捷的方式,因而java在并发的手段上也多了起来。
解决原子性问题
由于synchronized
是独占锁(同一时间只能有一个线程可以调用),没有获取锁的线程会被阻塞;另外也会带来很多线程切换的上下文开销!
所以JDK中就有了非阻塞CAS(Compare and Swap)算法
实现的原子操作类AtomicLong
等工具类!
CAS算法
CAS算法涉及到三个操作数:
- 需要读写的内存值V。
- 进行比较的值A(寄存器中的值)。
- 要写入的新值B。
当且仅当V的值等于A时(如果这个数据没有被更新,当前线程将自己修改的数据成功写入),CAS通过原子方式用新值B来更新V的值(比较+更新
整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。
进入原子类AtomicInteger
的源码:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
}
各属性的作用:
- unsafe:获取并操作内存的数据。
- valueOffset:存储value在AtomicInteger中的偏移量。
- value:存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。
查看AtomicInteger
的自增函数incrementAndGet()
的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()
。但是由于JDK本身只有Unsafe.class
,只通过class文件中的参数名,并不能很好的了解方法的作用,所以通过OpenJDK 8来查看Unsafe
的源码:
// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
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;
}
// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
根据OpenJDK 8的源码可以看出,getAndAddInt()
循环获取给定对象o中的偏移量处的值v
,然后判断内存值
是否等于v。如果相等
则将内存值设置为v + delta
,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。
整个“比较+更新”操作封装在compareAndSwapInt()
中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
后续JDK通过CPU的
cmpxchg
指令,去比较寄存器中的A和内存中的值V。如果相等,就把要写入的新值B存入内存中。如果不相等,就将内存值V赋值给寄存器中的值A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。
CAS存在的问题
CAS虽然很高效,但是它也存在三大问题:
-
ABA问题:CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”
- JDK从1.5开始提供了
AtomicStampedReference
类来解决ABA问题,具体操作封装在compareAndSet()
中。compareAndSet()
首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
- JDK从1.5开始提供了
-
循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
-
只能保证一个共享变量的原子操作:对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
- Java从1.5开始JDK提供了
AtomicReference
类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
- Java从1.5开始JDK提供了