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

一、写在开头

依稀记得多年以前的一场面试中,面试官从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的实例,为什么呢?我们看它的这段源码便知:

java 复制代码
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 实例。但是!当我们直接去调用这个方法的时候,会报如下错误:

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

方式二

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

java 复制代码
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来感受一下它的使用吧。

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

输出:

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

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

三、Unsafe类的8种应用

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

3.1 内存操作

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

内存操作的常用方法:

java 复制代码
/*包含堆外内存的分配、拷贝、释放、给定地址值操作*/
//分配内存, 相当于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方法来实现内存屏障:

java 复制代码
//内存屏障,禁止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(),一起去看一下。

java 复制代码
	/**
     * 使用乐观读锁访问共享资源
     * 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候					可能其他写线程已经修改了数据,
     * 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
     *
     * @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中做了一个根绝印章校验写锁发生的操作,我们跟入这个校验源码中:

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

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

3.3 对象操作

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

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

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

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

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

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

java 复制代码
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使用的测试代码,我们增加如下的方法:

java 复制代码
  //获取数组元素在内存中的偏移地址,以及偏移量
    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));
        }
    }

输出:

java 复制代码
数组第一个元素的偏移地址:16
元素偏移量4
16 : aaa
20 : bb
24 : 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()方法。

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

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

java 复制代码
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方式来实现。

java 复制代码
//取消阻塞线程
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);

LockSupport源码:

java 复制代码
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的相关操作主要包括静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。

java 复制代码
//获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
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 复制代码
@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 的整数幂。

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

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

输出为:8,4096

四、总结

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

五、结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

如果您想与Build哥的关系更近一步,还可以关注"JavaBuild888",在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

相关推荐
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck1 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei1 小时前
java的类加载机制的学习
java·学习
码农小旋风3 小时前
详解K8S--声明式API
后端
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml43 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~3 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616883 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7894 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java4 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet