简单了解sun.misc.Unsafe

这里简单了解下sun.misc.Unsafe,jdk中的类很多地方都有用到它,它是许多类的实现的关键,知道这个类干嘛的,对一些源码的阅读很有帮助。
sun.misc.Unsafe算是Java留下的后门,能提供相当强大的操作,但是又不提供专业的文档。看名字就知道,sun本身就不推荐使用这个类。一般应用级的代码都不应该使用它,框架级别的,用这个的很多,比如jdk自身,一些框架netty,最常见的框架spring。

jdk9做了一些限制,不过还是可以使用这个类。


这个类本身是单例的,需要通过静态方法获取唯一实例。不过编译器、运行时都会报错。编译器的降低下错误级别就行,运行时的,根据代码

public static Unsafe getUnsafe() {
    Class<?> caller = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(caller.getClassLoader())) // 判断调用Unsafe的类是否是BootstrapClassLoader加载的类
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

知道应该是通过类加载器限制。一般我们写的类都是由Application ClassLoader(sun.misc.Launcher$AppClassLoader)进行加载的,层级比较低,这里的SystemDomainLoader就是BootstarpClassLoader(C++写的),也就是加载rt.jar里面的类的加载器,所以java.*用就不会有事,我们用就会有事。

想要使用Unsafe有两种方式。一种是用反射,比较简单

Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe u = (Unsafe) unsafeField.get(null);
另外一种是通过虚拟机启动参数-Xbootclasspath,把你的classpath变为启动路径之一,这样就是BootstarpClassLoader加载你的类,跟java.*一个待遇了,就不会报错了。


Unsafe这个类对于大多数开发者来说,主要还是供研究学习用的,用于理解一些源码设计,自己的程序尽量不要用这个类。

要用的话:下面的1234点,如果你觉得AtomicXXX满足不了,那么可以考虑使用下,影响比较小。第5点,java.util.concurrent.locks中都提供了更好的功能,不需要用。第6点,volatile有内存屏障的功能,因此一般也不需要用。第7点,不要使用,直接内存访问有一些现成的,比如java.nio和netty,可以用这些,更好更方便。第8点,用现成的框架更好,第9点,不知道用来干嘛。


下面简单说下Unsafe提供的功能

1、偏移量相关
这部分是最基础的,下面很多方法都需要这部分的配合
     long staticFieldOffset(Field f):静态属性的偏移量,用于在对应的Class对象中读写静态属性
     long objectFieldOffset(Field f):类属性的偏移量,在对应的实例对象中读写类属性
     Object staticFieldBase(Field f):返回值就是f.getDeclaringClass()
     int arrayBaseOffset(Class<?> arrayClass):数组第一个元素的实际地址相对于整个数组对象的地址的偏移量,在C语言中原生数组首地址就是第一个元素的地址,这里可以理解为是C结构体中包含一个数组,这个数组的第一个元素的地址相对于结构体地址的偏移量。
     int arrayIndexScale(Class<?> arrayClass):数组中一个元素占据的内存空间,就是C语言中 *(p + 1),这个“1”代表的实际内存字节数。基本类型就是基本类型的长度;对象类型,在32位虚拟机中是4字节;64位虚拟机,开启指针压缩时是4字节,不指针压缩是8字节。
此外还有一堆常量,是关于基本类型的数组,以及java.lang.Object类型的数组的基本偏移量。对这块的偏移量不好理解,那就去复习下C语言结构体,本质上就是对结构体的理解。


2、普通读写

基本类型,以及对象类型(引用)都有这三个方法,这里拿int型说。
    int getInt(long address):读取内存地址address所表示的int型,address最好是自己通过allocateMemory申请的内存中的地址,否则会产生不可预料的结果。相当于C语言,int* p = xxx ... return *p。
    int  getInt(Object o, long offset):读取一个Java对象实例中的int型变量的值(static/static final类型不行,它们属于整个类,不属于实例)。对于Java,相当于直接使用 . 运算读取属性(不通过方法),无视访问限定符,也就是全部相当于public,可以直接读写。C语言中,相当于已知结构体一个实例的内存地址o,以及结构体中某个int型属性的偏移量offset,读取结构体中这个int型的值,*(o + offset)。
    getInt(Object o, int offset) :行为同getInt(Object o, long offset),因为内存地址使用64位时会有问题,所以被废弃了。


上面三个对应的写操作

    void  putInt(long address, int x)
    void putInt(Object o, int offset, int x)
    void putInt(Object o, long offset, int x)
特别地,这些可以用来更改final/static final属性的值


3、volatile相关的读写

能够对非volatile修饰的变量执行volatile读写。
    int getIntVolatile(Object o, long offset)
    void putIntVolatile(Object o, long offset, int x)
基本类型以及对象类型都有上面这两个方法。

volatile变量执行orderedSet/lazySet
     void putOrderedInt(Object o, long offset, int x)
volatile变量的写,能够对其他线程立即可见,还隐含地具有顺序性。这个方法的作用就是只保持顺序性,但不保证对其他立即可见,只有volatile写一半的功能。它避免了其他线程中已经读取了的本地变量,因为volatile写而失效,降低对volatile变量的写引起的巨大开销,提升程序整体效率。常用于不要求严格的读写一致性,但要求写操作准确无误的地方,比如ConcurrentHashMap。
这个方法只有int long Object 三个版本,其余的需要通过自己转化(可以在程序设计时,使用int替换boolean char short float,使用long替换double,运行时使用方法进行转换,或者直接使用基本类型的包装类)。


简单看几个例子,学习下如何使用Unsafe提供的底层读写功能。

// 属性读写,可以绕过访问限制
public class TestUnsafe {
    public static void main(String[] args) {
        try {
            Field unsafeField = Unsafe.class.getDeclaredFields()[0];
            unsafeField.setAccessible(true);
            Unsafe u = (Unsafe) unsafeField.get(null);
            Class<?> tk = A.class;
            A a = new A();

            // 下面这些偏移量实际都可以用常量来表示,在一次程序运行过程中它们是不变的
            long offset = u.objectFieldOffset(tk.getDeclaredField("x"));
            System.err.println(u.getInt(a, offset)); // 读取x

            long finalOffset = u.objectFieldOffset(tk.getDeclaredField("finalX"));
            System.err.println(u.getInt(a, finalOffset)); // 读取finalX 233
            u.putInt(a, finalOffset, 100); // 可以用来更改final的值
            System.err.println(u.getInt(a, finalOffset)); // 读取finalX 100

            Object staticBase = u.staticFieldBase(tk.getDeclaredField("staticX")); // 这里就是返回 A.class,跟逻辑是一致
            long staticOffset = u.staticFieldOffset(tk.getDeclaredField("staticX"));
            System.err.println(u.getInt(staticBase, staticOffset));

            long staticFinalOffset = u.staticFieldOffset(tk.getDeclaredField("staticFinalX"));
            System.err.println(u.getInt(staticBase, staticFinalOffset));

            System.err.println(A.class == staticBase); // true
            System.err.println(u.getInt(A.class, staticOffset)); // 等价于u.getInt(staticBase, staticOffset)
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

class A {
    private int x = 123456;
    private final int finalX = 233;
    private static int staticX = 987654;
    private static final int staticFinalX = 666;
}

// Unsafe提供的强化数组读写的功能
// 大多数情况都可以直接使用AtomicReferenceArray来代替
public class TestArrayOffset {
    static Unsafe U;
    static {
        try {
            Field unsafeField = Unsafe.class.getDeclaredFields()[0];
            unsafeField.setAccessible(true);
            U = (Unsafe) unsafeField.get(null);
        } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    // 这个可以认为就是求a[i],不过这种看不出有什么实际的价值
    // getIntVolatile/putIntVolatile就很有价值了,提供了数组元素的volatile读写,如果写入的值不依赖当前值,可以做到原子读写
    // AtomicReferenceArray就是这样实现数组中某个元素的原子读写
    static int array(Object a, int i) {
        return U.getInt(a, (long)(Unsafe.ARRAY_INT_BASE_OFFSET + Unsafe.ARRAY_INT_INDEX_SCALE * i));
    }

    public static void main(String[] args) {
        int[] ints = {13, 17, 19, 23, 29, 31, 37};
        for (int i = 0; i < ints.length; i++) {
            System.err.println(array(ints, i));
        }
    }
}


4、CAS相关

CAS是一些CPU直接支持的指令,是许多无锁操作的核心,是并发相关的重要知识。简单来说,它执行的操作是:如果变量在当前时刻的值和预期值expected相等,那么就 尝试把它更新为指定的值x,更新成功返回true,更新失败返回false。
    boolean compareAndSwapInt(Object o, long offset, int expected, int x)
    boolean compareAndSwapObject(Object o, long offset, Object expected, Object x)
    boolean compareAndSwapLong(Object o, long offset, long expected, long x)
只支持三个版本的,其他的需要自己转换。
CAS是个强大的操作,不过处理不了ABA的问题,这个一定要注意。另外,线程竞争很激烈时,CAS会白白浪费很多CPU时间,吞吐量反而没加锁好。
Unsafe还提供了几个对CAS简单封装的方法,用于实现AtomicXXX,可以通过这些代码简单学习下如何使用CAS。
下面这几个方法,返回值都是旧值,不是返回更新之后的值。
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

public final long getAndAddLong(Object o, long offset, long delta) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, v + delta));
    return v;
}

public final int getAndSetInt(Object o, long offset, int newValue) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, newValue));
    return v;
}

public final long getAndSetLong(Object o, long offset, long newValue) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, newValue));
    return v;
}

public final Object getAndSetObject(Object o, long offset, Object newValue) {
    Object v;
    do {
        v = getObjectVolatile(o, offset);
    } while (!compareAndSwapObject(o, offset, v, newValue));
    return v;
}
虽然简单,不过要表达的意思很清楚:CAS无锁操作使用上的重点就是循环,以及配套的volatile。

5、线程调度相关
    void unpark(Object thread)
    void park(boolean isAbsolute, long time)
LockSupport中对这两个方法进行了简单的封装,这里就先不说了,后面再专门看下LockSupport。
下面三个是关于对象锁的,synchronized会使用monitor来实现,因此下面几个可以模拟synchronized的功能,try方法获取不到时会返回false,可以避免线程死等。
    void monitorEnter(Object o)
    void monitorExit(Object o)
    boolean tryMonitorEnter(Object o)


6、内存屏障

    void loadFence():运行时保证程序代码中,在该方法之前的所有读操作,一定在load屏障之前执行完成,x86有现成的指令lfence
    void storeFence():运行时保证程序代码中,在该方法之前的所有
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值