关于java: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…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理