JAVA线程安全之CAS机制

 

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();

    }
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值