Java UnSafe 类使用介绍

1. UnSafe 简介

UnsafeJava 整个并发包底层实现的核心,它具有像 C++ 的指针一样直接操作内存的能力,而这也就意味着其越过了 JVM 的限制。Unsafe 的特性可归结如下,它虽然在一定程度上提升了效率,但是也带来了指针的不安全性

  1. Unsafe 不受 JVM 管理,也就无法被自动 GC,需要手动 GC,容易出现内存泄漏
  2. Unsafe 的大部分方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量需自行计算,一旦出现问题必然是 JVM 崩溃级别的异常,会导致整个应用程序直接 crash
  3. 直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率

2. UnSafe 功能与使用示例

Unsafe 类的构造函数是私有的,而对外提供的静态方法Unsafe#getUnsafe() 又对调用者的 ClassLoader 有限制 ,如果这个方法的调用者不是由 Boot ClassLoader 加载的,则会报错

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

因为 Java 源码中由开发者自定义的类都是由 Appliaction ClassLoader 加载的,所以正常情况下我们无法直接使用Unsafe ,如果需要使用它,则需要利用反射

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

2.1 操作对象属性

方法 功能
public native Object getObject(Object o, long offset) 获取一个Java 对象中偏移地址为 offset 的属性的值,此方法可以突破修饰符的限制,类似的方法有 getInt()、getDouble()等,同理还有 putObject() 方法
public native Object getObjectVolatile(Object o, long offset) 强制从主存中获取目标对象指定偏移量的属性值,类似的方法有getIntVolatile()、getDoubleVolatile()等,同理还有putObjectVolatile()
public native void putOrderedObject(Object o, long offset, Object x) 设置目标对象中偏移地址 offset 对应的对象类型属性的值为指定值。这是一个有序或者有延迟的 putObjectVolatile()方法,并且不保证值的改变被其他线程立即看到。只有在属性被volatile修饰并且期望被修改的时候使用才会生效,类似的方法有 putOrderedInt() 和 putOrderedLong()
public native long objectFieldOffset(Field f) 返回给定的非静态属性在它的类的存储分配中的位置(偏移地址),然后可根据偏移地址直接对属性进行修改,可突破属性的访问修饰符限制

private String name;

@Test
public void test() {
    
    
        Unsafe unsafe = getUnsafe();
        try {
    
    
            DirectMemory directMemory = (DirectMemory) unsafe.allocateInstance(DirectMemory.class);
            long nameOffset = unsafe.objectFieldOffset(DirectMemory.class.getDeclaredField("name"));
            unsafe.putObject(directMemory, nameOffset, "luck");
            System.out.println(directMemory.getName());
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
}

2.2 操作数组元素

方法 功能
public native int arrayBaseOffset(Class arrayClass) 返回数组类型的第一个元素的偏移地址(基础偏移地址)
public native int arrayIndexScale(Class arrayClass) 返回数组中元素与元素之间的偏移地址的增量,配合 arrayBaseOffset() 使用就可以定位到任何一个元素的地址

private String[] names = {
    
    "nathan", "goog", "luck"};
  
@Test
public void test() {
    
    
        Unsafe unsafe = getUnsafe();
        try {
    
    
             Class<?> a = String[].class;
             int base = unsafe.arrayBaseOffset(a);
             int scale = unsafe.arrayIndexScale(a);
             // base + i * scale 即为字符串数组下标 i 在对象的内存中的偏移地址
             System.out.println(unsafe.getObject(names, (long) base + 2 * scale));
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
}

ForkJoinPool 中有一种新的元素定位算法,这种方法要求 scale 也就是元素的内存大小为 2 的次幂。假如 scale 等于 4,那么 ASHIFT 值为 2,乘以 4 和向左移 2 位结果是一样的,但是移位操作显然更高效

ASHIFT = 31 - Integer.numberOfLeadingZeros(scale)
offset = (i << ASHIFT) + ABASE

2.3 内存地址操作

方法 功能
public native int addressSize() 获取本地指针的大小(单位是byte),通常值为 4 或者 8
public native int pageSize() 获取本地内存的页数,此值为2的幂次方
public native long allocateMemory(long bytes) 分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址
public native long reallocateMemory(long address, long bytes) 通过指定的内存地址 address 重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)
public native void setMemory(Object o, long offset, long bytes, byte value) 将给定内存块中的所有字节设置为固定值(通常是0)
public class DirectMemoryBuffer {
    
    
    private final static int BYTE = 1;
    private long size;
    private long address;
    private Unsafe unsafe;

    public DirectMemoryBuffer(long size, Unsafe unsafe) {
    
    
        this.size = size;
        this.unsafe = unsafe;
        address = unsafe.allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
    
    
        unsafe.putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
    
    
        return unsafe.getByte(address + idx * BYTE);
    }

    public long size() {
    
    
        return size;
    }

    public void freeMemory() {
    
    
        unsafe.freeMemory(address);
    }
}

2.4 CAS 操作

CAS 是一种乐观锁机制,它不需要抢占锁,能有效地提高效率,依赖于硬件的原子操作实现

方法 功能
public final native boolean compareAndSwapObject(Object target, long offset, Object exceptData, Object targetData) 其作用为比较目标对象指定偏移量的属性的期望值与主存中的值,如果二者相等,则将主存中的值更新为目标值。同样还有 compareAndSwapInt() 和 compareAndSwapLong()
public class CASCounter {
    
    
    private Unsafe unsafe;
    // count 需要声明为 volatile 来保证对所有线程可见
    private volatile long count = 0;
    private long offset;

    public CASCounter(Unsafe unsafe) {
    
    
        this.unsafe = unsafe;
        try {
    
    
            offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("count"));
        } catch (NoSuchFieldException e) {
    
    
            e.printStackTrace();
        }
    }

    // 关键的 increment 方法,其在 while 循环里不断尝试调用 compareAndSwapLong,在方法内部累加count的同时
    // 检查其值有没有被其他线程改变。如没有,就提交更改,如果不一致,那么继续尝试提交更改
    public void increment() {
    
    
        long before = count;
        while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
    
    
            before = count;
        }
    }

    public long getCount() {
    
    
        return count;
    }
}

2.5 线程挂起与恢复

方法 功能
public native void park(boolean isAbsolute, long time) 阻塞当前线程,一直等到 unpark()方法被调用或者超时,和 Object.await()非常类似,但是 park 是操作系统调用,因此在某些操作系统架构上这会带来更好的性能
public native void unpark(Object thread) 唤醒被 park 阻塞的线程,由于其不安全性,因此必须保证线程是存活的

2.6 内存屏障

方法 功能
public native void loadFence() 如果不要volatile去增加内存屏障,即可用该方法手动增加屏障。在该方法之前的所有读操作,一定在load屏障之前执行完成
public native void storeFence() 在该方法之前的所有写操作,一定在store屏障之前执行完成
public native void fullFence() 在该方法之前的所有读写操作,一定在 full 屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能

2.7 Class 相关

方法 功能
public native boolean shouldBeInitialized(Class<?> c) 判断是否需要初始化一个类,通常在获取一个类的静态属性的时候使用(一个类如果没初始化,它的静态属性也不会初始化)。 当且仅当ensureClassInitialized方法不生效时返回false
public native void ensureClassInitialized(Class<?> c) 检测给定的类是否已经初始化,通常在获取一个类的静态属性的时候使用(因为一个类如果没初始化,它的静态属性也不会初始化)
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain) 定义一个类,此方法会跳过JVM的所有安全检查,默认情况下 ClassLoader(类加载器)和 ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches) 定义一个匿名类
public native long staticFieldOffset(Field f) 返回给定的静态属性在它的类的存储分配中的位置(偏移地址)
public native Object staticFieldBase(Field f) 返回给定的静态属性的位置,配合 staticFieldOffset()方法使用

猜你喜欢

转载自blog.csdn.net/weixin_45505313/article/details/106542241