并发编程之CAS

我们知道保证线程安全的三个要素是原子性,可见性,有序性

CAS(Compare And Swap),指令级别保证某一内存地址V上的值的更新修改是一个原子操作
需要三个值:一个内存地址V,一个该线程拿到的值A,一个期望更新后的值B
思路:如果地址V上的实际值和该线程拿到的值A相等,就给地址V赋给新值B,如果不是,不做任何操作。
循环(死循环,自旋)里不断的进行CAS操作

JDK里为我们提供了这些原子操作类
更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

我们看看AtomicInteger的源码知道里面的compareAndSet方法是由Unsafe去实现的,点进去看全是native方法..
我们不知道compareAndSwapInt本地方法的实现,我们就退一步自己实现一个AtomicInteger类吧

我们定义一个Test类,和AtomicInteger一样先获取Unsafe对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
问题来了,会有个报错Caused by: java.lang.SecurityException: Unsafe
查了资料后知道Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 仅在引导类加载器`BootstrapClassLoader`加载时才合法
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {    
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

那如若想使用这个类,该如何获取其实例?有如下两个可行方案。
其一,从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
其二,通过反射获取单例对象theUnsafe.
我们就用第二种

public class Test3 {
    static volatile long valueOffset;
    static Unsafe unsafe;
    static {
        initUnsafe();
        try {
            valueOffset = unsafe.objectFieldOffset
                    (Test3.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private static void initUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe1 =  (Unsafe) f.get(null);
            unsafe = unsafe1;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
    volatile int value;
    public void test() {
        System.out.println(unsafe.compareAndSwapInt(this, valueOffset , value, 10));
        System.out.println("value:" + value);
        System.out.println(unsafe.compareAndSwapInt(this, valueOffset , 1, 20));
        System.out.println("value:" + value);
    }
    public static void main(String[] args) {
        new Test3().test();
    }
}
打印结果
true
value:10
false
value:10

关于valueOffset,我们从static代码块里面可以看到,这个值是取的Test3这个class对象里面的value域,我们就先把它理解成前面说的三个值中的V吧,第一个打印操作拿valueOffset里的值和该线程拿到的value比较,发现相等就把value赋值成10,第二个cas操作不等,就是false了
我们现在写个value的自增方法然后多线程去跑,看看会不会有问题

public void increment() {
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
//        for(;;) {
//            boolean b = unsafe.compareAndSwapInt(this, valueOffset, value, value + 1);
//            if (b) {
//                break;
//            }
//        }
    value ++;
}
public static void main(String[] args) {
    CountDownLatch countDownLatch = new CountDownLatch(100);
    Test3 test3 = new Test3();
    for (int i = 0; i < 100; i ++) {
        new Thread(()->{
            test3.increment();
            countDownLatch.countDown();
        }).start();
    }
    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("value-----" + test3.value);
}

先用value++的方法跑了几次,基本在98,99,100之间

然后increment()方法里注释打开,value++注释掉,打印的结果总是100;
所以说,我们用CAS实现的自增方法是线程安全的
关于CountDownLatch这个工具类后面再讲,包括CyclicBarrier,Semaphore,Exchange还有Fork/Join思想都是jdk提供的并发包里的工具类
在这里CountDownLatch是用来保证主函数的countDownLatch.await()后的打印一定是我们for循环里的子线程执行完成之后执行


我们实现个引用类型的原子操作
volatile Cat value = new Cat(1);
private void test() {
    System.out.println(unsafe.compareAndSwapObject(this,valueOffset,value,new Cat(3)));
    System.out.println(value.age);
    value.age = 13;
    System.out.println(unsafe.compareAndSwapObject(this,valueOffset,new Cat(1),new Cat(15)));
    System.out.println(value.age);
}
public static void main(String[] args) {
    Test3 test3 = new Test3();
    test3.test();
}
class Cat{
    int age;
    Cat(int age){
        this.age = age;
    }
}
打印结果
true
3
false
13

一些思考:
查阅CAS相关资料的时候总是听到ABA问题,回顾下之前CAS定义
需要三个值:一个内存地址V,一个该线程拿到的值A,一个期望更新后的值B
思路:如果地址V上的实际值和该线程拿到的值A相等,就给地址V赋给新值B,如果不是,不做任何操作。

我们的目的是把A改成B,这里如果地址V上的值在我们获取改线程拿到的值A之前,已经经历过一次CAS操作被改成了B,然后又一次CAS操作改成了A,我们第三次去把A改成B的时候虽然成功了,但是我们不清楚他之前有没有进行过CAS操作,这就是ABA问题.这里jdk也提供了两个类
AtomicMarkableReference,boolean 有没有动过
AtomicStampedReference 动过几次
看看源码,理一下实现的思路
AtomicMarkableReference先看构造函数,只有一个,需要传入初始的引用对象和一个默认boolean值mark(假设此时引用对象为null,boolean为false)
里面定义了一个内部类,把两个初始值赋值给该new出该内部类对象pair,他的compareAndSet()要传入4个值,不仅要判断引用对象是否相同,还要判断mark是否相同
如果只最求结果一致,不关心中途有没有改变过或者改过多少次,后面两个类不用了解

你可能感兴趣的:(并发编程之CAS)