• sun.misc.Unsafe的理解


    1、Unsafe类的作用

    可以直接操作堆外内存,可以随意查看及修改JVM中运行时的数据结构,例如查看和修改对象的成员,Unsafe的操作粒度不是类,而是数据和地址。

    另外,还支持一些CAS原子操作

    2、获取Unsafe的对象

    Unsafe不能直接通过new Unsafe()或者调用Unsafe.getUnsafe()获取,原因如下:

    不能直接new Unsafe,原因是Unsafe被设计成单例模式,构造方法是private的

    不能通过调用Unsafe.getUnsafe()获取,因为getUnsafe被设计成只能从引导类加载器(bootstrap class loader)加载,从getUnsafe的源码中也可以看出来,如下:

    public static Unsafe getUnsafe() {
            //获得调用该方法的class对象
            Class var0 = Reflection.getCallerClass();
            //判断调用该方法的类是否是引导类加载器(bootstrap class loader)
            //如果不是的话,比如由AppClassLoader调用该方法,则抛出SecurityException异常
            if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
                throw new SecurityException("Unsafe");
            } else {
                return theUnsafe;
            }
        }
        public static boolean isSystemDomainLoader(ClassLoader var0) {
            return var0 == null;
        }

    虽然不能通过上边的方法得到Unsafe对象,但Unsafe类中有个私有的静态全局属性theUnsafe(Unsafe实例对象),通过反射,可以获取到该成员属性theUnsafe对应的Field对象,并将其设置为可访问,从而得到theUnsafe具体对象。

    package concurrency;
    
    import java.lang.reflect.Field;
    import sun.misc.Unsafe;
    import sun.reflect.Reflection;
    
    public class Test {
        public static void main(String[] args) throws NoSuchFieldException,
                SecurityException, IllegalArgumentException, IllegalAccessException {
            // 通过反射得到theUnsafe对应的Field对象
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            // 设置该Field为可访问
            field.setAccessible(true);
            // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
            Unsafe unsafe = (Unsafe) field.get(null);
            System.out.println(unsafe);
    
        }
    }

    3、Unsafe类的Api

    allocateInstance方法,不调用构造方法生成对象

    本地方法,功能是生成一个对象实例,但是不会运行该对象的构造方法;由于natUnsafe.cc版本较老,没找到对应的c++实现;

    /** Allocate an instance but do not run any constructor. Initializes the class if it has not yet been. */
        public native Object allocateInstance(Class cls)
            throws InstantiationException;

    例子,利用Unsafe的allocateInstance方法,在未调用构造方法的情况下生成了对象:

    package concurrency;
    
    import java.lang.reflect.Field;
    
    import sun.misc.Unsafe;
    import sun.reflect.Reflection;
    
    class User {
        private String name = "";
        private int age = 0;
    
        public User() {
            this.name = "test";
            this.age = 22;
        }
        
        @Override
        public String toString() {
            return name + ": " + age;
        }
    }
    
    
    public class Test {
        public static void main(String[] args) throws NoSuchFieldException,
                SecurityException, IllegalArgumentException, IllegalAccessException, InstantiationException {
            // 通过反射得到theUnsafe对应的Field对象
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            // 设置该Field为可访问
            field.setAccessible(true);
            // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
            Unsafe unsafe = (Unsafe) field.get(null);
    
            User user = (User) unsafe.allocateInstance(User.class);
            System.out.println(user); //dont invoke constructor, print null: 0
            
            User userFromNormal = new User();
            System.out.println(userFromNormal); //print test: 22
    
        }
    }

    objectFieldOffset方法,返回成员属性在内存中的地址相对于对象内存地址的偏移量

    比较简单,就是返回成员属性内存地址相对于对象内存地址的偏移量,通过该方法可以计算一个对象在内存中的空间大小,方法是通过反射得到它的所有Field(包括父类继承得到的),找出Field中偏移量最大值,然后对该最大偏移值填充字节数即为对象大小;

    关于该方法的使用例子可以看下面的修改内存数据的例子;

    putLong,putInt,putDouble,putChar,putObject等方法,直接修改内存数据(可以越过访问权限)

    这里,还有put对应的get方法,很简单就是直接读取内存地址处的数据,不做举例;

     我们可以举个putLong(Object, long, long)方法详细看下其具体实现,其它的类似,先看Java的源码,没啥好看的,就声明了一个native本地方法:

    三个参数说明下:

    Object o//对象引用
    long offset//对象内存地址的偏移量
    long x//写入的数据
    public native void    putLong(Object o, long offset, long x);

    还是看下natUnsafe.cc中的c++实现吧,很简单,就是计算要写入数据的内存地址,然后写入数据,如下:

    void
    sun::misc::Unsafe::putLong (jobject obj, jlong offset, jlong value)
    {
      jlong *addr = (jlong *) ((char *) obj + offset);//计算要修改的数据的内存地址=对象地址+成员属性地址偏移量
      spinlock lock;//自旋锁,通过循环来获取锁, i386处理器需要加锁访问64位数据,如果是int,则不需要改行代码
      *addr = value;//往该内存地址位置直接写入数据
    }

    如下例子,即使User类的成员属性是私有的且没有提供对外的public方法,我们还是可以直接在它们的内存地址位置处写入数据,并成功;

    import sun.misc.Unsafe;
    
    import java.lang.reflect.Field;
    
    public class User {
        private String name = "wangwp";
        private int age = 12;
        private double height = 1.61;
    
        @Override
        public String toString() {
            return name + "," + age + "," + height;
        }
    
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
            //通过反射得到unsafe对应的field对象
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            //设置field为可访问
            field.setAccessible(true);
            //通过field得到该field对应的具体对象,传入null是因为该field为static的
            Unsafe unsafe = (Unsafe) field.get(null);
    
            User user = new User();
            System.out.println(user);//wangwp,12,1.61
    
            //Class userClass = User.class;
            Class userClass = user.getClass();
    
            Field name = userClass.getDeclaredField("name");
            Field age = userClass.getDeclaredField("age");
            Field height = userClass.getDeclaredField("height");
    
            //直接往内存地址写数据
            unsafe.putObject(user, unsafe.objectFieldOffset(name), "gaogao");
            unsafe.putInt(user, unsafe.objectFieldOffset(age), 23);
            unsafe.putDouble(user, unsafe.objectFieldOffset(height), 1.98);
    
            System.out.println(user);//gaogao,23,1.98
    
        }
    }

    copyMemory、freeMemory

    copyMemory:内存数据拷贝

    freeMemory:用于释放allocateMemory和reallocateMemory申请的内存

     CAS操作的方法,compareAndSwapInt,compareAndSwapLong等

    看下natUnsafe.cc中的c++实现吧,加深理解,其实就是将内存值与预期值作比较,判断是否相等,相等的话,写入数据,不相等不做操作,返回旧数据;

    static inline bool
    compareAndSwap (volatile jint *addr, jint old, jint new_val)
    {
      jboolean result = false;
      spinlock lock;
      if ((result = (*addr == old)))
        *addr = new_val;
      return result;
    }

    J.U.C里原子类就是基于以上CAS操作实现的;

    getLongVolatile/putLongVolatile等等方法

    这类方法使用volatile语义去存取数据,我的理解就是各个线程不缓存数据,直接在内存中读取数据;

    java一些使用Unsafe的类

    • AbstractQueuedSynchronizer即同步器的抽象类,里面实现了线程的阻塞和唤醒:
      LockSupport.park(this)和LockSupport.unpark(this)用于阻塞和唤醒线程。
    • 如何去使用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;

    private volatile long value;
        public final boolean compareAndSet(long expect, long update) {
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

      很好奇compareAndSwapLong到底做了什么操作,首先分析一下这个方法的参数
      compareAndSwapLong(Object obj, long offset, long expect, long update)

        

    从参数表述可以看出,每次修改变量值之前都会比较当前实际值和预期值是否一致,只有一致才会执行值修改,否则do nothing。
    注意到counter声明为volatile,这意味着counter的值每次都是在内存中取,再看compareAndSet方法,每次修改值之前会设置当前字段值为预期值,value值每次取都和预期值相同才会执行。
  • 相关阅读:
    解决:只有 DBA 才能导入由其他 DBA 导出的文件
    查找—顺序查找
    软件测试,想说爱你不容易
    Oracle常用SQL
    排序—直接插入排序
    排序—归并排序
    排序—快速排序
    排序—选择排序
    查找—折半查找
    排序—堆排序
  • 原文地址:https://www.cnblogs.com/cherish010/p/11059019.html
Copyright © 2020-2023  润新知