Java中的魔法类Unsafe介绍

一、Unsafe类简介

Unsafe是位于sun.misc包下的一个类,不属于 Java 标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如 Netty 、
Hadoop 、 Kafka 等;Unsafe可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、CAS 、线程同步、内存屏障。

CAS 就是 Unsafe 类中提供的一个原子操作,第一个参数为需要改变的对象,第二个为偏移量 即之前求出来的 headOffset 的值 ,第三个参数为期待的值,第
四个为更新后的值 整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5 ,如果更新成功,则返回 true ,否则返回 false。

由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

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

二、如何获取Unsafe实例?

1、从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a 把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被
引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。

java Xbootclasspath/a:${path} // 其中path为调用Unsafe相关方法的类所在jar包路径

2、通过反射获取单例对象theUnsafe。 

public class UnsafeInstance {

    public static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

三、Unsafe功能介绍

Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍。 

Java中的魔法类Unsafe介绍_第1张图片

 1、内存操作

这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。

  • public native long allocateMemory(long bytes);//分配内存, 相当于C++的malloc函数
  • 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);//内存拷贝
  • public native Object getObject(Object o, long offset);//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
  • public native void putObject(Object o, long offset, Object x);//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有:putInt,putDouble,putLong,putChar等
  • public native byte getByte(long address);
  • public native void putByte(long address, byte x);//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配 时,此方法结果才是确定的)

通常,我们在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被垃圾回收时,分配的堆外内存一起被释放。

Java中的魔法类Unsafe介绍_第2张图片

 2、CAS相关

如下源代码释义所示,这部分主要为CAS相关操作的方法。

/**
 *  CAS
 *
@param o         包含要修改field的对象
 *
@param offset    对象中某field的偏移量
 *
@param expected  期望值
 *
@param update    更新值
 *
@return          true | false
 */

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,对象有一个起始地址即基地址,对象中的字段可以通过起始地址+字段相对于起始地址的偏移量定位到。

CAS操作的典型应用:如下图所示,AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作。

Java中的魔法类Unsafe介绍_第3张图片

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

 Java中的魔法类Unsafe介绍_第4张图片

 3、线程调度

包括线程挂起、恢复、锁机制等方法。

  • public native void unpark(Object thread);//取消阻塞线程
  • public native void park(boolean isAbsolute, long time);//阻塞线程
  • public native void monitorEnter(Object o);//获得对象锁(可重入锁)
  • public native void monitorExit(Object o);//释放对象锁
  • public native boolean tryMonitorEnter(Object o);//尝试获取对象锁

方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常。

unpark函数为线程提供许可 ( permit),线程调用park函数则等待许可 。这个有点像信号量,但是这个许可是不能叠加的,许可是一次性的。permit相当于 0/1 的开关,默认是0 ,调用一次 unpark 就加1变成了1。调用一次park会消费permit,又会变成0。 如果再调用一次park会阻塞,因为permit已经是0了。直到permit变成变成1。每个线程都有一个相关的permit,permit最多只有一个,重复调用最多只有一个,重复调用unpark不会累积。

典型应用:Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。

park,unpark方法使用示例如下:

public class UnsafeParkTest {
    public static void main(String[] args) throws InterruptedException {
        Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();

        Thread t=new Thread(()->{
            System.out.println("线程被阻塞=========");
            unsafe.park(false,0L);
            System.out.println("线程被唤醒继续执行=========");
            System.out.println("线程又一次被阻塞=========");
            unsafe.park(false,0L);
            System.out.println("线程又一次被唤醒继续执行=========");
        });
        t.start();
        Thread.sleep(1000L);
        unsafe.unpark(t);
        Thread.sleep(1000L);
        unsafe.unpark(t);
        System.out.println("多次调用unpark不会叠加,也只会有一个permit");
    }
}

控制台输出:

线程被阻塞=========
线程被唤醒继续执行=========
线程又一次被阻塞=========
多次调用unpark不会叠加,也只会有一个permit
线程又一次被唤醒继续执行=========

4、内存屏障

内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序,前面介绍volatile关键字防止指令重排的时候介绍过。 

  • public native void loadFence();//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前

  • public native void storeFence();//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前

  • public native void fullFence();//内存屏障,禁止load、store操作重排序

注意:load为读操作,store为写操作

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

Java中的魔法类Unsafe介绍_第5张图片

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

下图为StampedLock.validate方法的源码实现,通过锁标记与相关常量进行位运算、比较来校验锁状态,在校验逻辑之前,会通过Unsafe的loadFence方法加入一个load内存屏障,目的是避免上图用例中步骤②和StampedLock.validate中锁状态校验运算发生重排序导致锁状态校验不准确的问题。

public boolean validate(long stamp) {
    U.loadFence();
    return (stamp & SBITS) == (state & SBITS);
}

你可能感兴趣的:(多线程,Unsafe使用介绍,Java多线程并发,CAS底层实现原理)