并发编程的基石——CAS机制

2022-10-10,,,,


本博客系列是学习并发编程过程中的记录总结。由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅。


java中提供了很多原子操作类来保证共享变量操作的原子性。这些原子操作的底层原理都是使用了cas机制。在使用一门技术之前,了解这个技术的底层原理是非常重要的,所以本篇博客就先来讲讲什么是cas机制,cas机制存在的一些问题以及在java中怎么使用cas机制。

其实java并发框架的基石一共有两块,一块是本文介绍的cas,另一块就是aqs,后续也会写博客介绍。

什么是cas机制

cas机制是一种数据更新的方式。在具体讲什么是cas机制之前,我们先来聊下在多线程环境下,对共享变量进行数据更新的两种模式:悲观锁模式和乐观锁模式。

悲观锁更新的方式认为:在更新数据的时候大概率会有其他线程去争夺共享资源,所以悲观锁的做法是:第一个获取资源的线程会将资源锁定起来,其他没争夺到资源的线程只能进入阻塞队列,等第一个获取资源的线程释放锁之后,这些线程才能有机会重新争夺资源。synchronized就是java中悲观锁的典型实现,synchronized使用起来非常简单方便,但是会使没争抢到资源的线程进入阻塞状态,线程在阻塞状态和runnable状态之间切换效率较低(比较慢)。比如你的更新操作其实是非常快的,这种情况下你还用synchronized将其他线程都锁住了,线程从blocked状态切换回runnable华的时间可能比你的更新操作的时间还要长。

乐观锁更新方式认为:在更新数据的时候其他线程争抢这个共享变量的概率非常小,所以更新数据的时候不会对共享数据加锁。但是在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就重试,直到成功为止。cas机制就是乐观锁的典型实现。

cas,是compare and swap的简称,在这个机制中有三个核心的参数:

  • 主内存中存放的共享变量的值:v(一般情况下这个v是内存的地址值,通过这个地址可以获得内存中的值)
  • 工作内存中共享变量的副本值,也叫预期值:a
  • 需要将共享变量更新到的最新值:b

如上图中,主存中保存v值,线程中要使用v值要先从主存中读取v值到线程的工作内存a中,然后计算后变成b值,最后再把b值写回到内存v值中。多个线程共用v值都是如此操作。cas的核心是在将b值写入到v之前要比较a值和v值是否相同,如果不相同证明此时v值已经被其他线程改变,重新将v值赋给a,并重新计算得到b,如果相同,则将b值赋给v。

值得注意的是cas机制中的这步步骤是原子性的(从指令层面提供的原子操作),所以cas机制可以解决多线程并发编程对共享变量读写的原子性问题。

cas机制优缺点

缺点

1. aba问题
aba问题:cas在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是a,接着变成b,最后又变成了a。经过检查这个值确实没有修改过,因为最后的值还是a,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,a——>b——>a问题就变成了1a——>2b——>3a。在jdk中提供了atomicstampedreference类解决aba问题,用pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareandset中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。

2. 可能会消耗较高的cpu
看起来cas比锁的效率高,从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。每个方法不能绝对的比另一个好,在线程之间竞争程度大的时候,如果使用cas,每次都有很多的线程在竞争,也就是说cas机制不能更新成功。这种情况下cas机制会一直重试,这样就会比较耗费cpu。因此可以看出,如果线程之间竞争程度小,使用cas是一个很好的选择;但是如果竞争很大,使用锁可能是个更好的选择。在并发量非常高的环境中,如果仍然想通过原子类来更新的话,可以使用atomiclong的替代类:longadder。

3. 不能保证代码块的原子性
java中的cas机制只能保证共享变量操作的原子性,而不能保证代码块的原子性。

优点

  • 可以保证变量操作的原子性;
  • 并发量不是很高的情况下,使用cas机制比使用锁机制效率更高;
  • 在线程对共享资源占用时间较短的情况下,使用cas机制效率也会较高。

java提供的cas操作类--unsafe类

从java5开始引入了对cas机制的底层的支持,在这之前需要开发人员编写相关的代码才可以实现cas。在原子变量类atomic中(例如atomicinteger、atomiclong)可以看到cas操作的代码,在这里的代码都是调用了底层(核心代码调用native修饰的方法)的实现方法。在atomicinteger源码中可以看getandset方法和compareandset方法之间的关系,compareandset方法调用了底层的实现,该方法可以实现与一个volatile变量的读取和写入相同的效果。在前面说到了volatile不支持例如i++这样的复合操作,在atomic中提供了实现该操作的方法。jvm对cas的支持通过这些原子类(atomic***)暴露出来,供我们使用。

而atomic系类的类底层调用的是unsafe类的api,unsafe类提供了一系列的compareandswap*方法,下面就简单介绍下unsafe类的api:

  • long objectfieldoffset(field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该unsafe函数中访问指定字段时使用。如下代码使用unsafe类获取变量value在atomiclong对象中的内存偏移。

    static {
       try {
           valueoffset = unsafe.objectfieldoffset
               (atomicinteger.class.getdeclaredfield("value"));
       } catch (exception ex) { throw new error(ex); }
    }
  • int arraybaseoffset(class arrayclass)方法:获取数组中第一个元素的地址。
  • int arrayindexscale(class arrayclass)方法:获取数组中一个元素占用的字节。
  • boolean compareandswaplong(object obj, long offset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。
  • public native long getlongvolatile(object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。
  • void putlongvolatile(object obj, long offset, long value)方法:设置obj对象中offset偏移的类型为long的field的值为value,支持volatile语义。
  • void putorderedlong(object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putlongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。
  • void park(boolean isabsolute, long time)方法:阻塞当前线程,其中参数isabsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。如果isabsolute等于true,并且time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unpark方法并且把当前线程作为参数时当前线程也会返回。
  • void unpark(object thread)方法:唤醒调用park后阻塞的线程。

下面是jdk8新增的函数,这里只列出long类型操作。

  • long getandsetlong(object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。

    //这个方法只是封装了compareandswaplong的使用,不需要自己写重试机制
    public final long getandsetlong(object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getlongvolatile(var1, var2);
        } while(!this.compareandswaplong(var1, var2, var6, var4));
    
        return var6;
    }
  • long getandaddlong(object obj, long offset, long addvalue)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量值为原始值+addvalue,原理和上面的方法类似。

cas使用场景

  • 使用一个变量统计网站的访问量;
  • atomic类操作;
  • 数据库乐观锁更新。

参考

《并发编程的基石——CAS机制.doc》

下载本文的Word格式文档,以方便收藏与打印。