关于多线程:Java魔法类Unsafe应用解析

5次阅读

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

Java 魔法类:Unsafe 利用解析

Unsafe 是位于 sun.misc 包下的一个类,次要提供一些用于执行低级别、不平安操作的办法,如间接拜访零碎内存资源、自主治理内存资源等,这些办法在晋升 Java 运行效率、加强 Java 语言底层资源操作能力方面起到了很大的作用。但因为 Unsafe 类使 Java 语言领有了相似 C 语言指针一样操作内存空间的能力,这无疑也减少了程序产生相干指针问题的危险。在程序中适度、不正确应用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种平安的语言变得不再“平安”,因而对 Unsafe 的应用肯定要谨慎

注:本文对 sun.misc.Unsafe 公共 API 性能及相干利用场景进行介绍。根本介绍如下 Unsafe 源码所示,Unsafe 类为一单例实现,提供静态方法 getUnsafe 获取 Unsafe 实例,当且仅当调用 getUnsafe 办法的类为疏导类加载器所加载时才非法,否则抛出 SecurityException 异样。

一:根本介绍

如下 Unsafe 源码所示,Unsafe 类为一单例实现,提供 静态方法 getUnsafe 获取 Unsafe 实例,当且仅当 调用 getUnsafe 办法的类 疏导类加载器 所加载时才非法,否则抛出 SecurityException 异样。

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;}
  }
}

那如若想应用这个类,该如何获取其实例?有如下两个可行计划。

  • 其一,从 getUnsafe 办法的应用限度条件登程,通过 Java 命令行命令 -Xbootclasspath/a 把调用 Unsafe 相干办法的类 A 所在 jar 包门路追加到默认的 bootstrap 门路中,使得 A 被疏导类加载器加载,从而通过 Unsafe.getUnsafe 办法平安的获取 Unsafe 实例。

    java -Xbootclasspath/a: ${path}   // 其中 path 为调用 Unsafe 相干办法的类所在 jar 包门路
  • 其二,通过反射获取单例对象theUnsafe

    private static Unsafe reflectGetUnsafe() {
      try {
        // Unsafe 类有个成员变量:private static final Unsafe theUnsafe
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
      } catch (Exception e) {log.error(e.getMessage(), e);
        return null;
      }
    }

    或者:先获取 Class 对象,再通过 结构器 创立Unsafe 对象

    // 通过反射取得 Unsafe 对象
    Class unsafeClass = Unsafe.class;
    // 取得构造函数,Unsafe 的构造函数为公有的
    Constructor constructor = unsafeClass.getDeclaredConstructor();
    // 设置为容许拜访公有内容
    constructor.setAccessible(true);
    // 创立 Unsafe 对象
    Unsafe unsafe = (Unsafe) constructor.newInstance();

二:性能介绍

如上图所示,Unsafe 提供的 API 大抵可分为 内存操作 CASClass 相干 对象操作 线程调度 零碎信息获取 内存屏障 数组操作 等几类,上面将对其相干办法和利用场景进行具体介绍。

1、内存操作

这部分次要蕴含 堆外内存的调配 拷贝 开释 给定地址值操作 等办法。

// 分配内存, 相当于 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);

通常,咱们在 Java 中创立的对象都处于堆内内存(heap)中,堆内内存 由 JVM 所管控的 Java 过程内存 ,并且它们遵循 JVM 的内存管理机制,JVM 会采纳垃圾回收机制对立治理堆内存。与之绝对的是堆外内存,存在于 JVM 管控之外的内存区域,Java 中对堆外内存的操作, 依赖于 Unsafe 提供的操作堆外内存的 native 办法

应用堆外内存的起因
  • 对垃圾回收进展的改善。因为 堆外内存是间接受操作系统治理而不是 JVM,所以当咱们应用堆外内存时,即可放弃较小的堆内内存规模。从而在 GC 时缩小回收进展对于利用的影响。
  • 晋升程序 I / O 操作的性能。通常在 I / O 通信过程中,会存在 堆内内存 堆外内存 的数据拷贝操作,对于须要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都倡议存储到堆外内存。

    典型利用

    DirectByteBuffer是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做 缓冲池 ,如在 Netty、MINA 等 NIO 框架中利用宽泛。DirectByteBuffer 对于堆外内存的创立、应用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。
    下图为 DirectByteBuffer 构造函数,创立 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,调配的堆外内存一起被开释。

那么如何通过构建垃圾回收追踪对象 Cleaner 实现堆外内存开释呢?
Cleaner继承自 Java 四大援用类型之一的 虚援用 PhantomReference(家喻户晓,无奈通过虚援用获取与之关联的对象实例,且当对象仅被虚援用援用时,在任何产生 GC 的时候,其均可被回收),通常 PhantomReference 与援用队列 ReferenceQueue 联合应用,能够实现虚援用关联对象被垃圾回收时可能进行零碎告诉、资源清理等性能。如下图所示,当某个被 Cleaner 援用的对象将被回收时,JVM 垃圾收集器会将此对象的援用放入到对象援用中的 pending 链表中,期待 Reference-Handler 进行相干解决。其中,Reference-Handler为一个领有最高优先级的守护线程,会循环不断的解决 pending 链表中的对象援用,执行 Cleaner 的 clean 办法进行相干清理工作。

所以当 DirectByteBuffe r 仅被Cleaner 援用(即为 虚援用 )时,其能够在任意 GC 时段被回收。当DirectByteBuffer 实例对象被回收时,在 Reference-Handler 线程操作中,会调用 Cleanerclean办法依据创立 Cleaner 时传入的 Deallocator 来进行堆外内存的开释。

2、CAS 相干

如下源代码释义所示,这部分次要为 CAS 相干操作的办法。

/**
    *  CAS
  * @param o         蕴含要批改 field 的对象
  * @param offset    对象中某 field 的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
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? 即比拟并替换,实现并发算法时罕用到的一种技术。CAS 操作蕴含三个操作数——内存地位 预期原值 新值 。执行CAS 操作的时候,将内存地位的值与预期原值比拟,如果相匹配,那么处理器会主动将该地位值更新为新值,否则,处理器不做任何操作。咱们都晓得,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不统一问题,Unsafe 提供的 CAS 办法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg

典型利用

CASjava.util.concurrent.atomic 相干类Java AQSCurrentHashMap 等实现上有十分宽泛的利用。如下图所示,AtomicInteger的实现中,动态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在动态代码块中通过 Unsafe 的 objectFieldOffset 办法获取。在 AtomicInteger 中提供的线程平安办法中,通过字段 valueOffset 的值能够定位到 AtomicInteger 对象中 value 的内存地址,从而能够依据 CAS 实现对 value 字段的原子操作。

下图为某个 AtomicInteger 对象自增操作前后的内存示意图,对象的基地址 baseAddress=“0x110000”,通过 baseAddress+valueOffset 失去 value 的内存地址 valueAddress=“0x11000c”;而后通过 CAS 进行原子性的更新操作,胜利则返回,否则持续重试,直到更新胜利为止。

3、线程调度

这部分,包含线程挂起、复原、锁机制等办法。

// 勾销阻塞线程
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形式来实现。

4、Class 相干

此局部次要提供 Class它的动态字段 的操作相干办法,蕴含 动态字段内存定位 定义类 定义匿名类 测验 & 确保初始化 等。

// 获取给定动态字段的内存地址偏移量,这个值对于给定的字段是惟一且固定不变的
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);
典型利用

从 Java 8 开始,JDK 应用 invokedynamicVM Anonymous Class联合来实现 Java 语言层面上的Lambda 表达式

  • invokedynamic: invokedynamic是 Java 7 为了实现在 JVM 上运行动静语言而引入的一条新的虚拟机指令,它能够实现在运行期动静解析出调用点限定符所援用的办法,而后再执行该办法,invokedynamic 指令的分派逻辑是由用户设定的疏导办法决定。
  • VM Anonymous Class:能够看做是一种模板机制,针对于程序动静生成很多构造雷同、仅若干常量不同的类时,能够先创立蕴含常量占位符的模板类,而后通过 Unsafe.defineAnonymousClass 办法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何 ClassLoader 上面,只有当该类没有存在的实例对象、且没有强援用来援用该类的 Class 对象时,该类就会被 GC 回收。故而 VM Anonymous Class 相比于 Java 语言层面的匿名外部类无需通过 ClassClassLoader 进行类加载且更易回收。
    Lambda 表达式 实现中,通过 invokedynamic 指令调用疏导办法生成调用点,在此过程中,会通过 ASM 动静生成字节码,而后利用 UnsafedefineAnonymousClass办法定义实现相应的函数式接口的匿名类,而后再实例化此匿名类,并返回与此匿名类中函数式办法的办法句柄关联的调用点;而后能够通过此调用点实现调用相应 Lambda 表达式 定义逻辑的性能。上面以如下图所示的 Test 类来举例说明。

    Test 类编译后的 class 文件反编译后的后果如下图一所示(删除了对本文阐明无意义的局部),咱们能够从中看到 main 办法的指令实现、invokedynamic指令调用的疏导办法 BootstrapMethods、及 静态方法 lambda$main$0(实现了 Lambda 表达式中字符串打印逻辑)等。在疏导办法执行过程中,会通过 Unsafe.defineAnonymousClass 生成如下图二所示的实现 Consumer 接口 的匿名类。其中,accept 办法 通过调用 Test 类中的静态方法 lambda$main$0 来实现 Lambda 表达式 中定义的逻辑。而后执行语句 consumer.accept("lambda") 其实就是调用下图二所示的匿名类的 accept 办法。

    5、对象操作

    此局部次要蕴含对象成员属性相干操作及非常规的对象实例化形式等相干办法。

    // 返回对象成员属性在内存地址绝对于此对象的内存地址的偏移量
    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.invoke、Objenesis(提供绕过类结构器的对象生成形式)、Gson(反序列化时用到)中都有相应的利用。

如下图所示,在 Gson 反序列化时,如果类有默认构造函数,则通过反射调用默认构造函数创立实例,否则通过 UnsafeAllocator 来实现对象实例的结构,UnsafeAllocator通过调用 UnsafeallocateInstance实现对象的实例化,保障在指标类无默认构造函数时,反序列化不够影响。

6、数组相干

这部分次要介绍与数据操作相干的 arrayBaseOffsetarrayIndexScale这两个办法,两者配合起来应用,即可定位数组中每个元素在内存中的地位。

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

这两个与数据操作相干的办法,在 java.util.concurrent.atomic 包下的 AtomicIntegerArray(能够实现对 Integer 数组中每个元素的原子性操作)中有典型的利用,如下图AtomicIntegerArray 源码所示,通过 UnsafearrayBaseOffsetarrayIndexScale别离获取 数组首元素的偏移地址 base 单个元素大小因子 scale。后续相干原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的 getAndAdd 办法 即通过 checkedByteOffset 办法 获取某数组元素的偏移地址,而后通过 CAS 实现原子性操作。

7、内存屏障

在 Java 8 中引入,用于定义 内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是 CPU 或编译器在对内存随机拜访的操作中的一个同步点,使得此点之前的所有读写操作都执行后才能够开始执行此点之后的操作),防止代码重排序。

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

在 Java 8 中引入了一种锁的新机制——StampedLock,它能够看成是 读写锁的一个改良版本 StampedLock 提供了一种乐观读锁的实现,这种乐观读锁相似于无锁的操作,齐全不会阻塞写线程获取写锁,从而 缓解读多写少时写线程“饥饿”景象。因为 StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不统一问题,所以当应用 StampedLock 的乐观读锁时,须要听从如下图用例中应用的模式来确保数据的一致性。

如上图用例所示计算坐标点 Point 对象,蕴含点挪动办法 move 及计算此点到原点的间隔的办法 distanceFromOrigin。在办法 distanceFromOrigin 中,首先,通过 tryOptimisticRead 办法获取乐观读标记;而后从主内存中加载点的坐标值 (x,y);而后通过 StampedLock 的 validate 办法校验锁状态,判断坐标点(x,y) 从主内存加载到线程工作内存过程中,主内存的值是否已被其余线程通过 move 办法批改,如果 validate 返回值为 true,证实 (x, y) 的值未被批改,可参加后续计算;否则,需加乐观读锁,再次从主内存加载 (x,y) 的最新值,而后再进行间隔计算。其中,校验锁状态这步操作至关重要,须要判断锁状态是否产生扭转,从而判断之前 copy 到线程工作内存中的值是否与主内存的值存在不统一。

下图为 StampedLock.validate 办法的源码实现,通过 锁标记 与相干常量进行 位运算 、比拟来校验锁状态,在校验逻辑之前,会通过UnsafeloadFence办法退出一个 load 内存屏障,目标是防止上图用例中步骤StampedLock.validate中锁状态校验运算产生重排序导致锁状态校验不精确的问题。

8、零碎相干

这部分蕴含两个获取零碎相干信息的办法。

// 返回零碎指针的大小。返回值为 4(32 位零碎)或 8(64 位零碎)。public native int addressSize();  
// 内存页的大小,此值为 2 的幂次方。public native int pageSize();
典型利用

如下图所示的代码片段,为 java.nio 下的工具类 Bits 中计算待申请内存所需内存页数量的静态方法,其依赖于 UnsafepageSize 办法 获取零碎内存页大小实现后续计算逻辑。

三:结语

本文对 Java 中的 sun.misc.Unsafe 的用法及利用场景进行了根本介绍,咱们能够看到 Unsafe 提供了很多便捷、乏味的 API 办法。即便如此,因为 Unsafe 中蕴含大量自主操作内存的办法,如若使用不当,会对程序带来许多不可控的劫难。因而对它的应用咱们须要慎之又慎。

正文完
 0