Unsafe 类简单介绍

UnSafe 是一个比较神秘的类,它和 CAS、Atomic 并发包有很大的关系,其中它的源码如下:

// JDK 1.7 版本
public final class Unsafe {

	private static final Unsafe theUnsafe;

    private static native void registerNatives();

    private Unsafe() {
    }

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (var0.getClassLoader() != null) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

    static {
        registerNatives();
        Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
        theUnsafe = new Unsafe();
    }
    
}

从代码可以看出,unsafe 类采用饿汉式单例模式,对象在加载类时初始化,静态代码块首先执行 registerNatives() 方法。registerNatives() 方法通过 native 修饰,基于底层操作系统实现,在正式介绍该方法前我们先了解 native 方法的原理:

一个 Java 程序想要调用本地方法,首先需要执行以下两个步骤:

  1. 通过 System.loadLibrary() 将包含本地方法实现的动态文件加载进内存
  2. 调用本地方法时,虚拟机在内存中定位并链接本地方法实现,从而执行本地方法

registerNatives() 方法的主要作用就是替换步骤二:通过调用该方法,使程序主动去链接本地方法实现,而不是被动的通过 jvm 定位并链接。这里链接的就是 unsafe 类中其它 native 方法(unsafe 类所有方法都通过 native 修饰,上述源码省略掉了,暂时不关键)

使用 registerNatives() 方法包含以下三个优点:

  • native 方法在类加载时就完成链接,无须调用时再通过 jvm 定位链接,整体效率更高
  • 支持动态更新,如果本地方法发生改变,可以通过调用 registerNatives() 方法更新
  • Java 程序调用本地应用提供的方法时,因为虚拟机只会检索本地动态库,无法定位到具体方法实现,此时只能使用 registerNatives() 方法进行主动链接

关于 registerNatives() 方法的介绍就写到这里,感兴趣可以 点击这里 参考一篇更详细的博客

执行完 registerNatives() 方法后,执行如下代码:

Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});

通过这行代码,增加过滤器,将 Unsafe 类的 getUnsafe 方法过滤,开发者无法通过反射获取并调用该方法

Unsafe 类获取唯一对象的静态方法通过 @CallerSensitive 修饰,该注释主要是为了控制权限。该注解搭配 Reflection.getCallerClass() 一起使用:

getCallerClass() 是一个 native 方法,该方法只能被 bootstrap 类加载器 和 extension 类加载器加载过的类调用,也就是说,我们平常编写的代码没有权限调用该方法。曾经有黑客通过双重反射的方式提升权限, @CallerSensitive 主要解决该漏洞

一般情况下通过 getCallerClass(2) 看上层调用者判断当前类是否有权限访问当前类,如果使用双重反射,整个调用链变成如下格式:我 -》 反射1 -》 反射2,此时判断 反射2 的权限时,实际看到的是 反射1 的权限,而反射类拥有极高的权限,导致安全漏洞。使用 @CallerSensitive 可以跳过所有被 @CallerSensitive 修饰的方法,反射类大多数方法都增加了 @CallerSensitive 注释,通过这种方法解决漏洞

最后 Unsafe 类通过 final 修饰,不允许被继承。总得来说,Java 通过过滤 getUnsafe() 方法、控制权限,单例、final 修饰等方式保证开发者不能直接使用该类


Java 不建议开发者使用 Unsafe 类的主要原因在于它非常危险:和 c 指针一样,unsafe 类可以直接操作内存,一旦开发者调用失败就可能导致内存溢出。Unsafe 类主要方法如下:

  • 直接操作内存的方法:
// 分配指定大小的内存
public native long allocateMemory(long bytes);
// 根据地质分配指定大小的内存
public native long reallocateMemory(long address, long bytes);
// 释放指定地址的内存
public native void freeMemory(long address);;
// 设置执行地址内存的值
public native void putAddress(long address, long x);
// 获取指定地址内存的值
public native long getAddress(long address);
  • 操作 Java 对象的方法:
// 不通过构造方法直接在内存创建指定类的对象
public native Object allocateInstance(Class cls) throws InstantiationException;
// 获取字段 f 在对象内存中的偏移量
public native long objectFieldOffset(Field f);
// 获取静态属性 f 在类对象内存中的偏移量
public native long staticFieldOffset(Field f);
// 获取参数对象相应偏移量上的引用对象
public native Object getObject(Object o, long offset);
// 设置参数对象相应偏移量上的对象值
public native void putObject(Object o, long offset, Object x);
// 获取参数对象相应偏移量上的最新 int 值
public native int getIntVolatile(Object o, long offset);
// 设置参数对象相应偏移量上的 int 值,volatile 修饰,保证可见性,设置后立刻可以看到
public native void putIntVolatile(Object o, long offset, int x);
// 设置参数对象相应偏移量上的对象值,volatile 修饰,保证可见性,设置后立刻可以看到
public native void putObjectVolatile(Object o, long offset, Object 02);
// 设置参数对象相应偏移量上的对象值,需要 volatile 修饰,但不保证可见性,效率更高
public native void putOrderedObject(Object o, long offset, Object 02);

putOrderedObject() 是 putObjectVolatile() 的内存非立即可见版本,它可以实现非阻塞的写入,这种写入不会被 Java 的 JIT 重新排序指令,实现快速的 存储->存储 而不是 存储->加载,后者虽然能保证可见,但代价较大,前者写的结果不会被立即看到,但效率更高,代价可忽略,ConcurrentHashMap 中大量使用 putOrderedObject 而不是 putObjectVolatile

  • 操作数组对象相关的方法:
// 获取数组第一个元素的偏移量
public native int arrayBaseOffset(Class arrayClass);
// 获取数组中每个元素占据的内存大小
public native int arrayIndexScale(Class arrayClass);
  • CAS实现 相关方法
// var1 表示给定对象,var2 表示偏移量,用来计算具体要操作的属性对象,var4 表示期望值,var5 表示要设置的新值
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);
  • 线程相关
// 挂起线程,知道超时或被中断
public native void park(boolean isAbsolute, long time);  
// 恢复挂起的线程
public native void unpark(Object thread); 
  • 内存屏障相关(JDK1.8 新增)
// 所有读操作必须在该方法前执行完毕
public native void loadFence();
// 所有写操作必须在该方法前执行完毕
public native void storeFence();
// 所有读、写操作必须在该方法前执行完毕
public native void fullFence();

好家伙,CAS、volatile 以及线程的挂起和恢复在 UnSafe 类中都有实现,这也是为什么该类不对开发者提供的主要原因(功能越大越不安全)。虽然 Java 对 UnSafe 类增加种种权限,防止开发者使用,但我们仍可以通过反射获取 unsafe 对象:

// 反射虽然屏蔽了 getUnsafe() 方法、但没有屏蔽 theUnsafe 属性
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置该属性可访问
field.setAccessible(true);
// 通过属性获取对应对象,这里传 null 是因为 theUnsafe 是静态的
Unsafe unsafe = (Unsafe) field.get(null);

unsafe 类自己玩一下可以,不建议在开发中使用,一旦内存操作出错,内存泄露是小,极有可能造成无法预估的结果

最后我们简单总结 UnSafe:Java 不建议开发者使用,由于基于内存操作,获取到的数据总是最新的,保证可见性和有序性。它实现了 CAS 和 内存屏障,CAS 最常在 Atomic 并发包中使用,volatile 常和锁联合使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值