Unsafe介绍及CAS原理解析

0.Unsafe介绍

JavaDoc说, Unsafe提供了一组用于执行底层的,不安全操作的方法。那么具体有哪些方法呢,我画了一张图。

可以看到Unsafe中提供了CAS,内存操作,线程调度,本机信息,Class相关方法,查看和设置某个对象或字段,内存分配和释放相关操作,内存地址获取相关方法。我自己抽空对上述方法进行了注释,
你可以在这里看到
那么如何使用Unsafe呢?下面我们就来说说如何获取Unsafe并操作。

1.获取Unsafe实例

如下所述,由于Unsafe.getUnsafe会判断调用类的类加载器是否为引导类加载器,如果是,可以正常获取Unsafe实例,否则会抛出安全异常。

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

主要有两种方式来绕过安全检查,一种是通过将使用Unsafe的类交给bootstrap class loader去加载,另一种方式是通过反射。

1.1 通过bootstrap class loader去加载Unsafe。

public class GetUnsafeFromMethod {
    public static void main(String[] args){
        //调用这个方法,必须要在启动类加载器中获取,否则会抛出安全异常
        Unsafe unsafe = Unsafe.getUnsafe();
        System.out.printf("addressSize=%d, pageSize=%d\n", unsafe.addressSize(), unsafe.pageSize());
    }
}

上面代码,直接执行会报安全异常SecurityException,原因是当前caller的类加载器是应用类加载器(Application Class loader),而要求的是启动类加载器,
因而!VM.isSystemDomainLoader(var0.getClassLoader()) 返回false,抛出异常。

但是通过下面的命令行,我们把GetUnsafeFromMethod.java 追加到bootclasspath(启动类加载路径)上,就可以正常执行了。

javac -source 1.8 -encoding UTF8 -bootclasspath "%JAVA_HOME%/jre/lib/rt.jar;." GetUnsafeFromMethod.java
java -Xbootclasspath:"%JAVA_HOME%/jre/lib/rt.jar;." GetUnsafeFromMethod

你也看到了这样做有点费事了,难不成每次启动都要加这么一大串指令,所以下面我们就来反射是不是好用些。

1.2 通过反射获取Unsafe

import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class GetUnsafeFromReflect {
    public static void main(String[] args){
        Unsafe unsafe = getUnsafe();
        System.out.printf("addressSize=%d, pageSize=%d\n", unsafe.addressSize(), unsafe.pageSize());
    }

    public static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return unsafe;
    }
}

嗯,通过反射就可以直接用了,是不是比上一种利用启动类加载器加载的方式好用很多

3.Unsafe API 的使用

具体如何使用,可以查看这篇文章
实际的应用案例,可以查看美团的一篇文章

4. Unsafe 中CAS部分的实现

我们可以看到Unsafe中基本都是调用native方法,如果你比较好奇这个native方法又是如何实现的,那么就需要去JVM里面找对应的实现。

http://hg.openjdk.java.net/ 进行一步步选择下载对应的hotspot版本,我这里下载的是http://hg.openjdk.java.net/jdk8u/jdk8u60/hotspot/archive/tip.tar.gz

然后解hotspot目录,发现 \src\share\vm\prims\unsafe.cpp,这个就是对应jvm相关的c++实现类了。

比如我们对CAS部分的实现很感兴趣,就可以在该文件中搜索compareAndSwapInt,此时可以看到对应的JNI方法为Unsafe_CompareAndSwapInt

// These are the methods prior to the JSR 166 changes in 1.6.0
static JNINativeMethod methods_15[] = {
     ...
    {CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z",  FN_PTR(Unsafe_CompareAndSwapObject)},
    {CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},
    {CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z",      FN_PTR(Unsafe_CompareAndSwapLong)}
     ...
};

接着我们在搜索Unsafe_CompareAndSwapInt的实现,

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; //执行Atomic类中的cmpxchg。
UNSAFE_END

可以看到最后会调用到Atomic::cmpxchg里面的函数,这个根据不同操作系统和不同CPU会有不同的实现,但都放在hotspot\src\os_cpu 目录下,比如linux_64x的,对应类就是hotspot\src\os_cpu\linux_x86\vm\atomic_linux_x86.inline.hpp, 而windows_64x的,对应类就是hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp。(此处也说明了为什么Java可以Write once, Run everywhere, 原因就是JVM源码对不同操作系统和不同CPU有不同的实现)

这里我们以linux_64x的为例,查看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;
}

0.os::is_MP
os::is_MP()hotspot\src\share\vm\runtime\os.hpp中,如下:

// Interface for detecting multiprocessor system
  static inline bool is_MP() {
    // During bootstrap if _processor_count is not yet initialized
    // we claim to be MP as that is safest. If any platform has a
    // stub generator that might be triggered in this phase and for
    // which being declared MP when in fact not, is a problem - then
    // the bootstrap routine for the stub generator needs to check
    // the processor count directly and leave the bootstrap routine
    // in place until called after initialization has ocurred.
    return (_processor_count != 1) || AssumeMP;
  }

1.__asm__: 表示接下来是内联的汇编代码,这里使用asm语句可以将汇编指令直接包含在C代码中,主要是为了极致的性能。

2.volatile: 表示去掉优化

3.LOCK_IF_MP 是一个宏定义,即:#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
替换文本里面也是汇编代码,LOCK_IF_MP根据当前操作系统是否为多核处理器,来决定是否为cmpxchg指令添加lock前缀。如果有lock前缀的话,则会根据CPU不同会采用锁总线或者锁cache line的方式,来实现缓存一致性。

4.cmpxchgl部分的解释

"cmpxchgl %1,(%3)"  
                    : "=a" (exchange_value) 
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory"

cmpxchgl的详细执行过程:
首先,输入是"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp),表示compare_value存入eax寄存器,而exchange_value、dest、mp的值存入任意的通用寄存器。嵌入式汇编规定把输出和输入寄存器按统一顺序编号,顺序是从输出寄存器序列从左到右从上到下以“%0”开始,分别记为%0、%1···%9。也就是说,输出的eax是%0,输入的exchange_value、compare_value、dest、mp分别是%1、%2、%3、%4。

因此,cmpxchgl %1,(%3)实际上表示cmpxchgl exchange_value,(dest),此处(dest)表示dest地址所存的值。需要注意的是cmpxchgl有个隐含操作数eax,其实际过程是先比较eax的值(也就是compare_value)和dest地址所存的值是否相等,如果相等则把exchange_value的值写入dest指向的地址。如果不相等则把dest地址所存的值存入eax中。
输出是"=a" (exchange_value),表示把eax中存的值写入exchange_value变量中。

Atomic::cmpxchg这个函数最终返回值是exchange_value,也就是说,如果cmpxchgl执行时compare_value和dest指针指向内存值相等则会使得dest指针指向内存值变成exchange_value,最终eax存的compare_value赋值给了exchange_value变量,即函数最终返回的值是原先的compare_value。此时Unsafe_CompareAndSwapInt的返回值(jint)(Atomic::cmpxchg(x, addr, e)) == e就是true,表明CAS成功。如果cmpxchgl执行时compare_value和(dest)不等则会把当前dest指针指向内存的值写入eax,最终输出时赋值给exchange_value变量作为返回值,导致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到false,表明CAS失败。

假设原值为old,存在ptr所执行的位置,想写入新值new,那么cmpxchg实现的功能就是比较old和ptr指向的内容,如果相等则ptr所指地址写入new,然后返回old,如果不相等则把ptr当前所指向地址存的值返回。(上面的没看懂没关系,记住这个结论就行了,或者你可以选择看看第5部分)

5.比较并交换 Compare-and-swap

第4部分,我们从Java一直探究到机器指令cmpxchgl尝试来搞懂Java的CAS是如何实现的。由于已经是机器指令了,所以任何一门编程语言都可能使用它,所以我们在从计算机科学的角度来看看比较并交换,从而做到举一反三。下面的内容主要来自维基百科。

比较并交换(compare and swap,
CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。
该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

一个CAS操作的过程可以用以下c代码表示:

int cas(long *addr, long old, long new)
{
    /* Executes atomically. */
    if(*addr != old)
        return 0;
    *addr = new;
    return 1;
}
在使用上,通常会记录下某块内存中的旧值,通过对旧值进行一系列的操作后得到新值,然后通过CAS操作将新值与旧值进行交换。如果这块内存的值在这期间内没被修改过,则旧值会与内存中的数据相同,这时CAS操作将会成功执行使内存中的数据变为新值。如果内存中的值在这期间内被修改过,则一般来说旧值会与内存中的数据不同,这时CAS操作将会失败,新值将不会被写入内存。

6.总结:

Java里的Unsafe.java的compareAndSwapInt方法,再到C++下的unsafe.cpp的Unsafe_CompareAndSwapInt, 再到CPU指令 lock cmpxchgl, 可以看到编程语言从Java,变到C/C++, 再到CPU指令,这真是一次奇妙的旅程。

一方面,Java帮程我们层层封装,不用再去担心底层的区别,不用再去担心如何维护内存,如何使用指针等等,你只需要好好地实现上层的应用。

另一方面,天下没有免费的午餐,出来混早晚都要还的,既然底层是这些语言实现的,当别人问到你时,你要么说不会,要么就得一层层看下去,最起码是要理解最关键的部分。

当然,从CPU指令到汇编,到C/C++, 再到Java,一层层抽象本意就是让编程语言越来越好用,但为了要走的更远,你就得记得自己从哪里来。

另外比较并交换(compare and set CAS)还会遇到ABA问题,我们放到下一节讲Java原子类的时候一并说明。

7.参考:

https://tech.meituan.com/2019...
http://mishadoff.com/blog/jav...
https://en.cppreference.com/w...
https://blog.csdn.net/prstaxy...
https://en.wikipedia.org/wiki...
https://zh.wikipedia.org/wiki...

你可能感兴趣的:(java,cas,面试)