Unsafe类,全限定名是sun.misc.Unsafe,从名字中我们可以看出来这个类对 普通程序员来说是“危险”的,一般应用开发者不会用到这个类。但是,几乎每个使用 java开发的工具、软件基础设施、高性能开发库都在底层使用了 sun.misc.Unsafe;(比如Netty、Cassandra、Hadoop、Kafka等)
Unsafe类官方并不对外开放,因为Unsafe类支持硬件级别的原子操作,提供了一些绕开JVM的更底层功能,通过它可以提高效率。Unsafe API的大部分方法都是native实现。
Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,一旦能够直接操作内存,这也就意味着
1、不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。
2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就
是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
3、直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。
因此,从上面三个角度来看,虽然在一定程度上提升了效率但是也带来了指针的不安全性。
下面直接上源码~
// final 类,不允许继承。
public final class Unsafe {
private static final Unsafe theUnsafe;
public static final int INVALID_FIELD_OFFSET = -1;
...
public static final int ADDRESS_SIZE;
// 注册native方法,使得Unsafe类可以操作C语言
private static native void registerNatives();
// 构造函数是private的,不允许外部实例化
private Unsafe() {
}
// 初始化方法实现
// 采用单例模式,不是系统加载初始化就会抛出SecurityException异常。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
怎么获取Unsafe ?
这个类尽管里面的方法都是 public 的,但是并没有办法使用它们;
该类功能很强大,涉及到类加载机制,其实例一般情况是获取不到的。只能通过反射来获取Unsafe;
public Unsafe getUnsafe() throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
return unsafe;
}
Unsafe主要功能
普通读写
Unsafe还可以直接在一个内存地址上读写。
通过Unsafe可以读写一个类的属性,即使这个属性是私有的,也可以对这个属性进行读写。
读写一个Object属性的相关方法
# 获取对象obj中,偏移地址为offset的属性值,不受修饰符的限制
public native Object getObject(Object obj, long offset);
# 用于从对象的指定偏移地址处读取一个int。
public native int getInt(Object var1, long var2);
# 用于在对象指定偏移地址处写入一个int。
public native void putInt(Object var1, long var2, int var4);
# 用于从指定内存地址处开始读取一个byte。
public native byte getByte(long var1);
# 用于从指定内存地址写入一个byte。
public native void putByte(long var1, byte var3);
其他的primitive type也有对应的方法。
volatile读写
普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。
# 强制从主内存中获取属性值
public native Object getObjectVolatile(Object o, long offset);
# getIntVolatile方法用于在对象指定偏移地址处volatile读取一个int。
public native int getIntVolatile(Object var1, long var2);
# putIntVolatile方法用于在对象指定偏移地址处volatile写入一个int。
public native void putIntVolatile(Object var1, long var2, int var4);
volatile读写相对普通读写是更加昂贵的,因为需要保证可见性和有序性,而与volatile写入相比putOrderedXX写入代价相对较低,putOrderedXX写入不保证可见性,但是保证有序性,所谓有序性,就是保证指令不会重排序。
有序写入
有序写入只保证写入的有序性,不保证可见性,就是说一个线程的写入不保证其他线程立马可见。
# 将对象var1中,偏移地址为var2的属性(object类型)值改为var4。
# 保证有序性,不保证可见性
public native void putOrderedObject(Object var1, long var2, Object var4);
public native void putOrderedInt(Object var1, long var2, int var4);
public native void putOrderedLong(Object var1, long var2, long var4);
直接内存操作
我们都知道Java不可以直接对内存进行操作,对象内存的分配和回收都是由JVM帮助我们实现的。但是Unsafe为我们在Java中提供了直接操作内存的能力。
# 获取本地指针的大小(单位是byte),通常值为4或者8。常量ADDRESS_SIZE就是调用此方法。
public native int addressSize();
# 获取本地内存的页数,此值为2的幂次方。
public native int pageSize();
# 分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。
public native long allocateMemory(long var1);
# 通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。
public native long reallocateMemory(long var1, long bytes);
# 将给定内存块中的所有字节设置为固定值(通常是0)。
public native void setMemory(long var1, long var3, byte var5);
# 内存复制
public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);
# 清除内存
public native void freeMemory(long var1);
CAS相关
CAS(Compare and Swap 即比较并交换),java并发常用到的一种技术,java.util.concurrent 包完全建立在 CAS 之上,没有 CAS 也就没有此包,可见 CAS 的重要性。(JUC中大量运用了CAS操作,CAS就是JUC的基础)
现在的处理器基本都支持 CAS,只不过不同的厂家的实现不一样罢了。CAS 有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做并返回 false。
Unsafe中提供了int,long和Object的CAS操作
final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
CAS一般用于乐观锁,它在Java中有广泛的应用;
ConcurrentHashMap,ConcurrentLinkedQueue中都有用到CAS来实现乐观锁。
ABA问题:
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了 B,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。
解决:java.util.concurrent 包为了解决这个问题,提供了一个带有标记的原子引用类 ”AtomicStampedReference”,它可以通过控制变量值的版本来保证 CAS 的正确性。不过目前来说这个类比较”鸡肋”,大部分情况下 ABA 问题并不会影响程序并发的正确性,如果需要解决 ABA 问题,使用传统的互斥同步可能会比原子类更加高效。
CAS 看起来很美,但这种操作显然无法涵盖并发下的所有场景
偏移量相关
# 用于获取静态属性Field在对象中的偏移量,读写静态属性时必须获取其偏移量。
public native long staticFieldOffset(Field var1);
# 用于获取非静态属性Field在对象实例中的偏移量,读写对象的非静态属性时会用到这个偏移量。
public native long objectFieldOffset(Field var1);
# 用于返回Field所在的对象。
public native Object staticFieldBase(Field var1);
# 用于返回数组中第一个元素实际地址相对整个数组对象的地址的偏移量。
public native int arrayBaseOffset(Class<?> var1);
# 用于计算数组中第一个元素所占用的内存空间
# 返回数组中元素与元素之间的偏移地址的增量
public native int arrayIndexScale(Class<?> var1);
arrayBaseOffset + arrayIndexScale这两个方法配合使用就可以定位到任何一个元素的地址
由于Java的数组最大值为Integer.MAX_VALUE,使用Unsafe类的内存分配方法可以实现超大数组。
实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。
线程调度
# 释放被park创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的。
public native void unpark(Object var1);
# 阻塞当前线程,一直等到unpark方法被调用。
public native void park(boolean var1, long var2);
public native void monitorEnter(Object var1);
public native void monitorExit(Object var1);
public native boolean tryMonitorEnter(Object var1);
monitorEnter方法和monitorExit方法用于加锁,Java中的synchronized锁就是通过这两个指令来实现的。
// 挂起线程
public static void park(Object blocker) {
Thread t = Thread.currentThread();
// 通过Unsafe的putObject方法设置阻塞当前线程的blocker
setBlocker(t, blocker);
// 通过Unsafe的park方法来阻塞当前线程,注意此方法将当前线程阻塞后;
// 当前线程就不会继续往下走了,直到其他线程unpark此线程
UNSAFE.park(false, 0L);
// 清除blocker
setBlocker(t, null);
}
// 唤醒线程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
看过LockSupport类的都不会陌生;整个并发框架中对线程的挂起操作被封装在LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。
类加载
# 定义一个类,用于动态地创建类。
public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
# 用于动态的创建一个匿名内部类。
public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);
# 用于创建一个类的实例,但是不会调用这个实例的构造方法,如果这个类还未被初始化,则初始化这个类。
# 在对象反序列化的时候很有用,能够重建和设置final字段,而不需要调用构造方法
public native Object allocateInstance(Class<?> var1) throws InstantiationException;
# 用于判断是否需要初始化一个类。
public native boolean shouldBeInitialized(Class<?> var1);
# 用于保证已经初始化过一个类。
public native void ensureClassInitialized(Class<?> var1);
内存屏障
# 保证在这个屏障之前的所有读操作都已经完成。
public native void loadFence();
# 保证在这个屏障之前的所有写操作都已经完成。
public native void storeFence();
# 保证在这个屏障之前的所有读写操作都已经完成。
public native void fullFence();
八卦
在很久之前盛传着这个类将要在jdk9移除,事实上如果移除了那么一大批框架将会消失,比如说赫赫有名的Netty框架。最终jdk9出现的时候也只是对其进行了改进和优化。不过这也再一次说明了这个类的重要地位。