sun.misc.Unsafe中一些常用方法记录
前情摘要
sun公司提供了可以用于直接操作内存的类,这个类就是sun.misc.Unsafe
。因为Java本身是不会涉及到直接操作内存的,Java API也没有提供这些操作,内存管理全部交给虚拟机来做。Sun之所以提供这个类,因为有些功能现有的Java API满足不了,如果没有这个类,可能就没有现在原子类,J.U.C包了,也许也没了各种Concurrent Collection类,可能也没了NIO的堆外内存,所以这个类十分的有用,并且很重要,Sun也没有开放这个类的源代码,并且对它的使用也做了一些限制。
通过反编译看到这个类中,几乎所有的方法都是native修饰的,即使其他非native修饰的,最后也是在调用本类的其他native方法。
Unsafe提供了一个static修饰的静态方法,用来获取这个类的实例,这是一个单例。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
这个getUnsafe()方法并不是直接返回Unsafe的实例,而是做了校验。通过校验直接使用getUnsafe这个方法的类的类加载器是否为Bootstrap类加载器,来做安全检查。由于Java API中的类都是由Bootstrap类加载器加载的,所以是可以直接调用这个方法来获取Unsafe的实例。不过如果要在自己的代码中使用Unsafe,则需要通过反射修改theUnsafe
字段的访问修饰符,然后获取Unsafe的实例。由于Unsafe可以直接操作内存,所以存在很大的风险,使用时需要特别谨慎!
想阅读下关于原子类和J.U.C包下的一些实现,发现这些经常使用的方法,记录下每个方法的具体含义,不然没法理解整个客户端方法的行为。
整个Unsafe类*分为以下几类操作:
- 底层内存信息相关的 比如内存页大小
- 操作对象及其字段 如:实例化对象,获取实例域的偏移量
- 操作类以及静态字段 如:定义一个类,获取静态字段的偏移量
- 操作数据对象
- 同步操作 提供操作监控器和CAS的支持
- 内存操作 主要是内存的分配,复制,销毁等 NIO中用到的多
JUC和原子类以及并发集合中主要用到以下这些方法
Unsafe.putObject()
将一个引用类型的值存入到给定对象的变量中,这个方法的参数列表为putObject(Object o, long offset, Object x)
,这里就是将对象x的引用,存到对象o的偏移量为offset的变量上。要注意这个操作不保证内存可见性,也就是说对对象o的指定字段的更新,并不会在多线程环境下被其他线程发现值的变动。所以提供了一个具有volatile语义的方法putObjectVolatile()
,这个操作具有volatile语义的store语义,并不具备load的语义,如果想在获取时具备load的语义,可以使用getObjectVolatile()
。
Unsafe.park()
此方法主要用于阻塞当前线程,方法的参数为park(boolean absolute, long time)
,如果absolute为false,time为0,则表示一直阻塞,直至unpark方法被调用,或者被中断。如果absolute为false,time不为0,则表示为给定的纳秒过后,中断阻塞,相应的线程可以被系统调度。如果absolute为true,time的单位为毫秒,不过这是一个绝对时间,Epoch Time—— Unix纪元时间 1970.1.1 零时,absolute表示的绝对是指基于这个时间的绝对时间,也就是说park(true, System.currentTimeMillis()+N)
这种写法才是对的,不然输入的任意数字都没有作用的,不会起到阻塞线程的效果。
会将线程一直阻塞,直至以下情况发生:
- 当相应的unpark方法在park方法调用前被调用,park方法调用会被立即返回
- 当相应的unpark方法在park方法后被调用,park方法返回
- 在调用park方法的前后,如果检测到线程已经被设置为中断,则park方法立即返回
- absolute为false并且time不为0,所给的纳秒已经过了
- absolute为true,并且所给的时间(必须是在Epoch纪元时间的基础上,通常取当前距离纪元时间的毫秒数加上希望阻塞时间毫秒数)毫秒已经用完
- 无理由返回
Unsafe.unpark()
将指定的线程从park阻塞状态中恢复过来,方法仅有一个参数unpark(Thraed thread)
,要确保thread对象没有被销毁,也就是要检查不为null
Unsafe.getObjectVolatile()
获取所给对象的所给变量的值,使用volatile语义的load语义,会在实际获取这个值的时候从主存中加载,不会使用CPU缓存中的,总能确保获取到的是有效的值。 getObjectVolatile(Object o, long offset)
Unsafe.getInt()
有三个重载方法getInt(Object o, long offset)
、getInt(long address)
和getIntVolatile(long address)
,都是从指定的位置获取变量的值,只不过第一个的offset是相对于对象o的相对偏移量,第二个address是绝对地址偏移量。如果第一个方法中o为null是,offset也会被作为绝对偏移量。第三个则是带有volatile语义的load读操作。
Unsafe.putInt()
同样有三个在用的重载方法,参数也差不多同putInt
类似,含义则是相反,用于对指定内存地址的变量赋值。
putInt(Object o, long offset, int x)
、putInt(long address)
和putIntVolatile(Object o, long offset, int x)
,最后一个是Volatile版本的putInt方法。
Unsafe.objectFieldOffset()
这个方法的作用是用来获取指定对象的域的偏移量,这个是指这个字段在内存中的位置相对于这个对象在内存中的起始地址的偏移量,也就是隔了多远,有了这个值,后续就能直接定位到这个域的内存地址,然后获取其中的值去操作。注意,这个方法只适用于域为非static修饰的,static修饰的域需要使用Unsafe.staticFieldOffset()
Usage:
long offset = unsafe.objectFieldOffset(Field var)
方法结收一个java.lang.reflect.Field对象。
静态域的偏移量也是一样的使用,方法名不同。
Unsafe.compareAndSwapObject()
以CAS的方式来更新一个引用类型的字段值,如果更新成功则返回true,否则返回false。
Usage:
boolean casResult = unsafe.compareAndSwapObject(object,offset,expected,update);
这个方法本身不会有自旋行为,直接做CAS操作,如果失败立即返回。通常是我们在代码中通过无限循环实现CAS的自旋,并且需要被更新的域一般都是用volatile
修饰的,不然多线程环境下无法保证正确性。
如果想通过此方式来CAS操作静态域的值,第一个参数为静态域所在的Class对象。
Unsafe.compareAndSwapInt()
和上面的方法作用一样,只不过是用来操作int类型的变量
Unsafe.putOrderedInt()
Unsafe.putIntVolatile()
的有序版本/延迟版本,只保证最终将制定的变量更新为新的值。
Unsafe.getAndSetInt()
这个不属于native方法,是用getIntVolatile
和compareAndSwapInt
两个方法组合,对指定的变量设置为新的值,不过会返回设置新值前的旧值,而不是无条件直接设置,在竞争的条件下,这样就需要在循环中不断做CAS。
Unsafe.getAndAddInt()
这个同样不属于native方法,和上面差不多,只不过是在旧值的基础上做了个加新值的操作