Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。
但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。
public class Unsafe { // 单例对象 private static final Unsafe theUnsafe; private Unsafe() { } @CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); // 仅在引导类加载器`BootstrapClassLoader`加载时才合法 if(!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } } }
如何获取Unsafe实例?
1)getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被 引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
java Xbootclasspath/a:${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
2)通过反射获取单例对象theUnsafe。
public class UnsafeInstance { public static Unsafe reflectGetUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { e.printStackTrace(); } return null; } }
再来看看 Unsafe 有什么功能。Unsafe 提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系 统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍。
这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。
// 分配内存, 相当于C++的malloc函数 public native long allocateMemory(long bytes); // 扩充内存 public native long reallocateMemory(long address, long bytes); // 释放内存 public native void freeMemory(long address); // 在给定的内存块中设置值 public native void setMemory(Object o, long offset, long bytes, byte value); // 内存拷贝 public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); // 获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: 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); public native byte getByte(long address); // 为给定地址设置byte类型的值(当且仅当该内存地址为 allocateMemory分配 时,此方法结果才是确定的) public native void putByte(long address, byte x);
通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM 所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机 制统一管理堆内存。与之相对的是堆外内存,存在于JVM管控之外的内存区域,Java 中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。
使用堆外内存的原因
典型应用
DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲 池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、 使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。
下图为DirectByteBuffer构造函数,创建DirectByteBuffer的时候,通过 Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建 Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。
如下源代码释义所示,这部分主要为CAS相关操作的方法。
/** * CAS * @param o 包含要修改field的对象 * @param offset 对象中某field的偏移量 * @param expected 期望值 * @param update 更新值 * @return true | false */ public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
典型应用
如下图所示,AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过 Unsafe的 objectFieldOffset 方法获取。
在 AtomicInteger 中提供的线程安全方法中,通过字段 valueOffset 的值可以定位到 AtomicInteger 对象中value的内存地址,从而可以根据CAS实 现对value字段的原子操作。
下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址 baseAddress=“0x110000”,通过baseAddress+valueOffset 得到value的内存地址 valueAddress=“0x11000c”;然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。
包括线程挂起、恢复、锁机制等方法
// 取消阻塞线程 public native void unpark(Object thread); // 阻塞线程 public native void park(boolean isAbsolute, long time); // 获得对象锁(可重入锁) @Deprecated public native void monitorEnter(Object o); // 释放对象锁 @Deprecated public native void monitorExit(Object o); // 尝试获取对象锁 @Deprecated public native boolean tryMonitorEnter(Object o);
方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现; unpark可以终止一个挂起的线程,使其恢复正常。
典型应用
Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用 LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而 LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。
在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的乐观读锁时,需要遵从如下图用例中使用的模式来确保数据的一致性。
如上图用例所示计算坐标点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中锁状态校验运算发生重排 序导致锁状态校验不准确的问题。
其它方面,大家可以自行查阅…