并发编程之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是否相同
如果只最求结果一致,不关心中途有没有改变过或者改过多少次,后面两个类不用了解

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容