由于JVM的synchronized重量级锁涉及操作系统内核态下互斥锁的使用,因此其线程阻塞和唤醒都涉及进程在用户态和内核态频繁的切换,导致重量级锁开销大,性能低。CAS,Compare And Swap比较并替换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(E)新值(N)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。CAS也称为自旋锁,在一个(死)循环[for(;;)]里不断进行CAS操作,直到成功为止(自旋操作),实际上,CAS也是一种乐观锁。
Unsafe 提供的CAS方法
public final native boolean compareAndSetReference(Object o, long offset,
Object expected,
Object x);
public final native boolean compareAndSetLong(Object o, long offset,
long expected,
long x);
public final native boolean compareAndSetLong(Object o, long offset,
long expected,
long x);
参数说明
o- 需要操作字段所在的对象
offset-需要操作字段的偏移量 相对对象头,在64位jvm虚拟机偏移量是12,因为markWord占64位,Class pointer(类对象指针)占32位,所以偏移量是12.
expected-期望值,也就是旧值
x-更新的值,也就是新值
在执行Unsafe的CAS方法时,这些方法值首先将内存位置的值与旧值比较,如果匹配那么CPU会自动将内存位置的值更新为新值,并返回true,如果不匹配,CPU不做任何操作,并返回false。当并发修改的线程少,冲突出现的机会少时,自旋次数也会减少,CAS的性能就会很高,反之冲突越多,自旋的次数越多,CAS的性能就会越低。
以AtomicInteger为例
public final int get(); //获取当前的值
public final int getAndSet(int newValue); //获取当前的值,然后设置新的值
public final int getAndIncrement() ;//获取当前的值,然后自增
public final int getAndDecrement() ; //获取当前的值,然后自减
public final int getAndAdd(int delta) ; //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update);//通过CAS方式设置整数值
public class AtomicIntegerDemo extends Thread {
private AtomicInteger atomicInteger;
AtomicIntegerDemo(AtomicInteger atomicInteger) {
this.atomicInteger = atomicInteger;
}
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
atomicInteger.getAndIncrement();
}
}
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(0);
AtomicIntegerDemo demo1 = new AtomicIntegerDemo(atomicInteger);
AtomicIntegerDemo demo2 = new AtomicIntegerDemo(atomicInteger);
AtomicIntegerDemo demo3 = new AtomicIntegerDemo(atomicInteger);
demo1.start();
demo2.start();
demo3.start();
demo1.join();
demo2.join();
demo3.join();
System.out.println(atomicInteger.get());//300000
}
}
由上面执行结果可知AtomicInteger是一个原子的操作,并发操作是线程安全的。AtomicInteger主要通过CAS自旋+volatile的方案实现,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的开销。
使用CAS操作内存数据时,数据发生过变化也能更新成功,如A——>B——>A,最后一个CAS预期数据A实际已经发生过更改,但也能修改成功,这就产生了ABA的问题。ABA的解决思路一般是使用版本号,每次变更都带上版本号。JDK提供了两个类AtomicStampedReference和AtomicMarkableReference解决ABA的问题。
AtomicStampedReference在CAS的基础上增加了一个Stamp(印戳或者标记),使用这个标识可以判断数据是否发生变化。
public static void main(String[] args) {
String str1 = "aaa";
String str2 = "bbb";
//初始化AtomicStampedReference 初始值是aaa,印戳是1
AtomicStampedReference reference = new AtomicStampedReference(str1, 1);
//cas比较str1=aaa ,reference.getStamp()=1,就把str2的值更新到新值,印戳加1
reference.compareAndSet(str1, str2, reference.getStamp(), reference.getStamp() + 1);
//当前的值为:bbb印戳是:2
System.out.println("当前的值为:" + reference.getReference() + "印戳是:" + reference.getStamp());
boolean flag = reference.weakCompareAndSet(str2, "ccc", 2, reference.getStamp() + 1);
//更新后的值为:bbb是否更新成功:false,更新失败,因为印戳4和2比较不对
System.out.println("更新后的值为:" + reference.getReference() + "是否更新成功:" + flag);
}
AtomicMarkableReference是AtomicStampedReference的简化版,不关心修改过几次,只关心是否修改过,标志属性是boolean类型,其值只记录是够修改过。
public static void main(String[] args) {
AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(1, true);
boolean b = atomicMarkableReference.compareAndSet(1, 2, true, false);
//是否更新成功true更新后的值:2更新后的标志 mark:false
System.out.println("是否更新成功" + b + "更新后的值:" + atomicMarkableReference.getReference() + "更新后的标志 mark:" + atomicMarkableReference.isMarked());
boolean b1 = atomicMarkableReference.compareAndSet(2, 3, true, false);
//是否更新成功false更新后的值:2更新后的标志 mark:false
System.out.println("是否更新成功" + b1 + "更新后的值:" + atomicMarkableReference.getReference() + "更新后的标志 mark:" + atomicMarkableReference.isMarked());
}
总结:
1、操作系统层面的CAS是一条CPU原子指令(cmpxchg指令),由于该指令具有原子性,因此使用CAS不会造成数据不一致的问题。
2、使用CAS无锁编程步骤如下,获取字段中的期望值(旧值)与内存地址上的值作比较(没有修改之前的旧值),如果两个值相等,就把新值放在字段的内存地址上,失败则自旋。
3、并发线程越少,自旋越少CAS性能越高,反之并发线程越多,自旋的次数越多,CAS的性能就越低。
4、jdk中的原子类大都是使用CAS实现的。
5、解决ABA问题使用版本号,jdk中有两个类AtomicStampedReference和AtomicMarkableReference解决ABA的问题。
参考:
《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著