CAS(compare-and-swap) ,保证数据的原子性,是硬件对于并发操作共享数据的支持
CAS不同于Synchronized,Synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起;而CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
当且仅当V==A时,V = B,否则,不进行任何操作
看看它的源码
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe 类
Unsafe 类方法都是本地方法,因为Java无法直接和操作系统打交道,但是C++可以,所以提供了这个通道来间接操作
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
public native Object getObjectVolatile(Object var1, long var2);
public native void putObjectVolatile(Object var1, long var2, Object var4);
public native int getIntVolatile(Object var1, long var2);
再看看 getAndIncrement 操作,类似Java的 i++
有三个参数,第一个是当前对象
第二个是值的偏移量,可以看成是值的内存地址
第三个就是自增1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
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)); //相等,内存地址值加1
return var5;
}
因为cas是直接操作内存,所以效率极高
CAS的缺陷
1.著名的 ABA 问题
2. 只能保证一个共享变量的原子操作
3. 循环时间长开销大
1、ABA 问题
CAS算法实现的一个重要前提是:取出内存中某时刻的数据,而在下一时刻比较并替换,那么在这个时间差中会导致数据的变化。
简单举例:
线程 1 从内存位置V中取出A。
线程 2 从位置V中取出A。
线程 2 进行了一些操作,将B写入位置V。
线程 2 将A再次写入位置V。
线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。
尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public boolean compareAndSet(
V expectedReference,//预期引用
V newReference,//更新后的引用
int expectedStamp, //预期标志
int newStamp //更新后的标志
)
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test06 {
public static void main(String[] args) {
//原子引用,带 版本号的原子操作 乐观锁
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//得到当前的版本号
System.out.println("A -version "+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean compareAndSet = atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println("A IS "+compareAndSet);
System.out.println("A -version "+atomicStampedReference.getStamp());
compareAndSet = atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println("A IS "+compareAndSet);
System.out.println("A -version "+atomicStampedReference.getStamp());
},"a").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//得到当前的版本号
System.out.println("B -version "+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean compareAndSet = atomicStampedReference.compareAndSet(1, 7, stamp, stamp + 1);
System.out.println("B IS "+compareAndSet);
System.out.println("B -version "+atomicStampedReference.getStamp());
},"b").start();
}
}
2、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
3、循环时间长开销大
自旋锁,自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
//比较当前工作内存中的值和主内存中的值,如果这个值是期望的,就执行操做,如果不是,就一直循环
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}