Java中的Unsafe类详解
- 1. Unsafe 概念
- 2. Unsafe 构造及获取
- 3. 功能和应用
- 3.1 内存管理
- 3.1.1 普通读写
- 3.1.2 volatile 读写
- 3.1.3 有序读写
- 3.1.4 直接操作内存
- 3.2 CAS
- 3.3 偏移量
- 3.4 线程调度
- 3.5 类加载
- 3.6 内存屏障
- 3.7 其他操作
- 4. 潜在风险和挑战
- 5. 最佳实践
- 5.1 使用案例:CAS 操作
- 6. 结论
1. Unsafe 概念
Java 作为一门广泛应用于企业级应用开发的编程语言,为了保障程序的稳定性和安全性,通常限制了开发者对底层内存和硬件的直接访问。然而,Java 中的 Unsafe
类却为开发者提供了一种突破这些限制的方式,让他们可以直接操作内存、线程和对象,同时也引发了一系列潜在的风险和挑战。
2. Unsafe 构造及获取
Unsafe 类使用 final
修饰,不允许继承,且构造函数是 private,使用了饿汉式单例,通过一个静态方法 getUnsafe() 来获取实例。
先看一下源码:
public final class Unsafe {private static final Unsafe theUnsafe;private static native void registerNatives();private Unsafe() {}@CallerSensitivepublic static Unsafe getUnsafe() {Class var0 = Reflection.getCallerClass();if (!VM.isSystemDomainLoader(var0.getClassLoader())) {throw new SecurityException("Unsafe");} else {return theUnsafe;}}
}
在 getUnsafe 方法中对单例模式中的对象获取做了限制,如果是普通的调用会抛出一个 SecurityException 异常。只有由主类加载器加载的类才能调用这个方法。
获取 Unsafe 对象的方式
- 通过 Unsafe.getUnsafe()
Unsafe unsafe = Unsafe.getUnsafe();
- 通过反射来获取
Class<Unsafe> unsafeClass = Unsafe.class; Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Object o = theUnsafe.get(null); Unsafe unsafe1 = (Unsafe) o;
3. 功能和应用
Unsafe 类提供了一些能够绕过 Java 语言安全机制的方法,例如直接操作内存、CAS(比较并交换)操作、分配和释放内存等。这使得开发者可以在某些情况下获得更高的性能,但同时也需要承担更大的风险和责任。
一些用途包括:
- 手动管理内存:开发者可以使用 Unsafe 类手动分配和释放内存,从而实现更精细的内存管理。
- 原子操作:Unsafe 提供了原子操作方法,使开发者可以实现高效的多线程并发控制。
- 绕过安全检查:Unsafe 可以绕过一些 Java 语言层面的安全检查,但这也会导致潜在的安全漏洞。
3.1 内存管理
Unsafe 的内存管理功能主要包括:普通读写、volatile读写、有序读写、直接操作内存等分配内存与释放内存的功能。
3.1.1 普通读写
Unsafe 可以读写一个类的属性,即便这个属性是私有的,也可以对这个属性进行读写。
public native int getInt(Object var1, long var2);public native void putInt(Object var1, long var2, int var4);
getInt 等于从对象的指定偏移地址处读取一个值。putInt等于在对象指定偏移处写入一个值。其他原始类型也提供有对应的方法。此外,Unsafe 的 getByte、putByte 方法提供了直接在一个地址上就行读写的功能。
3.1.2 volatile 读写
普通的读写无法保证可见性和有序性,而 volatile 读写就可以保证;但是相对普通读写更加昂贵。
public native int getIntVolatile(Object var1, long var2);public native void putIntVolatile(Object var1, long var2, int var4);
3.1.3 有序读写
有序写入只保证写入的有序性,不保证可见性,就是说一个线程的写入不保证其他线程立马可见。
而与 volatile 写入相比 putOrderedXX 吸入代价相对较低,putOrderedXX 写入不保证可见性,但是保证有序性,所谓有序性,就是保证指令不会重排序。
3.1.4 直接操作内存
Unsafe 提供了直接操作内存的能力:
public native long allocateMemory(long var1);public native long reallocateMemory(long var1, long var3);public native void setMemory(Object var1, long var2, long var4, byte var6);public void setMemory(long var1, long var3, byte var5) {this.setMemory((Object)null, var1, var3, var5);
}public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);public native void freeMemory(long var1);
也提供了一些获取内存信息的方法:getAddress、addressSize、pageSize
注意:利用 copyMemory 方法可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现 clone 方法,但只能做到对象浅拷贝。
3.2 CAS
Unsafe 类的 CAS 操作作为 Java 的锁机制提供了一种新的解决方法,比如 AtomicInteger 等类都是通过该方法来实现的。compareAndSwap* 方法是原子的,可以避免的繁琐的锁机制,提高代码效率。
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);
CAS 一般用于乐观锁,它在 Java 中有广泛的应用,ReentrantLock、ConcurrentHashMap、ConcurrentLinkedQueue 等都有用到CAS来实现乐观锁。
3.3 偏移量
Unsafe 提供以下方法获取对象的指针,通过对指针进行偏移,不仅可以直接修改指针指向的数据(及时它们是私有的),甚至可以找到JVM已经认定为垃圾、可以进行回收的对象。
// 获取静态属性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);
3.4 线程调度
// 唤醒线程
public native void unpark(Object var1);// 挂起线程
public native void park(boolean var1, long var2);
通过 park 方法将线程挂起,线程将一直阻塞到超时或者中断条件出现。unpark 方法可以终止一个挂起的线程,使其恢复正常。
整个并发框架中对线程的挂起操作被封装在 LockSupport 类中,LockSupport 类中有各种版本 park 方法,但最终都调用了 Unsafe.park() 方法。
3.5 类加载
// 方法定义一个类,用于动态地创建类
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);// 判断是否需要初始化一个类
public native boolean shouldBeInitialized(Class<?> var1);// 保证已经初始化过一个类
public native void ensureClassInitialized(Class<?> var1);
3.6 内存屏障
// 保证在这个屏障之前的所有读操作都已完成
public native void loadFence();// 保证在这个屏障之前的所有写操作都已完成
public native void storeFence();// 保证在这个屏障之前的所有读写操作都已完成
public native void fullFence();
3.7 其他操作
当然,Unsafe 类中还提供了大量其他的方法,比如上面提供的CAS操作,以 AtomicInteger 为例,当我们调用 getAndIncrement、getAndDecrement 等方法时,本质上调用就是 Unsafe 类的 getAndAddInt 方法。
// 返回系统指针的大小。返回值为4(32位系统)或8(64位系统)。
public native int addressSize();// 内存页的大小,此值为2的幂次方
public native int pageSize();
4. 潜在风险和挑战
尽管 Unsafe 类在某些情况下可能提供了极大的灵活性和性能优势,但它也带来了一些严重的潜在问题:
- 内存泄漏:手动管理内存可能导致难以察觉的内存泄漏,从而降低应用的稳定性和性能。
- 安全漏洞:绕过安全检查可能导致潜在的安全漏洞,使应用容易受到恶意攻击。
- 不稳定性:直接操作内存和线程可能导致应用的不稳定性和不可预测的行为。
5. 最佳实践
虽然 Unsafe 类提供了一些强大的功能,但在大多数情况下,开发者应该避免直接使用它。如果确实需要使用 Unsafe,请遵循以下最佳实践:
- 仅在必要时使用:只有在必须绕过 Java 安全机制并获得更高性能时才考虑使用 Unsafe。
- 小心操作:确保在使用 Unsafe 时仔细考虑可能的风险,并采取适当的措施来减少潜在问题。
- 文档和测试:详细记录 Unsafe 使用情况,并进行充分的测试,以确保应用在各种情况下都能够稳定运行。
5.1 使用案例:CAS 操作
下面是一个使用 Unsafe
进行 CAS(比较并交换)操作的简单案例。CAS 是一种常见的并发控制手段,可用于线程安全的更新变量。
import sun.misc.Unsafe;public class CasExample {private static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {valueOffset = unsafe.objectFieldOffset(CasExample.class.getDeclaredField("value"));} catch (NoSuchFieldException e) {throw new Error(e);}}private volatile int value = 0;public void increment() {int current;do {current = unsafe.getIntVolatile(this, valueOffset);} while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));}public int getValue() {return value;}
}
6. 结论
Java 中的 Unsafe 类为开发者提供了一种强大而危险的工具,可以用于在某些特定情况下实现高性能的操作。然而,使用 Unsafe 也需要开发者对风险有清晰的认识,并采取适当的措施来确保应用的稳定性和安全性。
请注意,在使用 Unsafe 时需要格外小心,遵循最佳实践,以免引发不必要的问题和风险。