Unsafe入门讲解
概述
作为一个8年多Javaer,曾无数次看到Unsafe这个类,但一直没有去翻过源码,此为背景。
借助于IDEA查看JDK源码,却发现有两个Unsafe类:
- sun.misc.Unsafe
- jdk.internal.misc.Unsafe
jdk.internal.misc.Unsafe和sun.misc.Unsafe
作为JDK中的两个不同类,它们提供一些底层的、非标准化的功能,用于直接操作内存和线程。
sun.misc.Unsafe
最初引入的类,提供一些低级别的操作,如直接内存访问、线程调度、CAS(Compare-And-Swap)操作等。功能强大,但也非常危险,绕过Java的安全机制,直接操作内存,容易导致崩溃或安全漏洞。
在早期的JDK中,许多框架和库(如juc包中的类)依赖于sun.misc.Unsafe
来实现高效的并发控制和内存操作。然而,sun.misc.Unsafe
是一个非标准API,并不是Java标准库的一部分,使用它的代码在不同的JDK版本之间可能不兼容。为了减少这种不兼容性,JDK开发者逐渐将对sun.misc.Unsafe
的依赖移除或替换。
jdk.internal.misc.Unsafe
为了应对sun.misc.Unsafe
的非标准化问题,JDK 9引入模块化系统(Project Jigsaw),并将一些内部API移动到jdk.internal
包中。jdk.internal.misc.Unsafe
是sun.misc.Unsafe
的替代品,提供类似功能,但位于一个内部的、更明确的命名空间中。
区别:
- 命名空间和访问控制:
jdk.internal.misc.Unsafe
位于jdk.internal
模块中,使用更严格的访问控制。只有在明确声明模块依赖的情况下才能访问,而sun.misc.Unsafe
则是一个开放的、非标准API - API变化:虽然两者提供的功能类似,但它们的API可能会有所不同。随着JDK的发展,
jdk.internal.misc.Unsafe
可能会引入新的方法或对现有方法进行调整,以适应内部需求 - 未来发展方向:
jdk.internal.misc.Unsafe
是JDK团队为了模块化和安全性而进行的改进。未来,更多内部功能可能会转移到jdk.internal
命名空间中,而sun.misc.Unsafe
则可能逐渐被废弃或减少使用。
参考如下截图,可知sun.misc.Unsafe
位于jdk.unsupported
包下,不再建议使用:
下文的源码分析都是基于jdk.internal.misc.Unsafe
。
native
查看源码,不难发现不管是public还是private方法,很多方法都标记有native关键字,即所谓的本地方法(Native Method),且这些方法和接口定义很相似,只是定义一个方法签名,没有任何实现,如:
private static native void registerNatives();
static {
registerNatives();
}
@IntrinsicCandidate
public native int getInt(Object o, long offset);
private native Object staticFieldBase0(Field f);
可将本地方法看作是Java中使用其他编程语言(实际上就是C语言)编写的方法,具体的实现则交给JVM。
实例化
Unsafe提供静态方法getUnsafe
用于获取Unsafe实例:
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() {
return theUnsafe;
}
参考方法的注释,开发者需要确保谨慎地安全地使用Unsafe实例。
功能
Unsafe类提供的功能可大致被分为下面7类:
- 线程调度
- CAS操作
- 数据操作
- 内存操作
- 内存屏障
- Class操作
- 其他
另外,有8种基础数据类型:int、byte、short、char、boolean、long、float、double。
线程调度
说到Unsafe提供的方法,最先想到的可能就是park和unpark,两个方法的签名如下:
@IntrinsicCandidate
public native void park(boolean isAbsolute, long time);
@IntrinsicCandidate
public native void unpark(Object thread);
unpark用于解除对park上阻塞的给定线程的阻塞;如果未阻塞,则导致后续对Park的调用不阻塞。
park方法有两个参数,boolean类型,以及表示纳秒的时间。park方法之所以放在Unsafe类中是因为有unpark在。使用场景:
- 用于阻塞当前线程,当发生平衡unpark时返回
- 平衡unpark已经发生
- 线程被中断
- isAbsolute为false,且时间不为零,则给定时间纳秒已过去
- isAbsolute为true,则为给定截止时间自纪元过去以来的毫秒数
- 其他不明原因
LockSupport里面的park方法就是基于Unsafe.park
来实现的:
private static final Unsafe U = Unsafe.getUnsafe();
private static final long PARKBLOCKER = U.objectFieldOffset(Thread.class, "parkBlocker");
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
try {
if (t.isVirtual()) {
// 虚拟线程
VirtualThreads.park();
} else {
U.park(false, 0L);
}
} finally {
setBlocker(t, null);
}
}
private static void setBlocker(Thread t, Object arg) {
U.putReferenceOpaque(t, PARKBLOCKER, arg);
}
CAS操作
compareAndSetReference
compareAndExchangeReference
compareAndExchangeReferenceAcquire
compareAndExchangeReferenceRelease
weakCompareAndSetReferencePlain
weakCompareAndSetReferenceAcquire
weakCompareAndSetReferenceRelease
weakCompareAndSetReference
共8个方法,然后对int等8种数据类型,各有8个方法。
数据操作
getAndAddInt:以原子方式将给定值添加到给定对象 o 内给定偏移量处的字段或数组元素的当前值
getAndAddIntRelease
getAndAddIntAcquire
类似地,对除int外的其他7种数据类型,各有3个方法。
getAndSetReference
getAndSetReferenceRelease
getAndSetReferenceAcquire
类似地,对int等8种数据类型,各有3个方法。
位运算主要有三种:或or、与and、异或xor,以boolean+or
为例,有如下3个方法:
getAndBitwiseOrBoolean
getAndBitwiseOrBooleanRelease
getAndBitwiseOrBooleanAcquire
针对于运算有三个方法
getAndBitwiseAndBoolean
getAndBitwiseAndBooleanRelease
getAndBitwiseAndBooleanAcquire
针对异或运算也有三个方法
getAndBitwiseXorBoolean
getAndBitwiseXorBooleanRelease
getAndBitwiseXorBooleanAcquire
值得注意的是,这3种位运算可适用于:int、byte、short、char、boolean、long。而对float、double除外。
getReferenceVolatile
putReferenceVolatile
共2个方法,然后对int等8种数据类型,各有2个方法。
getReferenceAcquire
putReferenceRelease
getReferenceOpaque
putReferenceOpaque
共4个方法,对int等8种数据类型,各有3个方法。
内存操作
Unsafe共提供7个方法:
- allocateMemory:分配新的本地空间
- reallocateMemory:重新调整内存空间的大小
- setMemory:有两个重载方法,将内存设置为指定值
- copyMemory:有两个重载方法,内存拷贝
- copySwapMemory:有两个重载方法,将所有元素从一个内存块复制到另一块,无条件地动态字节交换元素
- freeMemory:清除内存
- writebackMemory:
内存屏障
Unsafe共提供5个方法:
- loadFence:内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
- storeFence:内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
- fullFence:内存屏障,禁止load、store操作重排序
- loadLoadFence:
- storeStoreFence
源码:
@IntrinsicCandidate
public final void loadFence() {
// If loadFence intrinsic is not available, fall back to full fence.
fullFence();
}
@IntrinsicCandidate
public final void storeFence() {
// If storeFence intrinsic is not available, fall back to full fence.
fullFence();
}
@IntrinsicCandidate
public native void fullFence();
public final void loadLoadFence() {
loadFence();
}
@IntrinsicCandidate
public final void storeStoreFence() {
// If storeStoreFence intrinsic is not available, fall back to storeFence.
storeFence();
}
class操作
Unsafe共提供7个方法:
- objectFieldOffset:获取静态属性的偏移量
- staticFieldOffset:获取静态属性的偏移量
- staticFieldBase:获取静态属性的对象指针
- arrayBaseOffset:
- arrayIndexScale:
- defineClass:让JVM定义一个类而不进行安全检查,类加载器和保护域来自调用者的类。允许程序在运行时动态地创建一个类
- allocateUninitializedArray:
- shouldBeInitialized:判断类是否需要初始化(用于获取类的静态属性前进行检测)
- ensureClassInitialized:
public Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain domain)
从class文件构造实例类,然后利用反射机制获取该类里的方法,然后使用invoke方法执行方法:
private static final Unsafe U = Unsafe.getUnsafe();
private static void testDefineClass() {
String fileName = "E:\\Student.class";
File file = new File(fileName);
try (FileInputStream fis = new FileInputStream(file)) {
byte[] content = new byte[(int) file.length()];
fis.read(content);
Class<?> clazz = U.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object name = clazz.getMethod("getName").invoke(o, null);
log.info(age);
} catch (Exception e) {
log.error("");
}
}
其他
其他不好归类的方法:
- addressSize:返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)
- pageSize:返回内存页大小,此值永远都为2的幂次方
- isBigEndian:是否大端字节序
- getLoadAverage:获取分配给可用处理器的系统运行队列中不同时间段内的平均负载。此方法检索给定的nelem样本并分配给给定的loadavg数组的元素。系统最多施加3个样本,分别代表过去1、5和15分钟的平均值
- invokeCleaner:基于给定的直接字节缓冲区清理器触发清理动作
- unalignedAccess:如果平台能够在与所访问的基本类型的类型不对齐的地址处执行访问,则返回true,否则返回false
- getLongUnaligned:内存对齐操作,适用于:int、short、char
- putLongUnaligned:内存对齐操作,适用于:int、short、char、
public int getLoadAverage(double[] loadavg, int nelems) {
if (nelems < 0 || nelems > 3 || nelems > loadavg.length) {
throw new ArrayIndexOutOfBoundsException();
}
return getLoadAverage0(loadavg, nelems);
}
private native int getLoadAverage0(double[] loadavg, int nelems);
Int
以int这个数据类型为对象,有如下方法:
- getInt
- putInt
- compareAndSetInt
- compareAndExchangeInt
- compareAndExchangeIntAcquire
- compareAndExchangeIntRelease
- weakCompareAndSetIntPlain
- weakCompareAndSetIntAcquire
- weakCompareAndSetIntRelease
- weakCompareAndSetInt
- getIntVolatile
- putIntVolatile
- getIntAcquire
- putIntRelease
- getIntOpaque
- putIntOpaque
- getAndAddInt
- getAndAddIntRelease
- getAndAddIntAcquire
- getAndSetInt
- getAndSetIntRelease
- getAndSetIntAcquire
- getAndBitwiseOrInt
- getAndBitwiseOrIntRelease
- getAndBitwiseOrIntAcquire
- getAndBitwiseAndInt
- getAndBitwiseAndIntRelease
- getAndBitwiseAndIntAcquire
- getAndBitwiseXorInt
- getAndBitwiseXorIntRelease
- getAndBitwiseXorIntAcquire
- getIntUnaligned
- putIntUnaligned
Object
还有几个自JDK 12版本被标记为@Deprecated且会移除,用于操作Object的19方法:
- getObject
- getObjectVolatile
拓展
Compare-And-Swap和Compare-And-Set
在JVM中,CAS操作主要指Compare-And-Swap,而在Java并发包中,使用的是Compare-And-Set。
Compare-And-Swap是一个底层的原子操作指令,用于实现无锁并发数据结构。CAS是一个硬件层面的原子指令,执行以下步骤:
- 比较一个内存位置的值是否与给定的预期值相等
- 如果相等,则将该内存位置的值更新为一个新的值
- 返回这个内存位置的原始值
CAS的原子性由硬件保证,因此在多线程环境中非常高效,避免使用锁带来的开销和复杂性。
Compare-And-Set
是juc包中的Atomic类使用的方法,与Compare-And-Swap类似,但是一个更高层的抽象,提供给Java开发者使用,通常用于实现乐观锁等无锁算法。
这个方法会:
- 比较当前值是否等于预期值(expect)
- 如果相等,则将当前值更新为新的值(update)
- 返回一个布尔值,指示更新是否成功
区别
- 层次:Compare-And-Swap是底层的原子操作,通常在硬件指令级别实现。Compare-And-Set是Java中使用的高层抽象,依赖底层的Compare-And-Swap实现
- 语法和使用:Compare-And-Swap是一种概念(理念),可在多种编程语言和平台上实现。Compare-And-Set是Java特有的,封装在Atomic类中的具体方法
- 返回值:Compare-And-Swap通常返回内存位置的原始值。Compare-And-Set返回一个布尔值,指示操作是否成功
注解
阅读源码时会发现很多注解,如:
- @IntrinsicCandidate
- @ForceInline
TODO,有待后续进一步学习。
虚拟线程
更轻量级的线程,JDK 19版本引入的新特性,在后续版本里持续优化。
TODO,待学习。