cas

CAS,即Compare-And-Swap比较并交换,它是一条CPU并发原语

它的功能是判断内存某个位置的值是否为预期值,如果是则改为新的值,这个过程是原子的

通过AtomInteger举例:

public class CasDemo {
    
    public static void main(String[] args) {

        compareAndSet();

    }

    private static void compareAndSet() {

        AtomicInteger integer = new AtomicInteger(4);

        for (int i = 0; i < 5; i++) {
            final int n = i;
            new Thread(() -> {
                // 比较并设置,如果主内存中的值与期望值相同,则设置新值并返回true,否则返回false
                boolean result = integer.compareAndSet(4, n);
                System.out.println(
                        Thread.currentThread().getName() + " result: " + result + ", current value: " + integer.get());
            }, "thread-" + i).start();
        }

    }

}

结果:

thread-0 result: true, current value: 0
thread-3 result: false, current value: 0
thread-4 result: false, current value: 0
thread-2 result: false, current value: 0
thread-1 result: false, current value: 0

可见只有一个线程设置新值成功,多个线程对同一变量的操作具有原子性

compareAndSet方法是如何做到在多线程环境下,安全的对变量比较并设置值的呢?

通过查看源码我们得知

/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

方法调用的是unsafe.compareAndSwapInt方法,而unsafe这个对象又是什么呢?

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

看到AtomInteger类有这么几个变量:

  1. Unsafe unsafe
  2. long valueOffset
  3. volatile int value

Unsafe类存在于rt.jar中的sun.misc包中,是CAS的核心类,由于Java无法直接访问操作系统底层,需要通过本地方法(native)来访问,Unsafe类就是Java访问系统底层的一种方式,可以直接操作特定内存中的数据

valueOffset表示该变量在内存中的内存偏移地址Unsafe类就是通过内存偏移地址获取数据

value是变量的值,利用volatile修饰后保证了多线程间内存的可见性

CAS并发原语体现在Java语言中就是sum.misc.Unsafe类中的各个方法,调用Unsafe类中的方法时,JVM会实现CAS汇编指令,调用Unsafe类的方法实际就是执行一套CPU原子指令,是不允许被终端的,不会造成数据不一致的问题

再来看AtomIntegercompareAndSet方法

// AtomInteger
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

可以看到AtomIntegercompareAndSet方法调用的是UnsafecompareAndSwapInt方法

// Unsafe
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

UnsafecompareAndSwapInt方法是一个本地方法,大致就是对比var1对象的var2地址的数据和期望值var4,如果相等就修改为var5

CAS的缺点

  1. 长时间循环操作的开销大

    看一下Unsafe类中的getAndAddInt方法

    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;
    }
    

    当CAS失败时,会循环尝试,如果CAS长时间不成功,可能会给CPU带来性能开销

  2. 只能保证一个共享变量的原子操作

    当对一个共享变量进行操作时,我么你可以使用循环CAS的方式保证原子操作,但当对多个共享变量操作时,循环CAS无法保证操作的原子性,只能用锁来保证

  3. 会引出ABA问题

ABA问题

当T1、T2两个线程同时访问一个变量时,两线程同时取出变量N为A,假设T2执行比T1快,T2修改值为B,然后再将其修改为A,此时T1线程进行CAS发现内存中值仍然是A,然后更新至成功

尽管T1线程操作执行成功,但是不代表过程没有问题

先来看一下原子引用(AtomicReference)

public class AtomicReferenceDemo {

    public static void main(String[] args) {

        Person p1 = new Person("p1", 10);
        Person p2 = new Person("p2", 20);

        AtomicReference atomicReference = new AtomicReference<>();
        atomicReference.set(p1);

        System.out.println(atomicReference.compareAndSet(p1, p2) + " ref: " + atomicReference.get());
        System.out.println(atomicReference.compareAndSet(p1, p2) + " ref: " + atomicReference.get());

    }

}

class Person {

    public String name;
    public int age;

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

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

结果:

true ref: Person{name='p2', age=20}
false ref: Person{name='p2', age=20}

原子引用就是将自定义的引用类型变为原子的引用类型,实现对引用类型数据的原子操作

与其他类一样,原子引用也会带来ABA问题

public class AbaDemo {

    private static AtomicReference atomicReference = new AtomicReference<>(100);

    public static void main(String[] args) {

        atomicRef();

    }

    private static void atomicRef() {

        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "thread-1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 200) + " ref: " + atomicReference.get());
        }, "thread-2").start();

    }

}

结果:

true ref: 200

我们可以通过对原子引用变量添加一个版本号,通过比较版本号是否相同来判断数据是否被修改过,这就是AtomicStampedReference

public class AbaDemo {

    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 0);

    public static void main(String[] args) {

        atomicStampedRef();

    }

    private static void atomicRef() {

        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "thread-1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 200) + " ref: " + atomicReference.get());
        }, "thread-2").start();

    }

    private static void atomicStampedRef() {

        new Thread(() -> {

            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " stamp-1: " + stamp);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " stamp-2: " + stamp);

            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " stamp-3: " + stamp);

        }, "thread-1").start();

        new Thread(() -> {

            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " stamp: " + stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean res = atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 1);
            stamp = atomicStampedReference.getStamp();

            System.out.println(Thread.currentThread().getName() + " res:" + res + " stamp: " + stamp);
            System.out.println(Thread.currentThread().getName() + " ref:" + atomicStampedReference.getReference());

        }, "thread-2").start();

    }

}

结果:

thread-1 stamp-1: 0
thread-2 stamp: 0
thread-1 stamp-2: 1
thread-1 stamp-3: 2
thread-2 res:false stamp: 2
thread-2 ref:100

可以看到两个线程第一次获取stamp均为0,然后thread-1进行了一次ABA操作,之后stamp(也就是stamp-3)变为了3,在当thread-2按照之前获取的版本号去更新数据时,虽然数据值仍然为100但是版本号已经变更,所以更新失败

你可能感兴趣的:(cas)