一,Unsafe使用建议

  1. unsafe有可能在未来几年的jdk版本中移除或者不允许java应用代码使用,这一点可能会导致使用了unsafe的应用无法运行在高版本的jdk中。
  2. unsafe不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是jvm级别的崩溃异常。
  3. unsafe提供的直接内存访问的方法中使用的内存不受jvm管理(无法被gc),需要手动管理,一旦出现疏忽可能成为内存泄漏的源头。

二,unsafe详解

unsafe类中一共有82个public native 修饰的方法,还有几十个基于这个82个方法的其他方法。

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类做了限制,如果是普通的调用的话,他会抛出一个权限异常,只有由引导类加载器加载的类才能使用这个类的方法。最简单的方式是通过反射获取unsafe实例。

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

2.类,对象和变量相关方法

2.1 getObject

通过给定的java变量获取引用值。这里实际上是获取一个java对象o中,获取偏移地址==offset的属性的值。

此方法可以突破修饰符的限制。类似的方法还有getInt,getDouble等。

2.2 putObject

将引用值存储到给定的java变量中,这里实际上是设置一个java对象o中偏移地址为offset的属性的值为x。

此方法可以突破修饰符的限制。类似的方法还有putInt,putDouble等。

2.3 getObjectVolatile

此方法和上面的getObject功能类似,不过附加了volatile,强制从主存中获取属性值。

这个方法要求被使用的属性被volatile修饰,否则和getObject没有区别。

2.4 putObjectVolatile

此方法和上面的putObject功能类似,不过附加了volatile,也就是设置值得时候强制刷新的主存,从而保证这些变更对其他线程可见。

2.5 putOrderedObject

设置o对象中offset偏移地址对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值得改变立马被其他线程看到。

只有在field被volatile修饰的时候期望被修改的时候才会生效

2.6 staticFieldOffset

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

2.7 objectFieldOffset

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

2.8 staticFieldBase

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

2.9 shouldBeInitialized

检测给定的类是否需要初始化。通常需要使用在获取一个类的静态属性的时候(一个类如果没有初始化,他的静态属性也不会初始化)。

2.10 arrayIndexScale

返回数组类型的比例因子(其实就是数据中元素偏移量地址的增量,因为数组中的元素地址是连续的)。

2.11 arrayBaseOffset

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

3.内存管理

3.1 allocateMemory

分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。如果内存块的内容不被初始化,那么它们一般会变成内存垃圾。

生成的本机指针永远不会为零,并将对所有值类型进行对齐。可以通过freelemory方法释放内存块 或者通过reallocatelemory方法调整内存块大小。

bytes值为负数或者过大会抛出IIlegalArgumentException异常,如果系统拒绝分配内存会抛出Out( OfMemoryError异常。

3.2 reallocateMemory

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

3.3 freeMemory

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

3.4 setMemory

将给定内存块中的所有字节设置为固定值(通常是0)。内存块的地址由对象引用o和偏移地址共同决定,如果对象引用o为null,offset就是绝对地址。

第三个参数就是内存块的大小,如果使用allocatelenory进行内存开辟的话,这里的值应该和all ocatelemory的参数一致。value就是设置的固定值,一般为0。

4.多线程同步

4.1 monitorEnter

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

4.2 monitorExit

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

4.3 tryMonitorEnter

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

4.4 compareAndSwapObject

针对Object对象进行CAS操作。即是对应Java变量引用o,原子性地更新o中偏移地址为offset的属性的值为x,当且仅当偏移地址为offset的属性的当前值为expected才会更新成功返回true,否则返回false。

o:目标Java变量引用。

offset:目标Java变量中的目标属性的偏移地址。

expected:目标Java变量中的目标属性的期望的当前值。 x:目标Java变量中的目标属性的目标更新值。

类似的方法有compareAndSwapInt和compareAndSwapLong,在Jdk8中基于CAS扩展出来的方法有getAn daddInt、getAndAddLong、getAndSetInt、getAndSetLong、getAndSet0bject,它们的作用都是:通过CAS设置新的值,返回旧的值。

5.线程的挂起和恢复

5.1 unpark

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

5.2 park

阻塞当前线程直到一个unpark方法出现(被调用)。

参数:isAbsolute true 表示绝对时间,阻塞时间按照纳秒计算。false表示相对时间,阻塞时间按照毫秒计算。

简单理解isAbsolute true时精度更高。

参数:time表示阻塞时间。值为0时表示无限期阻塞,直到有一个线程通过unsafeunpark(当前阻塞线程)才会唤醒。