美文网首页
高性能的秘密:Unsafe

高性能的秘密:Unsafe

作者: rock_fish | 来源:发表于2020-01-03 17:44 被阅读0次

    便用边整理!!!

    获取Unsafte

    在Unsafe类中有一个成员变量theUnsafe,因此我们可以通过反射将private单例实例的accessible设置为true,然后通过Field的get方法获取,

    Unsafe开发时通过反射来获取

    private static Unsafe reflectGetUnsafe() {
        try {
          // 通过反射得到theUnsafe对应的Field对象
          Field field = Unsafe.class.getDeclaredField("theUnsafe");
          // 设置该Field为可访问
          field.setAccessible(true);
          // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
          return (Unsafe) field.get(null);
        } catch (Exception e) {
          log.error(e.getMessage(), e);
          return null;
        }
    }
    

    不调用构造方法生成对象

    利用Unsafe的allocateInstance方法,不调用构造方法就能生成对象

    MyClass myclass = (MyClass) unsafe.allocateInstance(MyClass.class);
    

    内存屏障

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

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

    典型应用

    在Java 8中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不一致问题,所以当使用StampedLock的乐观读锁时,需要遵从如下图用例中使用的模式来确保数据的一致性。

    image

    如上图用例所示计算坐标点Point对象,包含点移动方法move及计算此点到原点的距离的方法distanceFromOrigin。在方法distanceFromOrigin中,首先,通过tryOptimisticRead方法获取乐观读标记;然后从主内存中加载点的坐标值 (x,y);而后通过StampedLock的validate方法校验锁状态,判断坐标点(x,y)从主内存加载到线程工作内存过程中,主内存的值是否已被其他线程通过move方法修改,如果validate返回值为true,证明(x, y)的值未被修改,可参与后续计算;否则,需加悲观读锁,再次从主内存加载(x,y)的最新值,然后再进行距离计算。其中,校验锁状态这步操作至关重要,需要判断锁状态是否发生改变,从而判断之前copy到线程工作内存中的值是否与主内存的值存在不一致。

    下图为StampedLock.validate方法的源码实现,通过锁标记与相关常量进行位运算、比较来校验锁状态,在校验逻辑之前,会通过Unsafe的loadFence方法加入一个load内存屏障,目的是避免上图用例中步骤②和StampedLock.validate中锁状态校验运算发生重排序导致锁状态校验不准确的问题。

    image

    读写操作

    修改属性值

    Field f = myclass.getClass().getDeclaredField("XXX");
    unsafe.putInt(myclass, unsafe.objectFieldOffset(f), 1); 
    

    无内存屏障的读写操作:

    //A plain store (no ordering/fences) of an element to a given offset
    //获得给定对象的指定地址偏移量的值,与此类似操作还有: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);
    

    带内存屏障的读写操作:
    lazy set是使用Unsafe.putOrderedXXX方法,会前置一个store-store屏障,也许你会为问不用putOrderedXXX那之前用什么?答案是putXXXVolitaile方法,这个方法具有Volatile语意,也就是store-load屏障。收益是修改后其他线程回立即看到修改的值,但代价是store-store接近2-3倍的耗时,store-store的劣势是纳秒级的延迟。

    //A volatile load (load + LoadLoad barrier) of an element from a given offset.
    //从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
    public native Object getObjectVolatile(Object o, long offset);
    
    //存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
    public native void putObjectVolatile(Object o, long offset, Object x);
    
    -----------------
    
    //An ordered store(store + StoreStore barrier) of an element to a given offset
    //有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效
    public native void putOrderedObject(Object o, long offset, Object x);
    

    计算Java对象大小

    有两种计算Java对象大小的方式。

    1. 通过java.lang.instrument.Instrumentation的getObjectSize(obj)直接获取对象的大小;
    2. 通过sun.misc.Unsafe对象的objectFieldOffset(field)等方法结合反射来计算对象的大小。

    通过Unsafe获取Java对象大小的基本思路如下:

    1. 通过反射获得一个类的Field;
    2. 通过Unsafe的objectFieldOffset()获得每个Field的off。Set;
    3. 对Field进行遍历,取得最大的offset,然后加上这个field的长度,再加上Padding对齐。

    这边不写代码了,想要了解的同学可以看一下这个

    数组相关的操作

    Arrays和Java别的对象一样,都有一个对象头,它是存储在实际的数据前面的。
    想通过sun.misc.Unsafe来访问数组的数据,需要两个东西:

    1. 数组对象里数据的偏移量,即对象头的长度:
      通过unsafe.arrayBaseOffset(T[].class)方法来获取到,这里T是数组元素的类型。

    2. 拷贝的元素在数组数据里的偏移量,即数组元素的大小✖️下标:
      数组元素的大小arrayScale可以通过unsafe.arrayIndexScale(T[].class)
      方法获取到。这也就是说要访问类型为T的第N个元素的话,你的偏移量offset应该是arrayOffset+N✖️arrayScale。

    分配一个long数组,然后更新它里面的几个字节。把最后一个元素更新成-1(16进制的话是0xFFFF FFFF FFFF FFFF),然再逐个清除这个元素的所有字节。

    private static final long longArrayOffset = unsafe.arrayBaseOffset(long[].class);
    private static final long longArrayScale= unsafe.arrayIndexScale(long[].class);//8
    
    final long[] ar = new long[5];
    final int index = ar.length - 1;
    ar[ index ] = -1; //FFFF FFFF FFFF FFFF
    System.out.println( "Before change = " + Long.toHexString( ar[ index ] ));
    
    for ( long i = 0; i < 8; ++i )
    {
        // ar : 数组地址,
        // longArrayOffset + longArrayScale * index + i :偏移量
        //(byte) 0 : 目标值
        unsafe.putByte( ar, longArrayOffset + longArrayScale * index + i, (byte) 0);
        System.out.println( "After change: i = " + i + ", val = "  +  Long.toHexString( ar[ index ] ));
    }
    

    暂停、唤醒线程

    //判断当前线程的信号,如果是可运行信号就执行,如果不是就挂起当前线程 直到unpark当前线程给出可运行信号后,当前线程被激活并继续执行。

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

    //可理解为一个boolean 类型的 线程可运行的信号。注意是boolean类型,没有计数效果,

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

    Java并发中的应用

    在Java并发中会用到CAS操作,对应于Unsafe类中的compareAndSwapInt,compareAndSwapLong等。下面的例子就是使用Unsafe实现的无所数据结构。

    class LongValue {
    
           private volatile long counter = 0;
    
           private Unsafe unsafe;
    
           private long offset;
    
           public LongValue() throws Exception {
    
               unsafe = getUnsafe();
    
               offset = unsafe.objectFieldOffset(LongValue.class.getDeclaredField("counter"));
    
           }
    
           public void increment() {
    
               long before = counter;
    
               while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
    
                   before = counter;
    
               }
    
           }
    
           public long getCounter() {
    
               return counter;
    
           }
    
       }
    
    

    看一下Java中AtomicLong的实现,下面摘出来一部分。可以看到该类在加载的时候将value的偏移位置计算出来,然后在compareAndSet等方法中使用Unsafe中的CAS操作进行替换,这样的无锁操作可以大大提高效率。

    
    public class AtomicLong extends Number implements java.io.Serializable {
    
        private static final long serialVersionUID = 1927816293512124184L;
    
        // setup to use Unsafe.compareAndSwapLong for updates
    
        private static final Unsafe unsafe = Unsafe.getUnsafe();
    
        private static final long valueOffset;
    
        ...
    
        static {
    
            try {
    
                valueOffset = unsafe.objectFieldOffset
    
                    (AtomicLong.class.getDeclaredField("value"));
    
            } catch (Exception ex) { throw new Error(ex); }
    
        }
    
        private volatile long value;
    
        ...
    
        public final boolean compareAndSet(long expect, long update) {
    
            return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    
        }
    
        ...
    
    

    其他使用方式

    1. 通过Unsafe的defineClass可以动态加载Class;
    2. 通过Unsafe的copyMemoryfreeMemory等可以实现内存的复制与释放,如果我们知道了对象的大小,利用arrayBaseOffsetcopyMemory可以完成对象的浅拷贝。

    感谢你们:


    这个知识点很全面->【基本功】Java魔法类:Unsafe应用解析
    Unsafe类初探
    Unsafe类源码解析
    Java并发编程-无锁CAS与Unsafe类及其并发包Atomic
    java并发专题 这个系列要看一看

    相关文章

      网友评论

          本文标题:高性能的秘密:Unsafe

          本文链接:https://www.haomeiwen.com/subject/zjvnictx.html