写点什么

当面试官问出“Unsafe”类时,我就知道这场面试废了,祖坟都能给你问出来!

作者:EquatorCoco
  • 2024-05-25
    福建
  • 本文字数:7751 字

    阅读完需:约 25 分钟

一、写在开头


依稀记得多年以前的一场面试中,面试官从 Java 并发编程问到了锁,从锁问到了原子性,从原子性问到了 Atomic 类库(对着 JUC 包进行了刨根问底),从 Atomic 问到了 CAS 算法,紧接着又有追问到了底层的 Unsafe 类,当问到 Unsafe 类时,我就知道这场面试废了,这似乎把祖坟都能给问冒烟啊。


但时过境迁,现在再回想其那场面试,不再觉得面试官的追毛求疵,反而为那时候青涩菜鸡的自己感到羞愧,为什么这样说呢,实事求是的说 Unsafe 类虽然是比较底层,并且我们日常开发不可能用到的类,但是!翻开 JUC 包中的很多工具类,只要底层用到了 CAS 思想来提升并发性能的,几乎都脱离不了 Unsafe 类的运用,可惜那时候光知道被八股文了,没有做到细心总结与发现。


二、Unsafe 的基本介绍


我们知道 C 语言可以通过指针去操作内存空间,Java 不存在指针,为了提升 Java 运行效率、增强 Java 语言底层资源操作能力,便诞生了 Unsafe 类,Unsafe 是位于 sun.misc 包下。正如它的名字一样,这种操作底层的方式是不安全的,在程序中过度和不合理的使用,会带来未知的风险,因此,Unsafe 虽然,但要慎用哦!


2.1 如何创建一个 unsafe 实例


我们无法直接通过 new 的方式创建一个 unsafe 的实例,为什么呢?我们看它的这段源码便知:

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;    }  }}
复制代码


从源码中我们发现 Unsafe 类被 final 修饰,所以无法被继承,同时它的无参构造方法被 private 修饰,也无法通过 new 去直接实例化,不过在 Unsafe 类提供了一个静态方法 getUnsafe,看上去貌似可以用它来获取 Unsafe 实例。但是!当我们直接去调用这个方法的时候,会报如下错误:

Exception in thread "main" java.lang.SecurityException: Unsafe  at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)  at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)
复制代码


这是因为在 getUnsafe 方法中,会对调用者的 classLoader 进行检查,判断当前类是否由 Bootstrap classLoader 加载,如果不是的话就会抛出一个 SecurityException 异常。


那我们如果想使用 Unsafe 类,到底怎样才能获取它的实例呢?


在这里提供给大家两种方式:


方式一


假若在 A 类中调用 Unsafe 实例,则可通过 Java 命令行命令-Xbootclasspath/a 把调用 Unsafe 相关方法的类 A 所在 jar 包路径追加到默认的 bootstrap 路径中,使得 A 被启动类加载器加载,从而通过 Unsafe.getUnsafe 方法安全的获取 Unsafe 实例。

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


方式二


利用反射获得 Unsafe 类中已经实例化完成的单例对象:

public static Unsafe getUnsafe() throws IllegalAccessException {     Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");     //Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以这样,作用相同     unsafeField.setAccessible(true);     Unsafe unsafe =(Unsafe) unsafeField.get(null);     return unsafe; }
复制代码


2.2 Unsafe 的使用


上面我们已经知道了如何获取一个 unsafe 实例了,那现在就开始写一个小 demo 来感受一下它的使用吧。

public class TestService {    //通过单例获取实例    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");        //Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以这样,作用相同        unsafeField.setAccessible(true);        Unsafe unsafe =(Unsafe) unsafeField.get(null);        return unsafe;    }    //调用实例方法去赋值    public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {        Persion persion = new Persion();        persion.setAge(10);        System.out.println("ofigin_age:" + persion.getAge());        long fieldOffset = unsafe.objectFieldOffset(Persion.class.getDeclaredField("age"));        System.out.println("offset:"+fieldOffset);        unsafe.putInt(persion,fieldOffset,20);        System.out.println("new_age:"+unsafe.getInt(persion,fieldOffset));    }     public static void main(String[] args) {        TestService testService = new TestService();        try {            testService.fieldTest(getUnsafe());        } catch (NoSuchFieldException | IllegalAccessException e) {            e.printStackTrace();        }    }}class Persion{     private String name;    private int age;     public String getName() {        return name;    }     public void setName(String name) {        this.name = name;    }     public int getAge() {        return age;    }     public void setAge(int age) {        this.age = age;    }}
复制代码


输出:

ofigin_age:10offset:12new_age:20
复制代码


通过 Unsafe 类的 objectFieldOffset 方法获取到了对象中字段的偏移地址,这个偏移地址不是内存中的绝对地址而是一个相对地址,之后再通过这个偏移地址对 int 类型字段的属性值进行读写操作,通过结果也可以看到 Unsafe 的方法和类中的 get 方法获取到的值是相同的。


三、Unsafe 类的 8 种应用


基于 Unsafe 所提供的 API,我们大致可以将 Unsafe 根据应用场景分为如下的八类,上一个脑图。



3.1 内存操作


学习过 C 或者 C++的同学对于内存操作应该很熟悉了,在 Java 里我们是无法直接对内存进行操作的,我们创建的对象几乎都在堆内内存中存放,它的内存分配与管理都是 JVM 去实现,同时,在 Java 中还存在一个 JVM 管控之外的内存区域叫做“堆外内存”,Java 中对堆外内存的操作,依赖于 Unsafe 提供的操作堆外内存的 native 方法啦。


内存操作的常用方法:

/*包含堆外内存的分配、拷贝、释放、给定地址值操作*///分配内存, 相当于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);
复制代码


在这里我们不仅会想,为啥全是 native 方法呢?


  1. native 方法通过 JNI 调用了其他语言,如果 C++等提供的现车功能,可以让 Java 拿来即用;

  2. 需要用到 Java 中不具备的依赖于操作系统的特性,Java 在实现跨平台的同时要实现对底层的控制,需要借助其他语言发挥作用;

  3. 程序对时间敏感或对性能要求非常高时,有必要使用更加底层的语言,例如 C/C++甚至是汇编。


【经典应用】在 Netty、MINA 等 NIO 框架中我们常常会应到缓冲池,而实现缓冲池的一个重要类就是 DirectByteBuffer,它主要的作用对于堆外内存的创建、使用、销毁等工作。


通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存




从上图我们可以看到,在构建实例时,DirectByteBuffer 内部通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放。


3.2 内存屏障


为了充分利用缓存,提高程序的执行速度,编译器在底层执行的时候,会进行指令重排序的优化操作,但这种优化,在有些时候会带来 有序性 的问题。(在将 volatile 关键字的时候提到过了)


为了解决这一问题,Java 中引入了内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令),通过组织屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。


在 Unsafe 类中提供了 3 个 native 方法来实现内存屏障:

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


【经典应用】


在之前的文章中,我们讲过 Java8 中引入的一个高性能的读写锁:StampedLock(锁王),在这个锁中同时支持悲观读与乐观读,悲观读就和 ReentrantLock 一致,乐观读中就使用到了 unsafe 的 loadFence(),一起去看一下。

	/**     * 使用乐观读锁访问共享资源     * 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候					可能其他写线程已经修改了数据,     * 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。     *     * @return     */   double distanceFromOrigin() {     long stamp = sl.tryOptimisticRead(); // 获取乐观读锁     double currentX = x, currentY = y;	// 拷贝共享资源到本地方法栈中     if (!sl.validate(stamp)) { // //检查乐观读锁后是否有其他写锁发生,有则返回false        stamp = sl.readLock(); // 获取一个悲观读锁        try {          currentX = x;          currentY = y;        } finally {           sl.unlockRead(stamp); // 释放悲观读锁        }     }     return Math.sqrt(currentX * currentX + currentY * currentY);   }
复制代码


在官网给出的乐观读的使用案例中,我们看到 if 中做了一个根绝印章校验写锁发生的操作,我们跟入这个校验源码中:

public boolean validate(long stamp) {        U.loadFence();//load内存屏障        return (stamp & SBITS) == (state & SBITS);    }
复制代码


这一步的目的是防止锁状态校验运算发生重排序导致锁状态校验不准确的问题!


3.3 对象操作


其实在 2.2 Unsafe 的使用中,我们已经使用了 Unsafe 进行对象成员属性的内存偏移量获取,以及字段属性值的修改功能了,除了 Int 类型,Unsafe 还支持对所有 8 种基本数据类型以及 Object 的内存数据修改,这里就不再赘述了。


需要额外强掉的一点,在 Unsafe 的源码中还提供了一种非常规的方式进行对象的实例化:

//绕过构造方法、初始化代码来创建对象public native Object allocateInstance(Class<?> cls) throws InstantiationException;
复制代码


这种方法可以绕过构造方法和初始化代码块来创建对象,我们写一个小 demo 学习一下。

@Data public class A {     private int b;     public A(){         this.b =1;     } }
复制代码


定义一个类 A,我们分别采用无参构造器、newInstance()、Unsafe 方法进行实例化。

public void objTest() throws Exception{     A a1=new A();     System.out.println(a1.getB());     A a2 = A.class.newInstance();     System.out.println(a2.getB());     A a3= (A) unsafe.allocateInstance(A.class);     System.out.println(a3.getB()); }
复制代码


输出结果为 1,1,0。这说明调用 unsafe 的 allocateInstance 方法确实可以跳过构造器去实例化对象!


3.4 数组操作


在 Unsafe 中,可以使用 arrayBaseOffset 方法获取数组中第一个元素的偏移地址,使用 arrayIndexScale 方法可以获取数组中元素间的偏移地址增量,通过这两个方法可以定位数组中的每个元素在内存中的位置。

基于 2.2 Unsafe 使用的测试代码,我们增加如下的方法:

  //获取数组元素在内存中的偏移地址,以及偏移量    private void arrayTest(Unsafe unsafe) {        String[] array=new String[]{"aaa","bb","cc"};        int baseOffset = unsafe.arrayBaseOffset(String[].class);        System.out.println("数组第一个元素的偏移地址:" + baseOffset);        int scale = unsafe.arrayIndexScale(String[].class);        System.out.println("元素偏移量" + scale);         for (int i = 0; i < array.length; i++) {            int offset=baseOffset+scale*i;            System.out.println(offset+" : "+unsafe.getObject(array,offset));        }    }
复制代码


输出:

数组第一个元素的偏移地址:16元素偏移量416 : aaa20 : bb24 : cc
复制代码


3.5 CAS 相关


终于,重点来了,我们写这篇文章的初衷是什么?是回想起曾经面时,面试官由原子类库(Atomic)问到了 CAS 算法,从而追问到了 Unsafe 类上,在 JUC 包中到处都可以看到 CAS 的身影,在 java.util.concurrent.atomic 相关类、Java AQS、CurrentHashMap 等等类中均有!


以 AtomicInteger 为例,在内部提供了一个方法为 compareAndSet(int expect, int update) ,如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update),而它的底层调用则是 unsafe 的 compareAndSwapInt()方法。

public final boolean compareAndSet(int expect, int update) {        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);    }
复制代码


CAS 思想的底层实现其实就是 Unsafe 类中的几个 native 本地方法:

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);
复制代码


3.6 线程调度


Unsafe 类中提供了 park、unpark、monitorEnter、monitorExit、tryMonitorEnter 方法进行线程调度,在前面介绍 AQS 的文章中我们学过,在 AQS 中通过调用 LockSupport.park()和 LockSupport.unpark()实现线程的阻塞和唤醒的,而 LockSupport 的 park、unpark 方法实际是调用 Unsafe 的 park、unpark 方式来实现。

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


LockSupport源码:

public static void park(Object blocker) {     Thread t = Thread.currentThread();     setBlocker(t, blocker);     UNSAFE.park(false, 0L);     setBlocker(t, null); } public static void unpark(Thread thread) {     if (thread != null)         UNSAFE.unpark(thread); }
复制代码


3.7 Class 操作


Unsafe 对 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);
复制代码


【测试案例】

@Data public class User {     public static String name="javabuild";     int age; } private void staticTest() throws Exception {     User user=new User();     //判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用     System.out.println(unsafe.shouldBeInitialized(User.class));     Field sexField = User.class.getDeclaredField("name");     //获取给定静态字段的内存地址偏移量     long fieldOffset = unsafe.staticFieldOffset(sexField);     //获取一个静态类中给定字段的对象指针     Object fieldBase = unsafe.staticFieldBase(sexField);     //根据某个字段对象指针和偏移量可以唯一定位这个字段。     Object object = unsafe.getObject(fieldBase, fieldOffset);     System.out.println(object); }
复制代码


此外,在 Java8 中引入的 Lambda 表达式的实现中也使用到了 defineClass 和 defineAnonymousClass 方法。


3.8 系统信息


Unsafe 中提供的 addressSize 和 pageSize 方法用于获取系统信息。


1) 调用 addressSize 方法会返回系统指针的大小,如果在 64 位系统下默认会返回 8,而 32 位系统则会返回 4。


2) 调用 pageSize 方法会返回内存页的大小,值为 2 的整数幂。


使用下面的代码可以直接进行打印:

private void systemTest() {     System.out.println(unsafe.addressSize());     System.out.println(unsafe.pageSize());}
复制代码


输出为:8,4096


四、总结


哎呀,妈呀,终于写完了,人要傻了,为了整理这篇文章看了大量的源码,人看的头大,跟俄罗斯套娃似的源码,严谨的串联在一起!Unsafe 类在日常的面试中确实不经常被问到,大家稍微了解一下即可。


文章转载自:JavaBuild

原文链接:https://www.cnblogs.com/JavaBuild/p/18211950

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
当面试官问出“Unsafe”类时,我就知道这场面试废了,祖坟都能给你问出来!_Java_EquatorCoco_InfoQ写作社区