谈谈java中的unsafe类

1,539 阅读5分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

前言

看过JUC并发包里面的源码,就一定明白Unsafe类是整个java并发包底层实现的核心。Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,Unsafe类提供了硬件级别的原子操作,Unsafe里面的方法都是 native方法,通过使用JNI的方式来访问本地C++实现库。下面就继续深入的看一下Unsafe类。

主要方法

Unsafe有很多public native修饰的方法,还有几十个基于public native方法的其他方法。但是大体可分为以下几类:

(1)初始化操作

(2)操作对象属性

(3)操作数组元素

(4)线程挂起和回复

(5)CAS机制

下面就对Unsafe源码进一步分析。

操作属性方法

//通过给定的Java变量获取引用值。这里实际上是获取一个Java对象o中,获取偏移地址为offset的属性的值,此方法可以突破修饰符的抑制,也就是无视privateprotecteddefault修饰符。类似的方法有getInt、getDouble等等。同理还有putObject方法。

public native Object getObject(Object o, long offset);
//强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。同理还有putObjectVolatile。

public native Object getObjectVolatile(Object o, long offset);
//设置o对象中offset偏移地址offset对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被修改的时候使用才会生效。类似的方法有putOrderedInt和putOrderedLong。

public native void putOrderedObject(Object o, long offset, Object x);
//返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。
public native long staticFieldOffset(Field f);

//返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。
public native long objectFieldOffset(Field f);

//返回给定的静态属性的位置,配合staticFieldOffset方法使用。
public native Object staticFieldBase(Field f);

操作数组

//返回数组类型的第一个元素的偏移地址(基础偏移地址)
public native int arrayBaseOffset(Class arrayClass);

//返回数组中元素与元素之间的偏移地址的增量。这两个方法配合使用就可以定位到任何一个元素的地址。
public native int arrayIndexScale(Class arrayClass);

内存管理

//获取本地指针的大小(单位是byte),通常值为4或者8。常量ADDRESS_SIZE就是调用此方法。
public native int addressSize();

//获取本地内存的页数,此值为2的幂次方。
public native int pageSize();

//分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址
public native long allocateMemory(long bytes);

//通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。
public native long reallocateMemory(long address, long bytes);

//将给定内存块中的所有字节设置为固定值(通常是0)。
public native void setMemory(Object o, long offset, long bytes, byte value);

内存屏障

//在该方法之前的所有读操作,一定在load屏障之前执行完成。
public native void loadFence();

//在该方法之前的所有写操作,一定在store屏障之前执行完成
public native void storeFence();

//在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能。
public native void fullFence();

线程挂起和恢复

//释放被park创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的
public native void unpark(Object thread);

//阻塞当前线程,一直等道unpark方法被调用
public native void park(boolean isAbsolute, long time);`

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

执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作

Unsafe的使用

获取Unsafe实例

Unsafe 提供了getUnsafe方法,如下所示:

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

直接这样用会报异常:

Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.bendcap.java.jvm.unsafe.Main.main(Main.java:13)

因为Unsafe类主要是JDK内部使用,并不提供给普通用户调用。但是仍让可以通过反射获取到实例:

public static Unsafe testUnsafe() {
    try {
        Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
        Field field = unsafeClass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        return unsafe;
       } catch (Exception e) {
        e.printStackTrace();
    }
 return null;
 }

改变私有字段

假设有如下类:

public class UnsafeDemo {
    private int juejin = 0;

    public boolean juejinDisclosed() {
        return juejin == 1;
    }
}

下面通过Unsafe来改变私有属性juejin的值。

UnsafeDemo ud = new UnsafeDemo();
Field field = null;
try {
    field = ud.getClass().getDeclaredField("juejin");
} catch (NoSuchFieldException e) {
    e.printStackTrace();
}
Unsafe unsafe = testUnsafe();
unsafe.putInt(ud, unsafe.objectFieldOffset(field), 1);
return ud.juejinDisclosed();

通过unsafe.putInt直接改变了ud的私有属性的值。一旦通过反射获得了类的私有属性字段,就可以直接操作它的值。

总结

虽然Unsafe看起来不会被用到,但是能帮助我们更好的理解JUC的并发原理,因此还是很有必要学习的。