深入理解Unsafe类和CAS

简述

CAS(Compare And Swap)是对一种处理器指令的称呼,例如x86处理器中的cmpxchg指令,是一种在多线程环境下实现同步功能的机制。CASjava.util.concurrent的相关类中有非常广泛的应用,理解CAS是非常有必要的。

Unsafe类

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行底层、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用,使Java语言拥有了类似C语言指针一样操作内存空间的能力。
如下Unsafe源码所示,Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

public final class Unsafe {
    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();
    
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
}    

在这里插入图片描述
上面的分析可知我们是无法直接调用getUnsafe()方法的,但可以用万能的反射来获取Unsafe对象。

Java对象的布局

为了一会看懂打印出来出来的对象信息,先学一下对象的布局。
在这里插入图片描述
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分:

  • Mark Word:其是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特。
  • klass pointer: 这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。 如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩,而hotspot虚拟机是默认开启指针压缩的,即Klass pointer在64位机器上是32位。
  • 实例数据:就是类中定义的成员变量。
  • 对齐填充:对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

用法

下面先介绍Unsafe类提供的几个常用方法,这几个核心方法都是native方法,也就是说是用C/C++语言实现的

  • public native long objectFieldOffset(Field var1):返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该 Unsafe 函数中访问指定字段时使用。
  • public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x):这个即CAS操作,比较对象o中偏移量为offset的变量的值是否与expected相等,相等则用x更新,然后返回true,否则返回false。

为了打印出对象头的信息,引入第三方依赖

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>

示例代码:

public class Person {
    private int age;
    private String name;

  @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
public class UnsafeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        //Unsafe类中该变量是static的,直接传入null即可
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        //获取属性的offest
        long ageOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("age"));
        long nameOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("name"));

        System.out.println("ageOffset:"+ageOffset);
        System.out.println("nameOffset:"+nameOffset);

        Person person = new Person();
        System.out.println(ClassLayout.parseInstance(person).toPrintable());

        //执行cas操作
        unsafe.compareAndSwapInt(person,ageOffset,0,1);
        unsafe.compareAndSwapObject(person,nameOffset,null,"张三");
        System.out.println(person);
    }
}

在这里插入图片描述
从上图可以看到获取的ageOffset正好是属性age在内存中的offsetnameOffset同理。同时person对象的属性也被成功修改。

原理

先看一下unsafe.cpp的一些实现:

// 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);
  // 根据偏移量,计算 value 的地址。这里的 offset 就是 AtomaicInteger 中的 valueOffset
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 调用 Atomic 中的函数 cmpxchg,该函数声明于 Atomic.hpp 中
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

// atomic.cpp
unsigned Atomic::cmpxchg(unsigned int exchange_value,
                         volatile unsigned int* dest, unsigned int compare_value) {
  assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  
 //省略了一些代码
 
  return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
                                       (jint)compare_value);
}

下面为Atomic,cpp在Windows平台的具体实现:

// atomic_windows_x86.inline.hpp
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
              
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

上面的代码由 LOCK_IF_MP 预编译标识符和 cmpxchg 函数组成。为了看到更清楚一些,我们将 cmpxchg 函数中的 LOCK_IF_MP 替换为实际内容。如下:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  // 判断是否是多核 CPU
  int mp = os::is_MP();
  __asm {
    // 将参数值放入寄存器中
    mov edx, dest    // 注意: dest 是指针类型,这里是把内存地址存入 edx 寄存器中
    mov ecx, exchange_value
    mov eax, compare_value
    
    // LOCK_IF_MP
    cmp mp, 0
    /*
     * 如果 mp = 0,表明是线程运行在单核 CPU 环境下。此时 je 会跳转到 L0 标记处,
     * 也就是越过 _emit 0xF0 指令,直接执行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令
     * 前加 lock 前缀。lock指令保证了多核cpu之间数据的可见性,如果是单核cpu就没必要加lock了
     */
    je L0
    /*
     * 0xF0 是 lock 前缀的机器码,这里没有使用 lock,而是直接使用了机器码的形式。
     */ 
    _emit 0xF0
L0:
    /*
     * 比较并交换。简单解释一下下面这条指令,熟悉汇编的朋友可以略过下面的解释:
     *   cmpxchg: 即“比较并交换”指令
     *   dword: 全称是 double word,在 x86/x64 体系中,一个 
     *          word = 2 byte,dword = 4 byte = 32 bit
     *   ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元
     *   [edx]: [...] 表示一个内存单元,edx 是寄存器,dest 指针值存放在 edx 中。
     *          那么 [edx] 表示内存地址为 dest 的内存单元
     *          
     * 这一条指令的意思就是,将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值
     * 进行对比,如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中。
     */
    cmpxchg dword ptr [edx], ecx
  }
}

其实核心代码就是一条带lock 前缀的 cmpxchg 指令,即lock cmpxchg dword ptr [edx], ecx

Atomic类

Java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个类是基于CAS实现的能够保障对共享变量进行read-modify-write更新操作的原子性和可见性的一组工具包。Atomic包里的类基本都是使用Unsafe实现的包装类,其性能相比使用锁来实现原子性提升了许多。关于volatile可以看这篇文章万字长文深入剖析volatile(Java)

read-modify-write更新操作:指对共享变量的更新不是一个简单的赋值操作,而是变量的新值依赖于变量的旧值,比如count++

AtomicInteger 源码分析

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 1.获取Unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            //2.获取变量的offset
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //3.要操作的数据
    private volatile int value;
}

因为AtomicInteger类是在rt.jar包下面,通过Bootstarp ClassLoader加载,所以代码1可以直接通过Unsafe.getUnsafe()方法获取Unsafe类的实例。属性value要声明为volatile,以保证在多线程环境下变量的可见性。

 //AtomicInteger.java
 
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

incrementAndGet()这个方法会在原来value值的基础上加1,返回值为递增后的值,内部调用了Unsafe的getAndAddInt方法来实现操作,第一个参数是AtomicInteger实例的引用,第二个参数是value变量在AtomicInteger的偏移值,第三个值是要加的数字。

//Unsafe.java

   public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }


  public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);

在这里插入图片描述

getAndAddInt方法内部先通过getIntVolatile(o, offset)获得期望值A,然后比较期望值A是否与此刻内存中的值V相等,如果相等,则将原来的值A更新为B,如果不相等,则再进行循环,直到期望值与内存中的值相等。

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马走日mazouri

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值