java基础----CAS

一.什么是cas

  CAS的全称是Compare-And-Swap,他是一条CPU并发原语。

  java中的CAS,都是通过unsafe类实现的,其主要的操作是,当一个线程从主内存拿到一个变量到自己工作内存,并经过计算处理,准备写回主内存的时候,会首先比对当前主内存的变量指向的内存地址里面的值,与期望值(线程一开始拿变量时,变量对应的值)是否相等,如果相等,则表示没有其他线程对这个变量操作过,随后就将要更新的值写进主内存中。假如不相等,则表示有线程修改过这个变量,则会把主内存中变量的最新值拿回去,重新做一次计算操作,以此循环。

二.cas的底层原理

下面是java atomicInteger的代码:

public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

下面是unsafe.class中的代码(这个类是java的原生类,在jdk的rt.jar/sun/misc里面):

//第一个参数var1为给定对象,var2为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//var5表示期望值,var4表示要添加的数值
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;
}

       一般来说,java无法直接访问底层系统,需要通过本地native方法来访问,而unsafe相当于一个后门,基于该类可以直接操作特定的内存。

  可以看出,unsafe类可以直接操作指针,根据给定的对象和内存偏移量迅速地获取到变量值。在do...while中,首先根据对象和内存偏移量拿到一个值作为期望值,然后在while的条件语句中,再一次根据对象和内存偏移量获取变量的当前值,并与期望值作出对比,如果相等,则加上var4。假如不相等,则从新获得期望值,并循环。

三.CAS的缺点

  最明显的一点,CAS有可能出现一个很长的循环,假如线程一直没有写成功,那他就会一直自旋,非常消耗CPU资源。而且它只能保证一个共享变量的操作,对于多变量操作,只能加锁。

  其次就是ABA问题,导致ABA问题的原因是两个线程的工作时间差距太大。例如线程a需要10秒,线程b需要2秒,它们同时从主线程中拿到x1并开始工作,在10秒中,线程b先把x1改成x2,x2改成x3。。。。最后x3又该回去x1,这时候线程a算出结果x4并对主内存中的变量进行CAS操作,通过比较期望值和现时变量的值发现是一致的,就认为这段时间里面没有其他线程对变量进行修改过,但是实际上,这个变量以及是被修改过多次了。

  那么我们改如何解决ABA问题呢?

  这里涉及到一个概念叫原子引用,在实现原子性的过程中,我们可以使用java 里面的atomic类,但是有时候一些数据类需要我们自己去定制,那这些类又怎么实现原子性呢。java里面有个类叫AtomicReference,是一个原子封装类,把我们自己定义的数据类传进去之后,就可以基于CAS实现原子性。下面举个例子

import java.util.concurrent.atomic.AtomicReference;

class User{
    String userName;
    int age;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

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

    public String toString(){
        return "userName: " + this.userName + ", age: " + this.age;
    }
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User u1 = new User("z3",22);
        User u2 = new User("li4", 34);
        AtomicReference<User> atomicReference = new AtomicReference<User>();
        atomicReference.set(u1);
        System.out.println(atomicReference.compareAndSet(u1,u2) + "	" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(u1,u2) + "	" + atomicReference.get().toString());
    }
}

  然后,在原子引用的基础上,延伸出了版本原子引用,就是在CAS的基础上,对主内存中的数据记录一个版本号(时间戳),这样在实现CAS的过程中,除了比对期望值和实际值是否相等之外,还会比对版本号是否有变动过,这样可以准确地知道数据究竟有没有被修改过,也可以有效地规避ABA问题。下面还是上一段代码:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);

    public static void main(String[] args) {

        //ABA问题演示,最终t2线程成功修改了变量的值
        /*
        new Thread(()->{
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        },"t1").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicReference.compareAndSet(100, 200);
        },"t2").start();
         */

        //版本原子引用下解决ABA原子引用问题示例
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "	当前版本" + atomicStampedReference.getStamp() + "	当前值" + atomicStampedReference.getReference());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
            stamp = stamp + 1;
            System.out.println(Thread.currentThread().getName() + "	当前版本" + atomicStampedReference.getStamp() + "	当前值" + atomicStampedReference.getReference());
            atomicStampedReference.compareAndSet(101,100,stamp,stamp+1);
            System.out.println(Thread.currentThread().getName() + "	当前版本" + atomicStampedReference.getStamp() + "	当前值" + atomicStampedReference.getReference());
        },"t3").start();

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "	当前版本" + atomicStampedReference.getStamp() + "	当前值" + atomicStampedReference.getReference());
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(atomicStampedReference.compareAndSet(100,200,stamp,stamp+1)){
                System.out.println("t4 修改成功!!!");
            } else {
                System.out.println("t4 修改失败!!!");
            }
        },"t4").start();

        while (Thread.activeCount() > 2){
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + "	当前版本" + atomicStampedReference.getStamp() + "	当前值" + atomicStampedReference.getReference());
    }
}

  运行结果:

    t3 当前版本1 当前值100
    t4 当前版本1 当前值100
    t3 当前版本2 当前值101
    t3 当前版本3 当前值100
    t4 修改失败!!!
    main 当前版本3 当前值100

  从上面可以看到,线程t3将变量的值从100修改成101,再从101修改为100,最后t4想去将变量的值从100修改为200的时候,修改失败,因为t4一开始拿的版本号是1,最后去做写操作的时候发现版本号是3,虽然变量的值都是100,但是变量明显被人修改过了,因此修改失败。