EchoDemo's Blogs

鲜为人知的Unsafe类

孤山寺北贾亭西,水面初平云脚低。

几处早莺争暖树,谁家新燕啄春泥。

乱花渐欲迷人眼,浅草才能没马蹄。

最爱湖东行不足,绿杨阴里白沙堤。

—-白居易《钱塘湖春行》

Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java官方也不建议直接使用的Unsafe类,但我们还是很有必要了解该类,因为Java中CAS操作的执行依赖于Unsafe类的方法,注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下:

1、内存管理,Unsafe类中存在直接操作内存的方法。

2、获取对象的实例。

3、挂起与恢复。

4、CAS操作

内存管理

通过Unsafe类可以分配内存,可以释放内存;类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。

//分配内存指定大小的内存
public native long allocateMemory(long bytes);

//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);

//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);

除此之外,它还有下列一些操作内存的方法:

//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);

//设置给定内存地址的值
public native void putAddress(long address, long x);

//获取指定内存地址的值
public native long getAddress(long address);

//设置指定内存的byte值
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
public native byte getByte(long address);

//获取指定内存的byte值
public native void putByte(long address, byte x);

//操作系统的内存页大小
public native int pageSize();

//对象字段的定位,该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的
public native long staticFieldOffset(Field field);

//获取对象中offset偏移地址对应的整型field的值,支持volatile load语义
public native int getIntVolatile(Object obj, long l);

//获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class class1);

//获取数组的转换因子,也就是数组中元素的增量地址
public native int arrayIndexScale(Class class1);

Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。


获取对象的实例

//传入一个对象的class并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class cls) throws InstantiationException;

挂起与恢复

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在 LockSupport类中(java.util.concurrent包中挂起操作都是在LockSupport类实现的),LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法来实现的。

public class LockSupport {
    public static void unpark(Thread thread) {
        if (thread != null)
            unsafe.unpark(thread);
    }

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        unsafe.park(false, 0L);
        setBlocker(t, null);
    }

    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            unsafe.park(false, nanos);
            setBlocker(t, null);
        }
    }

    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        unsafe.park(true, deadline);
        setBlocker(t, null);
    }

    public static void park() {
        unsafe.park(false, 0L);
    }

    public static void parkNanos(long nanos) {
        if (nanos > 0)
            unsafe.park(false, nanos);
    }

    public static void parkUntil(long deadline) {
        unsafe.park(true, deadline);
    }
}

Unsafe里的CAS操作

CAS是一些CPU直接支持的指令,在Java中无锁操作CAS基于以下3个方法实现:

public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x); 

第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值。expected表示期望值,x表示要设置的值。


利用Unsafe类

获取Unsafe实例

public static Unsafe getUnsafeInstance() throws Exception{
    Field unsafeStaticField = Unsafe.class.getDeclaredField("theUnsafe");
    unsafeStaticField.setAccessible(true);
    return (Unsafe) unsafeStaticField.get(Unsafe.class);
}

通过java反射机制,我们跳过了安全检测,拿到了一个Unsafe类的实例。


修改和读取数组中的值

@RequestMapping("/readandwritearray")
public void readAndWriteArray() throws Exception {
    Unsafe unsafe = getUnsafeInstance();
    int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    //返回当前数组的首地址
    int b = unsafe.arrayBaseOffset(int[].class);
    //返回当前数组一个元素占用的字节数
    int s = unsafe.arrayIndexScale(int[].class);
    //获取数组对象obj的起始地址,加上偏移值,得到对应元素的地址,将intval写入内存
    unsafe.putInt(arr, (long) b + s * 9, 1);

    for (int i = 0; i < 10; i++) {
        //获取数组对象obj的起始地址,加上偏移值,得到对应元素的地址,从而获得元素的值
        int v = unsafe.getInt(arr, (long) b + s * i);
        System.out.print(v + " ");
    }
}

打印结果:1 2 3 4 5 6 7 8 9 1 ,可以看到,成功读出了数组中的值,而且最后一个值由10改为了1。

偏移值: 数组元素偏移值 = arrayBaseOffset + arrayIndexScalse * i。


修改静态变量和实例变量的值

先定义一个UnsafeTest类:

public class UnsafeTest {
    public int infield;

    public static int staticIntField;

    public static int[] arr;

    private UnsafeTest() {
        System.out.println("constructor called!");
    }
}

修改UnsafeTest类的实例变量:

@RequestMapping("/changeinstancefield")
public void changeInstanceField() throws Exception {
    Unsafe unsafe = getUnsafeInstance();
    //传入一个对象的class并创建该实例对象,但不会调用构造方法
    UnsafeTest unsafeTest = (UnsafeTest)unsafe.allocateInstance(UnsafeTest.class);
    //获取对象某个属性的地址偏移值
    long b1 = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("infield"));
    unsafe.putInt(unsafeTest, b1, 2);
    System.out.println("infield:" + unsafeTest.infield);
}

这里使用allocateInstance方法获取了一个UnsafeTest类的实例,并且没有打印“constructor called”,说明构造方法没有调用。修改实例变量与修改数组的值类似,同样要获取地址偏移值,然后调用putInt方法。

修改UnsafeTest类的静态变量:

@RequestMapping("/changestaticfield")
public void changeStaticField() throws Exception {
    Unsafe unsafe = getUnsafeInstance();
    Field staticIntField = UnsafeTest.class.getDeclaredField("staticIntField");
    //获取静态变量所属的类在方法区的首地址。可以看到,返回的对象就是UnsafeTes.class
    Object o = unsafe.staticFieldBase(staticIntField);
    System.out.println(o == UnsafeTest.class);
    //获取静态变量地址偏移值
    Long b4 = unsafe.staticFieldOffset(staticIntField);
    //因为是静态变量,传入的Object参数应为class对象
    unsafe.putInt(o, b4, 10);
    System.out.println("staticIntField:" + unsafe.getInt(UnsafeTest.class, b4));
}

打印结果:

true

staticIntField:10

静态变量与实例变量不同之处在于,静态变量位于方法区中,它的地址偏移值与UnsafeTest类在方法区的地址相关,与UnsafeTest类的实例无关。


调戏String.intern

在jdk7中,String.intern不再拷贝string对象实例,而是保存第一次出现的对象的引用。在下面的代码中,通过Unsafe修改被引用对象s的私有属性value达到间接修改s1的效果!

@RequestMapping("/stringintern")
public void stringIntern() throws Exception {
    String s = "abc";
    //保存s的引用
    s.intern();
    //此时s1==s,地址相同
    String s1 = "abc";
    Unsafe unsafe = getUnsafeInstance();
    //获取s的实例变量value
    Field valueInString = String.class.getDeclaredField("value");
    //获取value的变量偏移值
    long offset = unsafe.objectFieldOffset(valueInString);
    //value本身是一个char[],要修改它元素的值,仍要获取baseOffset和indexScale
    long base = unsafe.arrayBaseOffset(char[].class);
    long scale = unsafe.arrayIndexScale(char[].class);
    //获取value
    char[] values = (char[]) unsafe.getObject(s, offset);
    //为value赋值
    unsafe.putChar(values, base + scale, 'c');
    System.out.println("s:" + s + " s1:" + s1);
    //将s的值改为 abc
    s = "abc";
    String s2 = "abc";
    String s3 = "abc";
    System.out.println("s:" + s + " s1:" + s1);
    System.out.println("s2:" + s2 +" s3:" + s3);
}

打印结果:

s:acc s1:acc

s:acc s1:acc 

s2:acc s3:acc

我们发现了什么?所有值为“abc”的字符串都变成了“acc”。Unsafe类果然不安全!!!

🐶 您的支持将鼓励我继续创作 🐶
-------------本文结束感谢您的阅读-------------