CAS详解

CAS在底层源码中是使用非常广的,像我之前的HashMap源码解析、volatile详解等文章都有提到CAS。本文将详细介绍CAS。


欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。


一、什么叫CAS?

CAS,是 compare and swap 的缩写,即比较并交换。它是一种基于乐观锁的操作。它有三个操作数,内存值V,预期值A,更新值B。当且仅当A和V相同时,才会把V修改成B,否则什么都不做。之前说到AtomicInteger用到了CAS,那么先从这个类说起。看如下代码:

public static void main(String[] args){
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,50));
        System.out.println(atomicInteger.compareAndSet(5,100));
}

AtomicInteger有一个compareAndSet方法,有两个操作数,第一个是期望值,第二个是希望修改成的值。首先初始值是5,第一次调用compareAndSet方法的时候,将5拷贝回自己的工作空间,然后改成50,写回到主内存中的时候,它期望主内存中的值是5,而这时确实也是5,所以可以修改成功,主内存中的值也变成了50,输出true。第二次调用compareAndSet的时候,在自己的工作内存将值修改成100,写回去的时候,希望主内存中的值是5,但是此时是50,所以set失败,输出false。这就是比较并交换,也即CAS。

二、CAS的工作原理

简而言之,CAS工作原理就是UnSafe类自旋锁
1、UnSafe类:
UnSafe类在jdk的rt.jar下面的一个类,全包名是sun.misc.UnSafe。这个类大多数方法都是native方法。由于Java不能操作计算机系统,所以设计之初就留了一个UnSafe类。通过UnSafe类,Java就可以操作指定内存地址的数据。调用UnSafe类的CAS,JVM会帮我们实现出汇编指令,从而实现原子操作。现在就来分析一下AtomicInteger的getAndIncrement方法是怎么工作的。看下面的代码:

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

这个方法调用的是unsafe类的getAndAddInt方法,有三个参数。第一个表示当前对象,也就是你new 的那个AtomicInteger对象;第二个表示内存地址;第三个表示自增步伐。然后再点进去看看这个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;
    }

这里的val1就是当前对象,val2是内存地址,val4是1,也就是自增步伐。首先把当前对象主内存中的值赋给val5,然后进入while循环。判断当前对象此刻主内存中的值是否等于val5,如果是,就自增,否则继续循环,重新获取val5的值。这里的compareAndSwapInt方法就是一个native方法,这个方法汇编之后是CPU原语指令,原语指令是连续执行不会被打断的,所以可以保证原子性。

2、自旋锁:
所谓的自旋,其实就是上面getAndAddInt方法中的do while循环操作。当预期值和主内存中的值不等时,就重新获取主内存中的值,这就是自旋。

三、CAS的缺点

缺点有三个。
1、循环时间长,开销大。
synchronized是加锁,同一时间只能一个线程访问,并发性不好。而CAS并发性提高了,但是由于CAS存在自旋操作,即do while循环,如果CAS失败,会一直进行尝试。如果CAS长时间不成功,会给CPU带来很大的开销。

2、只能保证一个共享变量的原子性。
上面也看到了,getAndAddInt方法的val1是代表当前对象,所以它也就是能保证这一个共享变量的原子性。如果要保证多个,那只能加锁了。

3、引来的ABA问题。

  • 什么是ABA问题?

假设现在主内存中的值是A,现有t1和t2两个线程去对其进行操作。t1和t2先将A拷贝回自己的工作内存。这个时候t2线程将A改成B,刷回到主内存。此刻主内存和t2的工作内存中的值都是B。接下来还是t2线程抢到执行权,t2又把B改回A,并刷回到主内存。这时t1终于抢到执行权了,自己工作内存中的值的A,主内存也是A,因此它认为没人修改过,就在工作内存中把A改成了X,然后刷回主内存。也就是说,在t1线程执行前,t2将主内存中的值由A改成B再改回A。这便是ABA问题。看下面的代码演示(代码涉及到原子引用,请参考下面的原子引用的介绍):

class ABADemo {
   static AtomicReference<String> atomicReference = new AtomicReference<>("A");
   public static void main(String[] args){
          new Thread(() -> {
              atomicReference.compareAndSet("A","B");
              atomicReference.compareAndSet("B","A");
              },"t2").start();
          new Thread(() -> {
              try { 
                   TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
                   e.printStackTrace(); 
              }
              System.out.println(atomicReference.compareAndSet("A","C") 
                                           + "\t" + atomicReference.get());
              },"t1").start();
   }
}

这段代码执行结果是"true C",这就证明了ABA问题的存在。如果一个业务只管开头和结果,不管这个A中间是否变过,那么出现了ABA问题也没事。如果需要A还是最开始的那个A,中间不许别人动手脚,那么就要规避ABA问题。要解决ABA问题,先看下面的原子引用的介绍。

  • 原子引用:

JUC包下给我们提供了原子包装类,像AtomicInteger。如果我不仅仅想要原子包装类,我自己定义的User类也想具有原子操作,怎么办呢?JUC为我们提供了AtomicReference<V>,即原子引用。看下面的代码:

@AllArgsConstructor
class User {
    int age;
    String name;

    public static void main(String[] args){
        User user = new User(20,"张三");
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(user);
    }
}

像这样,就把User类变成了原子User类了。

  • 解决ABA问题思路:

我们可以这个共享变量带上一个版本号。比如现在主内存中的是A,版本号是1,然后t1和t2线程拷贝一份到自己工作内存。t2将A改为B,刷回主内存。此时主内存中的是B,版本号为2。然后再t2再改回A,此时主内存中的是A,版本号为3。这个时候t1线程终于来了,自己工作内存是A,版本号是1,主内存中是A,但是版本号为3,它就知道已经有人动过手脚了。那么这个版本号从何而来,这就要说说AtomicStampedReference这个类了。

  • 带时间戳的原子引用(AtomicStampedReference):
    这个时间戳就理解为版本号就行了。看如下代码:
class ABADemo {
        static AtomicStampedReference<String> atomicReference = new AtomicStampedReference<>("A", 1);
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);// 睡一秒,让t1线程拿到最初的版本号
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicReference.compareAndSet("A", "B", atomicReference.getStamp(), atomicReference.getStamp() + 1);
                atomicReference.compareAndSet("B", "A", atomicReference.getStamp(), atomicReference.getStamp() + 1);
            }, "t2").start();
            new Thread(() -> {
                int stamp = atomicReference.getStamp();//拿到最开始的版本号
                try {
                    TimeUnit.SECONDS.sleep(3);// 睡3秒,让t2线程的ABA操作执行完
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(atomicReference.compareAndSet("A", "C", stamp, stamp + 1));
            }, "t1").start();
        }
}

初始版本号为1,t2线程每执行一次版本号加。等t1线程执行的时候,发现当前版本号不是自己一开始拿到的1了,所以set失败,输出false。这就解决了ABA问题。

总结:

1.什么是CAS? ------ 比较并交换,主内存值和工作内存值相同,就set为更新值。
2.CAS原理是什么? ------ UnSafe类和自旋锁。理解那个do while循环。
3.CAS缺点是什么? ------ 循环时间长会消耗大量CPU资源;只能保证一个共享变量的原子性操作;造成ABA问题。
4.什么是ABA问题? ------ t2线程先将A改成B,再改回A,此时t1线程以为没人修改过。
5.如何解决ABA问题?------ 使用带时间戳的原子引用。

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

推荐阅读更多精彩内容