Unsafe魔法类解析及应用


Unsafe类提供了一些极度不安全的方法,这些方法会直接访问系统内存和对系统内存进行操作。,由于它是直接对内存进行的操作,所以从他的命名也可以看出它是不安全的。Unsafe类的使用必须慎重;juc包中大量运用了Unsafe类,对Unsafe的了解也会方便与了解juc的一些类,即使一般情况下我们不使用这个类,但我们也有必要对其进行理解。

Unsafe的API的分类大致是内存操作,CAS,内存屏障,线程调度;

Unsafe类的使用

Unsafe方法是单例的,要想使用Unsafe首先需要调用Unsafe类的getUnsafe方法。

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

从上面代码**!VM.isSystemDomainLoader(var0.getClassLoader())**

public static boolean isSystemDomainLoader(ClassLoader var0) {
    return var0 == null;
}

可以看出如果你调用Unsafe类必须保证Unsafe类是系统类加载器(系统类加载器ClassLoader==null,因为系统类加载器是由C加载的)去加载的。直接调用getUnsafe会报错。

public static void main(String[] args) {
    Unsafe unsafe = Unsafe.getUnsafe();
}

// 调用结果
Exception in thread "main" java.lang.SecurityException: Unsafe
    at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
    at com.dm.jmm.util.test.main(test.java:33)

通过资料发现Unsafe的使用有2种方法

  1. 通过Java命令行命令-Xbootclasspath/a: ${path}把调用Unsafe相关方法的类test所在jar包路径追加到默认的bootstrap路径中,使得test被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。

  2. 通过反射拿取,下面的应用皆用这种方式

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

内存屏障

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

还记得之前JMM模型是如何禁止重排序的吗,里面有详细介绍了内存屏障包括volatile触发内存屏障。Java关键字系列(一)-synchronized与volatile

在这里我们可以使用Unsafe手动设置内存屏障。

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

以之前举例证明重排序存在的代码为例,之前是通过添加volatile可以解决,现在通过手动添加内存屏障解决

@Slf4j
public class test {
    private  static int x = 0, y = 0;
    private  static int a = 0, b = 0;
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    shortWait(10000);
                    a = 1;
                    
                    // 添加屏障代码,需引入上面通过反射拿取Unsafe类
                    UnsafeInstance.reflectGetUnsafe().fullFence();
                    
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    
                     // 添加屏障代码,需引入上面通过反射拿取Unsafe类
                    UnsafeInstance.reflectGetUnsafe().fullFence();
                    
                    y = a;
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                log.info(result);
            }
        }

    }

    /**
     * 等待一段时间,时间单位纳秒
     * @param interval
     */
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

现在这段代码运行就不会结束了,证明内存屏障设置成功了。

内存操作

Unsafe相关的内存操作都是与堆外内存相关,包括堆外内存的分配,释放,拷贝等等。

堆外内存

大家都知道Java 对象基本都是存放在堆或者栈上面的,它们的分配,释放都是依靠JVM来解决的。但在我们的Java中有一种内存叫做堆外内存,这部分内存不被JVM所托管。元空间的内存就是堆外内存,所以如果我们不设置**-XX:MaxMetaspaceSize**参数,由于它是堆外内存就有可能导致元空间内存不断往上加,最后可能撑爆系统内存(堆外内存不受JVM内存管理,它依托的是系统内存)。

而对堆外内存的操作我们通常是使用Unsafe类操作,这种操作危险。如果代码内存溢出,可能会把服务器内存打宕掉。这里一定得注意内存的释放。

Q:既然JVM可以管理内存,为什么我们会有使用堆外内存的需求呢?

  1. 减少GC次数,避免垃圾回收停顿对应用程序的影响。JVM是有GC机制的。如果我们操作的一个内存是比较大的,可能会频繁GC。但我们使用堆外内存(不受JVM控制),会有效的减少GC的STW。
  2. 提升程序I/O操作的性能。在I/O通信过程中,有堆内内存->堆外内存的数据拷贝操作,如果有这种需求我们可以直接把数据存储到堆外内存,减少堆内内存到堆外内存的时间,提升IO性能。这也是零拷贝的思想,在Netty和RocketMQ都有应用。关于零拷贝的详细可见Netty线程模型初探和Netty的常见问题
//分配内存, 相当于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);

CAS

CAS全称CompareAndSwap,从名字就可以看出它是什么作用了,就是比较和交换。

// 下面几个方法作用一样
// 介绍一下参数值0--修改field的对象,offset--对象中某field的偏移量,expected--期望值,update--更新值
// 他的操作就是将内存位置的值和expected进行比较,是不是一样,一样就把update和当前内存位置的值进行交换
// 整个过程是原子的。
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在整个juc中应用是最为广泛的,比如AQS,Atomic等等,可能就是因为他是原子的所以才会在并发包中得到这么广泛的应用吧。其实他整个操作都是基于offset进行操作的,offset可以使用unsafe的objectFieldOffset获取。通过offset就可以找到内存地址,我们在从内存地址中获取这个值,拿到值就跟期望值expected进行比较,如果一样就把更新值update赋值到这个地址上去。


文章作者: dm
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 dm !
评论
 上一篇
HashMap1.7和1.8对比与源码解析 HashMap1.7和1.8对比与源码解析
常用APIput过程:它会hash传入的key值,将hash的值&上map长度减一(这里用的是&而不是取模运算,应该是考虑到性能问题,这里是length-1是应为这样可以取到0到map.length-1的值)插入到对应的数组
2021-11-05
下一篇 
Java多线程介绍与线程池底层实现原理 Java多线程介绍与线程池底层实现原理
Java线程跟操作系统的关系CPU一般有4个安全等级ring0,ring1,ring2,ring3,操作系统的内部程序指令一般运行在ring0级别,而我们的应用程序会运行在ring3上面,比如JVM进程。为什么说JVM线程的创建是一个比较重
2021-08-27
  目录