JAVA线程安全之CAS机制
1、什么是CAS?
Compare and Swap,即比较再交换,是CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。我们先看一段代码
public class CSADemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);//从主物理内存拿的值
//expect:期望值,update:更新的值
//如果期望值和主物理内存拿的快照值相同,则更新
System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t current value:"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2021)+"\t current value:"+atomicInteger.get());
}
}
true current value:2020
false current value:2020
这里的AtomicInteger()类是JUC包下的原子整型类,他是基于CAS算法实现的。我们知道JVM线程对对象进行操作时,它会把对象从主物理内存拷贝到自己的工作内存中进行操作,操作完成之后要写回到朱物理内存中去,在写回的过程中进行CAS操作,CAS思想是把从内存中拿到对象的快照与他的期望值进行比较,如果相同则更新为要更新的值。其伪代码可以表示为:
do{
工作内存中的旧数据;
基于旧数据的新数据;
}while(!CAS(期望值(也就是主内存里的值),旧数据,新数据))
通俗的来讲CPU要更新一个值,如果想改的值不再是原来的值,就不会更新。这种机制满足了JMM规定的线程操作的原子性
2、CAS的底层原理
2.1、UnSafe类
是CAS的核心类,由于java方法无法直接访问底层系统,需要本地native方法来访问,unsafe类相当于后门,基于该类可以直接操作特定的内存数据。unsafe类存在于sun.misc包下,其内部方法相当于c指针一样直接操作内存,因为java中的cas操作的执行依赖于unsafe类的方法。unsafe类中的方法都是由native修饰的,也就是说unsafe类中的方法可以直接调用底层操作系统的资源执行相应的任务。
我们来看一段代码:以原子整型类的++操作为例
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
//变量valueoffset表示内存的偏移量,每次偏移量为1
//this表示当前对象
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));
return var5;
}
unsafe类就可以根据偏移量获取内存地址
cas实现原子性的底层解释:
CAS是CPU的并发原语,他是属于系统原语,调用Unsafe类中的CAS方法时,jvm会生产相应的CAS汇编指令,他是由若干条指令实现的,用于实现某个功能的一个过程,原语是连续的,中间不允许被打断,也就是说CAS是CPU的原子指令,保证了数据的一致性。
底层汇编原语:(Atomic::cmpxchg(x,addr,e))==e
2.2、自旋锁
3、CAS的缺点
与sysnronized相比,sysnronized并发差,cas并发强
3.1、循环时间长CPU开销大
以AtomicInteger类的getAndAddInt方法为例,他里面有个do{while}循环,如果CAS失败它会一直进行尝试,直到成功为止,它会给CPU带来很大的开销
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;
}
3.2、只能保证一个共享变量的原子操作
3.3、会产生ABA的问题
ABA问题:one线程从主物理内存中取出A,two线程也从主物理内存拿到A,由于one线程比two线程执行速度快,one线程将A改为了B,然后再改为A,two线程在进行比较交换的时候,主物理内存已经不是以前的A,而是经过改为B再改为A的过程,但two线程不知道,继续进行自己的操作。尽管two线程操作是成功的,但并不代表此次过程没有问题
4、如何解决ABA的问题
4.1、原子引用
原子引用包装类AtomicReference<V>,这个类可以用于类的原子更新引用
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("张三", 22);
User li4 = new User("李四",23);//相当于主物理内存的地址
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);//从主物理内存拿到张三
//调用CAS方法,比较,如果是z3,就改为li4
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
}
}
4.2、原子引用+版本号机制(类似于时间戳)
juc包下的AtomicStampedReference类解决了这一问题
看下一段代码
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("以下ABA问题的产生");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//true 101
System.out.println(atomicReference.compareAndSet(100, 101)+"\t"+atomicReference.get());
},"t2").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===================以下解决ABA问题");
new Thread(()->{
//拿到初始版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第二次版本号"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第三次版本号"+atomicStampedReference.getStamp());
},"t3").start();
new Thread(()->{
//拿到初始版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第一次版本号"+stamp);
try {
//确保完成了一次ABA操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
//最新版本号
int newStamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t修改成功否"+result+"\t最新版本号"+newStamp);
System.out.println(Thread.currentThread().getName()+"\t当前实际最新值"+atomicStampedReference.getReference());
},"t4").start();
}
}