关于java:Unsafe介绍及CAS原理解析

34次阅读

共计 6756 个字符,预计需要花费 17 分钟才能阅读完成。

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…

正文完
 0