CAS
什么是CAS?
CAS:Compare and Swap,即比较再交换。
jdk5 JUC并发包下的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
示例
public class CASDemo {
public static void main(String[] args) {
//Atomic表示是一种原子操作,线程是安全的。
//原子类的底层就是通过CAS实现的
//内存值为2020
AtomicInteger atomicInteger = new AtomicInteger(2020);
// boolean compareAndSet(int expect, int update)
//该方法的缩写就是CAS
//参数一:期望值
//参数二:更新值
//如果实际值(内存值) 和 我期望值相同,那么就更新 2020=2020所以修改成功
System.out.println(atomicInteger.compareAndSet(2020,2021));
System.out.println(atomicInteger.get());
//如果实际制 和 我期望值不同,那么就不更新
//因为上面修改成功,所以实际值却变成了2021,此处的期望值还是2020, 所以会修改失败
System.out.println(atomicInteger.compareAndSet(2020,2022));
System.out.println(atomicInteger.get());
}
}
true
2021
false
2021
源码剖析
atomicInteger.getAndIncrement();方法分析到底层,我们发现也使用了CAS比较并交换。
AtomicInteger atomicInteger = new AtomicInteger(2020);
//原子类AtomicInteger有一个自增(每次加一)的操作,我们来分析一下它的源码
atomicInteger.getAndIncrement();
//getAndIncrement方法的源码
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//点击unsafe查看AutomicInteger类的源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
//此处将一个Unsafe类作为其成员变量
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类源码,我们发现unsafe类中几乎全是native方法
//来看一下该类中是如何完成加一操作的。
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;
}
关于Unsafe类
Java语言无法直接操作内存,通常可以通过native调用C++来操作内存,而Unsafe类是一个特殊的类,Java通过它可以操作内存完成一些任务。
对于valueOffset的理解:如果想获取一个对象的属性的值,我们一般通过getter方法获得,而sun.misc.Unsafe却不同,我们可以把一个对象实例想象成一块内存,而这块内存中包含了一些属性,如何获取这块内存中的某个属性呢?那就需要该属性在该内存的偏移量了,每个属性在该对象内存中valueOffset偏移量不同,每个属性的偏移量在该类加载或者之前就已经确定了(则该类的不同实例的同一属性偏移量相等),所以sun.misc.Unsafe可以通过一个对象实例和该属性的偏移量用原语获得该对象对应属性的值;参考文章
Unsafe类的getAndAddInt通过do-while循环完成了自旋锁的操作。
通过上面的分析,发现CAS的缺点。
- 比较时的循环操作耗时间。
- 一次只能保证一个变量的原子性。
- 会存在ABA问题。
CAS的 ABA问题(狸猫换太子)
场景:有一个值为1,A线程将其改为ASC(1,2)后有改为ASC(2,1)。B线程又去改为ASC(1,3)
分析:上面的改名B线程将原始值由1标为3。但是这个1是A线程改过后的1,并不是最开始的1,虽然这样该成功了, 但是在很多场景下这样改是不对的。
示例一:存在ABA问题的示例
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
//此处的2020并不是真正意义上的原始值
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
示例二:通过原子引用解决ABA问题
其核心思想就是乐观锁,每次修改的时候都修改版本号加1,通过版本号我们就能知道某个值什么时候被修改了。
需要注意的地方:
- 下面示例中的Integer 使用了对象缓存机制,默认范围是 -128~127,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new ,因为 valueOf 使用了缓存,而 new 一定会创建新的对象分配新的内存空间。
- 而AtomicStampedReference类的泛型通常都是一个实力对象(指向的地址是一样的),以Integer为例就不一样在-128~127是Interger缓存区的对象(一个地址),一超过这个这就是new一个新的对象。下面的例子就会报错。
- 阿里巴巴的一条规范如下:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo {
/**AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
* 正常在业务操作,这里面比较的都是一个个对象
*/
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改操作时,版本号更新 + 1
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println("a2=>" + atomicStampedReference.getStamp());
// 重新把值改回去, 版本号更新 + 1
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
// 跟新版本号加一和乐观锁的原理相同!
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//因为是多线程的原因:此时b1是还是最初的版本1,但此时的期望值1的版本是3了,
//3!=1 所以期望值和原始值并不是指的通过一个值,尽管他们值相同。
// 所以修改会失败。下面的b2版本依然还是3。(如果不理解,可以去看看乐观锁原理)
System.out.println(atomicStampedReference.compareAndSet(1, 3,
stamp, stamp + 1));
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
a1=>1
b1=>1
a2=>2
true
a3=>3
false
b2=>3