一、Unsafe的使用建议

使用Unsafe要注意以下几个问题:

  • 1、Unsafe有可能在未来的Jdk版本移除或者不允许Java应用代码使用,这一点可能导致使用了Unsafe的应用无法运行在高版本的Jdk。
  • 2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
  • 3、Unsafe提供的直接内存访问的方法中使用的内存不受JVM管理(无法被GC),需要手动管理,一旦出现疏忽很有可能成为内存泄漏的源头。

    二、Unsafe详解

    Unsafe中一共有82个public native修饰的方法,还有几十个基于这82个public native方法的其他方法。当然,方便讲解我去掉了一些我认为我这辈子都用不上的API

1、初始化

  1. private static native void registerNatives();
  2. static {
  3. registerNatives();
  4. sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
  5. }
  6. private Unsafe() {}
  7. private static final Unsafe theUnsafe = new Unsafe();
  8. @CallerSensitive
  9. public static Unsafe getUnsafe() {
  10. Class<?> caller = Reflection.getCallerClass();
  11. if (!VM.isSystemDomainLoader(caller.getClassLoader()))
  12. throw new SecurityException("Unsafe");
  13. return theUnsafe;
  14. }

新建一个Unsafe实例命名为theUnsafe,通过静态方法getUnsafe()获取,获取的时候需要做权限判断。由此可见,Unsafe使用了单例设计(可见构造私有化了)。Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器(BootStrap classLoader)加载的类才能调用这个类中的方法。最简单的使用方式是基于反射获取Unsafe实例。

  1. Field f = Unsafe.class.getDeclaredField("theUnsafe");
  2. f.setAccessible(true);
  3. Unsafe unsafe = (Unsafe) f.get(null);

2、类、对象和变量相关方法

  1. getObject

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

  1. putObject

将引用值存储到给定的Java变量中。这里实际上是设置一个Java对象o中偏移地址为offset的属性的值为x,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有putInt、putDouble等等。

  1. getObjectVolatile

此方法和上面的getObject功能类似,不过附加了’volatile’加载语义,也就是强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和getObject方法相同。

  1. putObjectVolatile

此方法和上面的putObject功能类似,不过附加了’volatile’加载语义,也就是设置值的时候强制更新到主存,从而保证这些变更对其他线程是可见的。类似的方法有putIntVolatile、putDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和putObject方法相同。

  1. putOrderedObject

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

  1. staticFieldOffset

返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。注意:这个方法仅仅针对静态属性,使用在非静态属性上会抛异常。

  1. objectFieldOffset

返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。注意:这个方法仅仅针对非静态属性,使用在静态属性上会抛异常。

  1. staticFieldBase

返回给定的静态属性的位置,配合staticFieldOffset方法使用。实际上,这个方法返回值就是静态属性所在的Class对象的一个内存快照。

  1. arrayIndexScale

返回数组类型的比例因子(其实就是数据中元素偏移地址的增量,因为数组中的元素的地址是连续的)。Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。

  1. arrayBaseOffset

返回数组类型的第一个元素的偏移地址(基础偏移地址)。如果arrayIndexScale方法返回的比例因子不为0,你可以通过结合基础偏移地址和比例因子访问数组的所有元素。

3、内存管理

  1. allocateMemory

分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。如果内存块的内容不被初始化,那么它们一般会变成内存垃圾。生成的本机指针永远不会为零,并将对所有值类型进行对齐。可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。bytes值为负数或者过大会抛出IllegalArgumentException异常,如果系统拒绝分配内存会抛出OutOfMemoryError异常。

  1. reallocateMemory

通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。bytes值为负数或者过大会抛出IllegalArgumentException异常,如果系统拒绝分配内存会抛出OutOfMemoryError异常

  1. freeMemory

释放通过allocateMemory方法申请的内存

  1. setMemory

将给定内存块中的所有字节设置为固定值(通常是0)。内存块的地址由对象引用o和偏移地址共同决定,如果对象引用o为null,offset就是绝对地址。第三个参数就是内存块的大小,如果使用allocateMemory进行内存开辟的话,这里的值应该和allocateMemory的参数一致。value就是设置的固定值,一般为0。

4、多线程同步

  1. monitorEnter

锁定对象,必须通过monitorExit方法才能解锁。此方法经过实验是可以重入的,也就是可以多次调用,然后通过多次调用monitorExit进行解锁。

  1. monitorExit

解锁对象,前提是对象必须已经调用monitorEnter进行加锁,否则抛出IllegalMonitorStateException异常。

  1. tryMonitorEnter

尝试锁定对象,如果加锁成功返回true,否则返回false。必须通过monitorExit方法才能解锁。

  1. compareAndSwapObject

针对Object对象进行CAS操作。即是对应Java变量引用o,原子性地更新o中偏移地址为offset的属性的值为x,当且仅的偏移地址为offset的属性的当前值为expected才会更新成功返回true,否则返回false。
o:目标Java变量引用。
offset:目标Java变量中的目标属性的偏移地址。
expected:目标Java变量中的目标属性的期望的当前值。
x:目标Java变量中的目标属性的目标更新值。
类似的方法有compareAndSwapInt和compareAndSwapLong,在Jdk8中基于CAS扩展出来的方法有getAndAddInt、getAndAddLong、getAndSetInt、getAndSetLong、getAndSetObject,它们的作用都是:通过CAS设置新的值,返回旧的值。

5、线程的挂起和恢复

  1. unpark

恢复线程,必须制定要恢复的线程thread。

  1. park

阻塞当前线程直到一个unpark方法出现(被调用)。
参数:isAbsolute true 表示绝对时间,阻塞时间按照纳秒计算。false表示相对时间,阻塞时间按照毫秒计算。
简单理解 isAbsolute true时 精度更高。
参数:time 表示阻塞时间。值为0时表示无限期阻塞,直到有一个线程通过 unsafe.unpark(当前阻塞线程) 才会唤醒。