Java-CAS

原子性操作

原子即为不可再分的,原子操作即要么所有操作全部完成 要么全不完成
用synchronized包围的代码块或方法就是原子操作。对于线程来讲,synchronized包围代码,只会全部完成,不会执行一半而中断。
synchronized是一个很重的操作,如果执行代码很简单,例如i++,很多线程都阻塞在外面很划不来,为了解决这种问题,引出了CAS(Compare And Swap),比较并且交换。系统提供了很多原子变量,Atomic开头的变量都是实现了CAS,例如AtomicBoolean、AtomicInteger等。

CAS

CAS原理

我们下面举个例子:
假如我们现在有多个线程执行count++这个操作。

CASCount.jpg
  1. 首先从内存中取出count的值。(假如这时是0)
  2. 然后进行累加操作。(count变为了1)
  3. 这时在从内存中取出count的值,如果与第一步取到的值相等,则将累加操作后的值写入内存,否则 说明有别的线程改过了,这时再重复第一步操作,直到完成赋值操作。

总结:
获取内存中的值 ,进行操作,再写入内存的时候,进行判断当前内存中的值是否与之前取出的值是否一致,不一致的话以内存中的新值,重新计算,反复执行(自旋,其实就是死循环),直到内存中的值没有在经过修改,才进行写入操作。

这里就引出了两个概念:

  • 悲观锁 先锁再操作(synchronized)
  • 乐观锁 先操作再判断是否进行修改

CAS和synchronized性能比较:
正常生产环境下CAS的效率是要高于synchronized,因为synchronized会阻塞线程,线程阻塞的时候会发生上下文切换(3-5ms),CAS执行指令的时间大概在0.6ns。
高度竞争,特意设计的情况下synchronized会优于CAS。

为什么有CAS还需要synchronized

  • ABA问题:
    假设当前有两个线程ThreadA和ThreadB,一个变量A
    ThreadA想把A修改为B,根据上面介绍的CAS原理,我们知道,修改的时候会判断A是否被修改过,用段伪代码表示也就是if(A==A) A = B
    ThreadB比ThreadA跑的快,ThreadA把A修改为C,然后又改回为A。
    可是ThreadA认为A没被修改过。

解决办法:版本戳 要求每个线程修改值的时候 加入一个版本戳
jdk中提供了相关的操作
AtomicMarkReference(有没有变过) AtomicStampedReference(变了几次)

public class UserAtomicMarkable {
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>("red",0);

    public static void main(String[] args) throws InterruptedException {
        final int oldStamp = atomicStampedReference.getStamp();
        final String oldReference = atomicStampedReference.getReference();
        System.out.println(oldReference + "----------" + oldStamp);

         Thread  threadA = new Thread(){
            @Override
            public void run() {
                super.run();
                System.out.println(Thread.currentThread().getName() + "当前变量值 " + oldReference + "-当前版本戳 " + oldStamp );
                atomicStampedReference.compareAndSet(oldReference,oldReference + "Java",oldStamp,oldStamp + 1);
            }
        };

        Thread  threadB = new Thread(){
            @Override
            public void run() {
                super.run();
                String reference = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + "当前变量值 " + reference + "-当前版本戳 " + stamp );
                atomicStampedReference.compareAndSet(reference,reference + "C",stamp,stamp + 1);
                String reference1 = atomicStampedReference.getReference();
                int stamp1 = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + "当前变量值 " + reference1 + "-当前版本戳 " + stamp1 );
            }
        };

        threadA.start();
        threadA.join();
        threadB.start();
        threadB.join();

        System.out.println(atomicStampedReference.getReference() + "----------" + atomicStampedReference.getStamp());
    }

}
  • 开销问题
    当竞争激烈的时候,会存在长时间完成不了操作 ,造成自旋,一直重试,会占用CPU资源。
    解决办法:换成synchronized。

  • 只能保证一个共享变量的原子操作
    CAS操作时,只能针对某个内存地址上的值进行修改,而一个地址往往只能保存一个变量。
    解决办法:AtomicReference把多个变量打包到一个对象中 替换对象

public class UseAtomicReference {
    static AtomicReference atomicReference;

    public static void main(String[] args) {
        User user = new User("xiaoming",22);
        atomicReference = new AtomicReference<>(user);
        User updateUser = new User("xiaohong",16);
        atomicReference.compareAndSet(user,updateUser);

        System.out.println(atomicReference.get());
        System.out.println(user);
    }

    static class User{
        private String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

你可能感兴趣的:(Java-CAS)