死磕Java——CAS

一、死磕Java——CAS

前面我们说到volatile不保证原子性,解决办法就是使用AtomicInteger代替int,但是为什么使用AtomicInteger就可以保证了原子性了,是因为AtomicInteger实现的就是CAS思想Unsafe的支持。

1.1.CAS是什么

AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.compareAndSet(5, 10);

CAS:即比较和交换(compareAndSet),CAS的思想比较简单就是三个值:当前内存值V,旧的预期值A,和要更新的值B,当且仅当内存值V等于预期值A,才将内存值修改为B,并返回true,否则什么都不做,返回false。下面就以atomicInteger.getAndIncrement();分析一下AtomicInteger使用的CAS思想。

1.2.CAS的原理

使用AtomicInteger

public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(5);
    atomicInteger.compareAndSet(5, 10);
    atomicInteger.getAndIncrement();
}

compareAndSet方法

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

unsafe.getAndAddInt(this, valueOffset, 1)说明:

  • this:代表当前对象,这里就是外部newatomicInteger对象;
  • valueOffset:代表当前对象的内存地址。
  • 1:就是加1。
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;

说明:

上述代码就是根据对象的内存地址获取当前内存的值,注意的是private volatile int value;添加了volatile关键字,所以,保证了可见性。

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

说明:

  • unsafe类是CAS的核心类,由于Java无法直接操作底层系统,只能通过native修饰的本地方法操作,基于unsafe类就可以直接操作内存中的数据。
  • getAndAddInt(Object var1, long var2, int var4)中:Object var1:当前对象,long var2:当前对象的内存地址,int var4:需要更新的值,这里就是1。
  • var5 = this.getIntVolatile(var1, var2);:就是取到当前对象的内存值;
  • 假如现在存在两个线程,跑在不同的cpu上,内存中atomicInteger的原始值为5,两个线程都拷贝一份到自己的工作内存中,
  • 线程A通过getIntVolatile(var1, var2)拿到value值5,这时线程A被挂起。
  • 线程B也通过getIntVolatile(var1, var2)方法获取到value值5,线程B没有被挂起,并执行compareAndSwapInt方法比较内存值也为5,成功修改内存值为10。
  • 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值(5)和内存的值(10)不一致,说明该值已经被其它线程提前修改过了,那只能重新来一遍了。
  • 重新获取value值,因为变量valuevolatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

1.3.CAS的缺点

前面的代码分析中我们知道getAndAddInt方法有一个do-while循环,如果CAS失败,就会进行一直尝试比较,如果很长时间都不成功,就会增加CPU的开销。所以CAS的一个缺点就是循环时间长开销大,由于this表示的是当前对象,所以,存在另外一个缺点就是只能保证一个共享变量的原子操作。最重要的缺点就是ABA问题

1.3.1.什么是ABA问题

前面分析CAS思想的时候,我们知道一个线程会先获取Value的值,比较和交换的时候再获取内存的值和手里的value进行比较,说的是如果一致就表示没有被其他线程修改过,然后就执行自己的交换操作,但是,如果,一个线程修改了,然后另外还有一个线程又修改会原来的值,这个时候一比较还是一样的,这就是ABA问题。简单讲就是狸猫换太子。如果业务中不关心中间操作,只在乎开始和结尾是否一致就可,就不必要解决ABA 问题。

1.3.2.使用原子引用和版本机制解决ABA问题

java.util.concurrent.atomic包下存在一个AtomicReference类,就是原子引用,CAS比较的只是内存中的值,现在增加一个版本号,比较值的同时再比较版本后是否一致。使用AtomicStampedReference带时间戳的原子引用来解决ABA问题。

 public static void main(String[] args) {
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(10, 1);

        new Thread(() -> {
            int stamp = reference.getStamp();
            System.out.println("T1拿到的第一次的版本号:" + stamp);
            // 先暂停1秒,等T2线程拿到相同的初始版本号
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            reference.compareAndSet(10, 101, reference.getStamp(), reference.getStamp() + 1);
            System.out.println("T1线程第一次操作后的版本号为:"  + reference.getStamp());
            reference.compareAndSet(101, 10, reference.getStamp(), reference.getStamp() + 1);
            System.out.println("T1线程第二次操作后的版本号为:"  + reference.getStamp());
        }, "T1").start();

        new Thread(() -> {
            int stamp = reference.getStamp();
            System.out.println("T2拿到的第一次的版本号:" + stamp);
            // 先暂停3秒,等T1线程有充分的时候做一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = reference.compareAndSet(10, 2019, stamp, stamp + 1);
            System.out.println("当前内存中的最新值为:" + reference.getReference());
            System.out.println("T2线程在T1线程执行完ABA问题后在执行的结果为:" + b);
        }, "T2").start();

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

推荐阅读更多精彩内容