CAS和Unsafe类

162 阅读19分钟

简单介绍

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

乐观锁和悲观锁

什么是乐观锁,悲观锁?

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。

对于同一个数据的并发操作:

  • 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
  • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。 如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

乐观锁,悲观锁的实现

  • 乐观锁的实现

    • CAS算法

      Java原子类中的递增操作就是通过CAS自旋实现的。

  • 悲观锁的实现

    • synchronized关键字的实现类
    • Lock的实现类

乐观锁和悲观锁的适用场景

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

CAS概述

什么是CAS?

什么是CAS

CASCompare And Swap的缩写,直译就是比较并交换

CAS是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令,这个指令会对内存中的共享数据做原子的读写操作。其作用是CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新。

  • CAS如何实现?

    CAS的实现方式是基于硬件平台的汇编指令,就是说CAS靠硬件实现的JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。

  • CAS是一种基于乐观锁的操作?

    使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。

    CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。

    CAS可以保证在多线程并发中保障共享资源的原子性操作,相较于synchronized来说是一种轻量级的实现方案。

  • 如果出现冲突了怎么办?

    无锁操作是使用**CAS(compare and swap)** 又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

Java如何调用CAS

Java中并没有直接实现CASCAS相关的实现是借助C/C++调用CPU指令来实现的,效率很高,但Java代码需通过JNI才能调用。

比如,Unsafe类提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg

CAS操作流程

CAS的操作过程

CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含的三个值分别为:

  • V 内存地址存放的实际值
  • O 预期的值(旧值)
  • N 更新的新值

image.png

VO相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V

反之,VO不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。

CAS问题

CAS解决并发问题相比于synchronized性能更优,但是也会引发以下问题

ABA问题

什么是ABA问题?

image.png

因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。

  • 如何解决ABA问题?

    ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A

    从Java 1.5开始,JDKAtomic包里提供了一个原子引用类AtomicStampedReference来解决ABA问题。

    AtomicStampedReferencecompareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大

什么是循环时间长开销大问题?

自旋CAS如果长时间不成功,会CPU带来非常大的执行开销。

如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。

  • pause指令有什么用?

    pause指令有两个作用:

    • 延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;
    • 避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。

只能保证一个共享变量的原子操作

什么是只能保证一个共享变量的原子操作问题?

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS无法保证操作的原子性,这个时候就得用到锁来保证原子操作了。

  • 还有一个取巧的办法

    把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2j = a,合并一下ij = 2a,然后用CAS来操作ij

    从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

CAS工作原理

CAS的实现原理简单来说就是由Unsafe类和其中的自旋锁来完成的。

什么是Unsafe类?

什么是Unsafe类?

Unsafe是位于sun.misc包下的一个类,Java原子类是通过Unsafe类实现的

Unsafe主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。

但由于**Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力**,这无疑也增加了程序发生相关指针问题的风险。正因为如此Unsafe才类如其名。

Unsafe类源码

Unsafe的单例实现

Unsafe类的单例实现:

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

Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

  • 如何获取Unsafe类的实例?

    • 方法一:加命令行参数然后调用单例实例获取方法

      getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载。

      从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。

    • 方法二:通过反射获取单例对象theUnsafe

       private static Unsafe reflectGetUnsafe() {
           try {
             Field field = Unsafe.class.getDeclaredField("theUnsafe");
             field.setAccessible(true);
             return (Unsafe) field.get(null);
           } catch (Exception e) {
             log.error(e.getMessage(), e);
             return null;
           }
       }
      

Unsafe的API

Unsafe的API?

这里我们直接借用了美团技术团队的思维导图:

image.png

可以知道Unsafe类的API按照功能分类如下:

  • 内存操作:

    • 主要包括

      • 分类、拷贝、扩充、释放堆外内存
      • 设置、获得给定地址中的值
    • 相关方法

       //分配内存, 相当于C++的malloc函数
       public native long allocateMemory(long bytes);
       //扩充内存
       public native long reallocateMemory(long address, long bytes);
       //释放内存
       public native void freeMemory(long address);
       //在给定的内存块中设置值
       public native void setMemory(Object o, long offset, long bytes, byte value);
       //内存拷贝
       public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
       //获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
       public native Object getObject(Object o, long offset);
       //为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
       public native void putObject(Object o, long offset, Object x);
       //获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
       public native byte getByte(long address);
       //为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
       public native void putByte(long address, byte x);
      
  • CAS

    • 相关方法

       public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);
       ​
       public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
         
       public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
      

      我们都知道,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg

      我们会在「Unsafe的CAS操作相关方法」和「compareAndSwapInt」两个章节补全CAS操作的实现相关内容。

    • 应用

      CASjava.util.concurrent.atomic相关类、Java AQSCurrentHashMap等实现上有非常广泛的应用。

  • Class相关

    • 主要包括

      • 动态创建类(普通类&匿名类)
      • 获取field的内存地址偏移量
      • 检测、确保类初始化
    • 相关方法

       //获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
       public native long staticFieldOffset(Field f);
       //获取一个静态类中给定字段的对象指针
       public native Object staticFieldBase(Field f);
       //判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
       public native boolean shouldBeInitialized(Class<?> c);
       //检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
       public native void ensureClassInitialized(Class<?> c);
       //定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
       public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
       //定义一个匿名类
       public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
      
    • 应用

      从JDK1.8开始,使用invokedynamicVM Anonymous Class结合来实现Java语言层面上的Lambda表达式。

  • 对象操作

    • 主要包括

      • 获取对象成员属性在内存偏移量
      • 非常规对象实例化
      • 存储、获取指定偏移量地址的变量值(包含延迟生效、volatile语义)
    • 相关方法

       //返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
       public native long objectFieldOffset(Field f);
       //获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等
       public native Object getObject(Object o, long offset);
       //给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等
       public native void putObject(Object o, long offset, Object x);
       //从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
       public native Object getObjectVolatile(Object o, long offset);
       //存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
       public native void putObjectVolatile(Object o, long offset, Object x);
       //有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效
       public native void putOrderedObject(Object o, long offset, Object x);
       //绕过构造方法、初始化代码来创建对象
       public native Object allocateInstance(Class<?> cls) throws InstantiationException;
      
    • 应用

      • 常规对象实例化方式

        我们通常所用到的创建对象的方式,从本质上来讲,都是通过new机制来实现对象的创建。

        但是,new机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。

      • 非常规的实例化方式

        Unsafe中提供allocateInstance方法,仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查等。

        它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。

        由于这种特性,allocateInstancejava.lang.invokeObjenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。

  • 数组相关

    • 主要包括

      • 返回数组元素内存大小
      • 返回数组首元素偏移地址
    • 相关方法

       //返回数组中第一个元素的偏移地址
       public native int arrayBaseOffset(Class<?> arrayClass);
       //返回数组中一个元素占用的大小
       public native int arrayIndexScale(Class<?> arrayClass);
      
    • 应用

      这两个与数据操作相关的方法,在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以实现对Integer数组中每个元素的原子性操作)中有典型的应用

  • 内存屏障

    • 主要包括

      • 禁止load、store重排序
    • 相关方法

       //内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
       public native void loadFence();
       //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
       public native void storeFence();
       //内存屏障,禁止load、store操作重排序
       public native void fullFence();
      
    • 应用

      在JDK1.8中引入了一种锁的新机制--StampedLock,可以看成是读写锁的一个改进版本。StampedLock提供了一种乐观读锁的实现。

      StampedLock.validate方法的源码实现中,通过锁标记与相关常量进行位运算、比较来校验锁状态,在校验逻辑之前,会通过UnsafeloadFence方法加入一个load内存屏障

  • 系统相关

    • 主要包括

      • 返回内存页大小
      • 返回系统指针大小
    • 相关方法

       //返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
       public native int addressSize();  
       //内存页的大小,此值为2的幂次方。
       public native int pageSize();
      
    • 应用

      java.nio下的工具类Bits中计算待申请内存所需内存页数量的静态方法,其依赖于UnsafepageSize方法获取系统内存页大小实现后续计算逻辑。

  • 线程调度

    • 主要包括

      • 线程挂起、恢复
      • 获取、释放锁
    • 相关方法

       //取消阻塞线程
       public native void unpark(Object thread);
       //阻塞线程
       public native void park(boolean isAbsolute, long time);
       //获得对象锁(可重入锁)
       @Deprecated
       public native void monitorEnter(Object o);
       //释放对象锁
       @Deprecated
       public native void monitorExit(Object o);
       //尝试获取对象锁
       @Deprecated
       public native boolean tryMonitorEnter(Object o);
      

      方法parkunpark即可实现线程的挂起与恢复

      将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;

      unpark可以终止一个挂起的线程,使其恢复正常。

    • 应用

      Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupportparkunpark方法实际是调用Unsafeparkunpark方式来实现。

Unsafe的CAS操作相关方法

我们又截取了部分Unsafe方法的源码,以下方法基本都是和「内存操作」相关的:

 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;
 }
 ​
 public final long getAndAddLong(Object var1, long var2, long var4) {
     long var6;
     do {
         var6 = this.getLongVolatile(var1, var2);
     } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
 ​
     return var6;
 }
 ​
 public final int getAndSetInt(Object var1, long var2, int var4) {
     int var5;
     do {
         var5 = this.getIntVolatile(var1, var2);
     } while(!this.compareAndSwapInt(var1, var2, var5, var4));
 ​
     return var5;
 }
 ​
 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;
 }
 ​
 public final Object getAndSetObject(Object var1, long var2, Object var4) {
     Object var5;
     do {
         var5 = this.getObjectVolatile(var1, var2);
     } while(!this.compareAndSwapObject(var1, var2, var5, var4));
 ​
     return var5;
 }

从源码发现,Unsafe内部使用自旋的方式进行CAS更新(do-while循环)

并且可以发现,Unsafe类只提供了3种CAS方法:

 public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);
 ​
 public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
 ​
 public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

并且这几个方法都是native的。但是既然要剖析Unsafe类,我们就继续把它对应的C++代码挖出来。

compareAndSwapInt

接上文我们知道compareAndSwapInt是一个native方法,这个CAS的实现在unsafe.cpp

 UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
   UnsafeWrapper("Unsafe_CompareAndSwapInt");
   oop p = JNIHandles::resolve(obj);
   jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
   return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
 UNSAFE_END

它通过 Atomic::cmpxchg 来实现比较和替换操作。(其中参数x是即将更新的值,参数e是原内存的值。)

  • Atomic::cmpxchg方法

    如果是Linux的x86,Atomic::cmpxchg方法的实现如下:

     inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
       int mp = os::is_MP();
       __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                         : "=a" (exchange_value)
                         : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                         : "cc", "memory");
       return exchange_value;
     }
    

    如果是windows的x86,Atomic::cmpxchg方法的实现如下:

     inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
         int mp = os::isMP(); //判断是否是多处理器
         _asm {
             mov edx, dest
             mov ecx, exchange_value
             mov eax, compare_value
             LOCK_IF_MP(mp)
             cmpxchg dword ptr [edx], ecx
         }
     }
     ​
     // Adding a lock prefix to an instruction on MP machine
     // VC++ doesn't like the lock prefix to be on a single line
     // so we can't insert a label after the lock prefix.
     // By emitting a lock prefix, we can define a label after it.
     #define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                            __asm je L0      \
                            __asm _emit 0xF0 \
                            __asm L0:
    
    • os::is_MP()作用

      mpos::is_MP的返回结果,os::is_MP()是一个内联函数,用来判断当前系统是否为多处理器。

      • 如果当前系统是多处理器,该函数返回1。
      • 否则,返回0。
    • LOCK_IF_MP(mp)作用

      • 如果是多处理器(mp==1),为cmpxchg指令添加lock前缀。
      • 反之,就省略lock前缀(单处理器会不需要lock前缀提供的内存屏障效果)。
    • lock前缀的作用?

      这里的lock前缀就是使用了处理器的总线锁(最新的处理器都使用缓存锁代替总线锁来提高性能)。

      总得来说,它能保证CAS是一个原子操作,且还拥有内存屏障的作用,保证了CAS同时有volatile读和volatile写的内存语义。

    • cmpxchg整体作用

      cmpxchg(void* ptr, int old, int new),如果ptrold的值一样,则把new写到ptr内存,否则返回ptr的值,整个操作是原子的。

      在Intel平台下,会用lock cmpxchg来实现,使用lock触发缓存锁,这样另一个线程想访问ptr的内存,就会被block住。

Unsafe在Atomic类中的应用

这里我们主要用AtomicInteger中对Unsafe调用实现的CAS操作源码来作为本章的内容:

AtomicInteger类的部分源码:

 public class AtomicInteger extends Number implements java.io.Serializable {
     private static final long serialVersionUID = 6214790243416807050L;
 ​
     // setup to use Unsafe.compareAndSwapInt for updates
     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;
     
     // ...
 }
  • Unsafe的应用

    AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过UnsafeobjectFieldOffset方法获取。

    AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作。

  • AtomicInteger自增操作

    image.png

    上图为AtomicInteger对象自增操作前后的内存示意图

    我们知道CAS需要知道3个参数:操作对象的内存地址,预期的值,要修改的值。

    于是我们首先要获取的就是对象的内存地址

    根据上面的图对象的基地址baseAddress=“0x110000”,通过baseAddress+valueOffset得到value的内存地址valueAddress=“0x11000c”

    然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功。这里预期值设置为1是成功的情况。

小结

本章我们从乐观锁和悲观锁的介绍出发,然后概述了CAS,接着指出Unsafe类是Java的CAS操作实现的底层。

通过分析Unsafe类的源码,我们理解了它是一个不可或缺的Java底层类,用于许多与内存地址,CAS等相关操作。

本篇参考: