本文整理完善自下面这两篇优秀的文章:Java 魔法类:Unsafe 应用解析 - 美团技术团队 -2019Java 双刃剑之 Unsafe 类详解 - 码农参上 - 2021
本文整理完善自下面这两篇优秀的文章:
Java 魔法类:Unsafe 应用解析 - 美团技术团队 -2019
Java 魔法类:Unsafe 应用解析 - 美团技术团队 -2019
Java 双刃剑之 Unsafe 类详解 - 码农参上 - 2021
Java 双刃剑之 Unsafe 类详解 - 码农参上 - 2021
阅读过 JUC 源码的同学,一定会发现很多并发工具类都调用了一个叫做 Unsafe 的类。
Unsafe
那这个类主要是用来干什么的呢?有什么使用场景呢?这篇文章就带你搞清楚!
Unsafe 介绍
Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。
Unsafe
sun.misc
Unsafe
Unsafe
Unsafe
另外,Unsafe 提供的这些功能的实现需要依赖本地方法(Native Method)。你可以将本地方法看作是 Java 中使用其他编程语言编写的方法。本地方法使用 native 关键字修饰,Java 代码中只是声明方法头,具体的实现则交给 本地代码。
Unsafe
native

为什么要使用本地方法呢?
需要用到 Java 中不具备的依赖于操作系统的特性,Java 在实现跨平台的同时要实现对底层的控制,需要借助其他语言发挥作用。
对于其他语言已经完成的一些现成功能,可以使用 Java 直接调用。
程序对时间敏感或对性能要求非常高时,有必要使用更加底层的语言,例如 C/C++甚至是汇编。
在 JUC 包的很多并发工具类在实现并发机制时,都调用了本地方法,通过它们打破了 Java 运行时的界限,能够接触到操作系统底层的某些功能。对于同一本地方法,不同的操作系统可能会通过不同的方式来实现,但是对于使用者来说是透明的,最终都会得到相同的结果。
Unsafe 创建
sun.misc.Unsafe 部分源码如下:
sun.misc.Unsafe
public final 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;
}
}
}
public final 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 类为一单例实现,提供静态方法 getUnsafe 获取 Unsafe 实例。这个看上去貌似可以用来获取 Unsafe 实例。但是,当我们直接调用这个静态方法的时候,会抛出 SecurityException 异常:
Unsafe
getUnsafe
Unsafe
Unsafe
SecurityException
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)
为什么 public static 方法无法被直接调用呢?
public static
这是因为在 getUnsafe 方法中,会对调用者的 classLoader 进行检查,判断当前类是否由 Bootstrap classLoader 加载,如果不是的话那么就会抛出一个 SecurityException 异常。也就是说,只有启动类加载器加载的类才能够调用 Unsafe 类中的方法,来防止这些方法在不可信的代码中被调用。
getUnsafe
classLoader
Bootstrap classLoader
SecurityException
为什么要对 Unsafe 类进行这么谨慎的使用限制呢?
Unsafe 提供的功能过于底层(如直接访问系统内存资源、自主管理内存资源等),安全隐患也比较大,使用不当的话,很容易出现很严重的问题。
Unsafe
如若想使用 Unsafe 这个类的话,应该如何获取其实例呢?
Unsafe
这里介绍两个可行的方案。
1、利用反射获得 Unsafe 类中已经实例化完成的单例对象 theUnsafe。
theUnsafe
private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
2、从 getUnsafe 方法的使用限制条件出发,通过 Java 命令行命令 -Xbootclasspath/a 把调用 Unsafe 相关方法的类 A 所在 jar 包路径追加到默认的 bootstrap 路径中,使得 A 被引导类加载器加载,从而通过 Unsafe.getUnsafe 方法安全的获取 Unsafe 实例。
getUnsafe
-Xbootclasspath/a
Unsafe.getUnsafe
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
Unsafe 功能
概括的来说,Unsafe 类实现功能可以被分为下面 8 类:
Unsafe
内存操作
内存屏障
对象操作
数据操作
CAS 操作
线程调度
Class 操作
系统信息
内存操作
介绍
如果你是一个写过 C 或者 C++ 的程序员,一定对内存操作不会陌生,而在 Java 中是不允许直接对内存进行操作的,对象内存的分配和回收都是由 JVM 自己实现的。但是在 Unsafe 中,提供的下列接口可以直接进行内存操作:
Unsafe
//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
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);
//清除内存
public native void freeMemory(long address);
//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
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);
//清除内存
public native void freeMemory(long address);
使用下面的代码进行测试:
private void memoryTest() {
int size = 4;
// 1. 分配初始内存
long oldAddr = unsafe.allocateMemory(size);
System.out.println("Initial address: " + oldAddr);
// 2. 向初始内存写入数据
unsafe.putInt(oldAddr, 16843009); // 写入 0x01010101
System.out.println("Value at oldAddr: " + unsafe.getInt(oldAddr));
// 3. 重新分配内存
long newAddr = unsafe.reallocateMemory(oldAddr, size * 2);
System.out.println("New address: " + newAddr);
// 4. reallocateMemory 已经将数据从 oldAddr 拷贝到 newAddr
// 所以 newAddr 的前4个字节应该和 oldAddr 的内容一样
System.out.println("Value at newAddr (first 4 bytes): " + unsafe.getInt(newAddr));
// 关键:之后所有操作都应该基于 newAddr,oldAddr 已失效!
try {
// 5. 在新内存块的后半部分写入新数据
unsafe.putInt(newAddr + size, 33686018); // 写入 0x02020202
// 6. 读取整个8字节的long值
System.out.println("Value at newAddr (full 8 bytes): " + unsafe.getLong(newAddr));
} finally {
// 7. 只释放最后有效的内存地址
unsafe.freeMemory(newAddr);
// 如果尝试 freeMemory(oldAddr),将会导致 double free 错误!
}
}
private void memoryTest() {
int size = 4;
// 1. 分配初始内存
long oldAddr = unsafe.allocateMemory(size);
System.out.println("Initial address: " + oldAddr);
// 2. 向初始内存写入数据
unsafe.putInt(oldAddr, 16843009); // 写入 0x01010101
System.out.println("Value at oldAddr: " + unsafe.getInt(oldAddr));
// 3. 重新分配内存
long newAddr = unsafe.reallocateMemory(oldAddr, size * 2);
System.out.println("New address: " + newAddr);
// 4. reallocateMemory 已经将数据从 oldAddr 拷贝到 newAddr
// 所以 newAddr 的前4个字节应该和 oldAddr 的内容一样
System.out.println("Value at newAddr (first 4 bytes): " + unsafe.getInt(newAddr));
// 关键:之后所有操作都应该基于 newAddr,oldAddr 已失效!
try {
// 5. 在新内存块的后半部分写入新数据
unsafe.putInt(newAddr + size, 33686018); // 写入 0x02020202
// 6. 读取整个8字节的long值
System.out.println("Value at newAddr (full 8 bytes): " + unsafe.getLong(newAddr));
} finally {
// 7. 只释放最后有效的内存地址
unsafe.freeMemory(newAddr);
// 如果尝试 freeMemory(oldAddr),将会导致 double free 错误!
}
}
先看结果输出:
Initial address: 140467048086752
Value at oldAddr: 16843009
New address: 140467048086752
Value at newAddr (first 4 bytes): 16843009
Value at newAddr (full 8 bytes): 144680345659310337
Initial address: 140467048086752
Value at oldAddr: 16843009
New address: 140467048086752
Value at newAddr (first 4 bytes): 16843009
Value at newAddr (full 8 bytes): 144680345659310337
reallocateMemory 的行为类似于 C 语言中的 realloc 函数,它会尝试在不移动数据的情况下扩展或收缩内存块。其行为主要有两种情况:
reallocateMemory
原地扩容:如果当前内存块后面有足够的连续空闲空间,reallocateMemory 会直接在原地址上扩展内存,并返回原始地址。
reallocateMemory
异地扩容:如果当前内存块后面空间不足,它会寻找一个新的、足够大的内存区域,将旧数据拷贝过去,然后释放旧的内存地址,并返回新地址。
结合本次的运行结果,我们可以进行如下分析:
第一步:初始分配与写入
unsafe.allocateMemory(size) 分配了 4 字节的堆外内存,地址为 140467048086752。
unsafe.allocateMemory(size)
140467048086752
unsafe.putInt(oldAddr, 16843009) 向该地址写入了 int 值 16843009,其十六进制表示为 0x01010101。getInt 读取正确,证明写入成功。
unsafe.putInt(oldAddr, 16843009)
16843009
0x01010101
getInt
第二步:原地内存扩容
long newAddr = unsafe.reallocateMemory(oldAddr, size * 2) 尝试将内存块扩容至 8 字节。
long newAddr = unsafe.reallocateMemory(oldAddr, size * 2)
观察输出 New address: 140467048086752,我们发现 newAddr 与 oldAddr 的值完全相同。
140467048086752
newAddr
oldAddr
这表明本次操作触发了“原地扩容”。系统在原地址 140467048086752 后面找到了足够的空间,直接将内存块扩展到了 8 字节。在这个过程中,旧的地址 oldAddr 依然有效,并且就是 newAddr,数据也并未发生移动。
140467048086752
oldAddr
newAddr
第三步:验证数据与写入新数据
unsafe.getInt(newAddr) 再次读取前 4 个字节,结果仍是 16843009,验证了原数据完好无损。
unsafe.getInt(newAddr)
16843009
unsafe.putInt(newAddr + size, 33686018) 在扩容出的后 4 个字节(偏移量为 4)写入了新的 int 值 33686018(十六进制为 0x02020202)。
unsafe.putInt(newAddr + size, 33686018)
33686018
0x02020202
第四步:读取完整数据
unsafe.getLong(newAddr) 从起始地址读取一个 long 值(8 字节)。此时内存中的 8 字节内容为 0x01010101(低地址) 和 0x02020202(高地址) 的拼接。
unsafe.getLong(newAddr)
0x01010101
0x02020202
在小端字节序(Little-Endian)的机器上,这 8 字节在内存中会被解释为十六进制数 0x0202020201010101。
0x0202020201010101
这个十六进制数转换为十进制,结果正是 144680345659310337。这完美地解释了最终的输出结果。
144680345659310337
第五步:安全的内存释放
finally 块中,unsafe.freeMemory(newAddr) 安全地释放了整个 8 字节的内存块。
finally
unsafe.freeMemory(newAddr)
由于本次是原地扩容(oldAddr == newAddr),所以即使错误地多写一句 freeMemory(oldAddr) 也会导致二次释放的严重错误。
oldAddr == newAddr
freeMemory(oldAddr)
典型应用
DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。
DirectByteBuffer
DirectByteBuffer
为什么要使用堆外内存?
对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
下图为 DirectByteBuffer 构造函数,创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放。
DirectByteBuffer
DirectByteBuffer
Unsafe.allocateMemory
Unsafe.setMemory
Cleaner
DirectByteBuffer
DirectByteBuffer
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配内存并返回基地址
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 内存初始化
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 跟踪 DirectByteBuffer 对象的垃圾回收,以实现堆外内存释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配内存并返回基地址
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 内存初始化
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 跟踪 DirectByteBuffer 对象的垃圾回收,以实现堆外内存释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
内存屏障
介绍
在介绍内存屏障前,需要知道编译器和 CPU 会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致 CPU 的高速缓存和内存中数据的不一致,而内存屏障(Memory Barrier)就是通过阻止屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。
Memory Barrier
在硬件层面上,内存屏障是 CPU 为了防止代码进行重排序而提供的指令,不同的硬件平台上实现内存屏障的方法可能并不相同。在 Java8 中,引入了 3 个内存屏障的函数,它屏蔽了操作系统底层的差异,允许在代码中定义、并统一由 JVM 来生成内存屏障指令,来实现内存屏障的功能。
Unsafe 中提供了下面三个内存屏障相关方法:
Unsafe
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
内存屏障可以看做对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。以 loadFence 方法为例,它会禁止读操作重排序,保证在这个屏障之前的所有读操作都已经完成,并且将缓存数据设为无效,重新从主存中进行加载。
loadFence
看到这估计很多小伙伴们会想到 volatile 关键字了,如果在字段上添加了 volatile 关键字,就能够实现字段在多线程下的可见性。基于读内存屏障,我们也能实现相同的功能。下面定义一个线程方法,在线程中去修改 flag 标志位,注意这里的 flag 是没有被 volatile 修饰的:
volatile
volatile
flag
flag
volatile
@Getter
class ChangeThread implements Runnable{
/**volatile**/ boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
}
}
@Getter
class ChangeThread implements Runnable{
/**volatile**/ boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
}
}
在主线程的 while 循环中,加入内存屏障,测试是否能够感知到 flag 的修改变化:
while
flag
public static void main(String[] args){
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.isFlag();
unsafe.loadFence(); //加入读内存屏障
if (flag){
System.out.println("detected flag changed");
break;
}
}
System.out.println("main thread end");
}
public static void main(String[] args){
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.isFlag();
unsafe.loadFence(); //加入读内存屏障
if (flag){
System.out.println("detected flag changed");
break;
}
}
System.out.println("main thread end");
}
运行结果:
subThread change flag to:false
detected flag changed
main thread end
subThread change flag to:false
detected flag changed
main thread end
而如果删掉上面代码中的 loadFence 方法,那么主线程将无法感知到 flag 发生的变化,会一直在 while 中循环。可以用图来表示上面的过程:
loadFence
flag
while

了解 Java 内存模型(JMM)的小伙伴们应该清楚,运行中的线程不是直接读取主内存中的变量的,只能操作自己工作内存中的变量,然后同步到主内存中,并且线程的工作内存是不能共享的。上面的图中的流程就是子线程借助于主内存,将修改后的结果同步给了主线程,进而修改主线程中的工作空间,跳出循环。
JMM
典型应用
在 Java 8 中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于 StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不一致问题。
StampedLock
StampedLock
StampedLock
为了解决这个问题,StampedLock 的 validate 方法会通过 Unsafe 的 loadFence 方法加入一个 load 内存屏障。
StampedLock
validate
Unsafe
loadFence
load
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
对象操作
介绍
例子
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class Main {
private int value;
public static void main(String[] args) throws Exception{
Unsafe unsafe = reflectGetUnsafe();
assert unsafe != null;
long offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("value"));
Main main = new Main();
System.out.println("value before putInt: " + main.value);
unsafe.putInt(main, offset, 42);
System.out.println("value after putInt: " + main.value);
System.out.println("value after putInt: " + unsafe.getInt(main, offset));
}
private 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;
}
}
}
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class Main {
private int value;
public static void main(String[] args) throws Exception{
Unsafe unsafe = reflectGetUnsafe();
assert unsafe != null;
long offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("value"));
Main main = new Main();
System.out.println("value before putInt: " + main.value);
unsafe.putInt(main, offset, 42);
System.out.println("value after putInt: " + main.value);
System.out.println("value after putInt: " + unsafe.getInt(main, offset));
}
private 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;
}
}
}
输出结果:
value before putInt: 0
value after putInt: 42
value after putInt: 42
value before putInt: 0
value after putInt: 42
value after putInt: 42
对象属性
对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的 putInt、getInt 方法外,Unsafe 提供了全部 8 种基础数据类型以及 Object 的 put 和 get 方法,并且所有的 put 方法都可以越过访问权限,直接修改内存中的数据。阅读 openJDK 源码中的注释发现,基础数据类型和 Object 的读写稍有不同,基础数据类型是直接操作的属性值(value),而 Object 的操作则是基于引用值(reference value)。下面是 Object 的读写方法:
putInt
getInt
Object
put
get
put
Object
value
Object
reference value
Object
//在对象的指定偏移地址获取一个对象引用
public native Object getObject(Object o, long offset);
//在对象指定偏移地址写入一个对象引用
public native void putObject(Object o, long offset, Object x);
//在对象的指定偏移地址获取一个对象引用
public native Object getObject(Object o, long offset);
//在对象指定偏移地址写入一个对象引用
public native void putObject(Object o, long offset, Object x);
除了对象属性的普通读写外,Unsafe 还提供了 volatile 读写和有序写入方法。volatile 读写方法的覆盖范围与普通读写相同,包含了全部基础数据类型和 Object 类型,以 int 类型为例:
Unsafe
volatile
Object
int
//在对象的指定偏移地址处读取一个int值,支持volatile load语义
public native int getIntVolatile(Object o, long offset);
//在对象指定偏移地址处写入一个int,支持volatile store语义
public native void putIntVolatile(Object o, long offset, int x);
//在对象的指定偏移地址处读取一个int值,支持volatile load语义
public native int getIntVolatile(Object o, long offset);
//在对象指定偏移地址处写入一个int,支持volatile store语义
public native void putIntVolatile(Object o, long offset, int x);
相对于普通读写来说,volatile 读写具有更高的成本,因为它需要保证可见性和有序性。在执行 get 操作时,会强制从主存中获取属性值,在使用 put 方法设置属性值时,会强制将值更新到主存中,从而保证这些变更对其他线程是可见的。
volatile
get
put
有序写入的方法有以下三个:
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
有序写入的成本相对 volatile 较低,因为它只保证写入时的有序性,而不保证可见性,也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性,需要对内存屏障的知识点再进一步进行补充,首先需要了解两个指令的概念:
volatile
Load:将主内存中的数据拷贝到处理器的缓存中
Load
Store:将处理器缓存的数据刷新到主内存中
Store
顺序写入与 volatile 写入的差别在于,在顺序写时加入的内存屏障类型为 StoreStore 类型,而在 volatile 写入时加入的内存屏障是 StoreLoad 类型,如下图所示:
volatile
StoreStore
volatile
StoreLoad

在有序写入方法中,使用的是 StoreStore 屏障,该屏障确保 Store1 立刻刷新数据到内存,这一操作先于 Store2 以及后续的存储指令操作。而在 volatile 写入中,使用的是 StoreLoad 屏障,该屏障确保 Store1 立刻刷新数据到内存,这一操作先于 Load2 及后续的装载指令,并且,StoreLoad 屏障会使该屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成之后,才执行该屏障之后的内存访问指令。
StoreStore
Store1
Store2
volatile
StoreLoad
Store1
Load2
StoreLoad
综上所述,在上面的三类写入方法中,在写入效率方面,按照 put、putOrder、putVolatile 的顺序效率逐渐降低。
put
putOrder
putVolatile
对象实例化
使用 Unsafe 的 allocateInstance 方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作:
Unsafe
allocateInstance
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
分别基于构造函数、反射以及 Unsafe 方法的不同方式创建对象进行比较:
Unsafe
public void objTest() throws Exception{
A a1=new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3= (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}
public void objTest() throws Exception{
A a1=new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3= (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}
打印结果分别为 1、1、0,说明通过 allocateInstance 方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了 Class 对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将 A 类的构造函数改为 private 类型,将无法通过构造函数和反射创建对象(可以通过构造函数对象 setAccessible 后创建对象),但 allocateInstance 方法仍然有效。
allocateInstance
Class
private
allocateInstance
典型应用
常规对象实例化方式:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显式声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
非常规的实例化方式:而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。
数组操作
介绍
arrayBaseOffset 与 arrayIndexScale 这两个方法配合起来使用,即可定位数组中每个元素在内存中的位置。
arrayBaseOffset
arrayIndexScale
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
典型应用
这两个与数据操作相关的方法,在 java.util.concurrent.atomic 包下的 AtomicIntegerArray(可以实现对 Integer 数组中每个元素的原子性操作)中有典型的应用,如下图 AtomicIntegerArray 源码所示,通过 Unsafe 的 arrayBaseOffset、arrayIndexScale 分别获取数组首元素的偏移地址 base 及单个元素大小因子 scale。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的 getAndAdd 方法即通过 checkedByteOffset 方法获取某数组元素的偏移地址,而后通过 CAS 实现原子性操作。
java.util.concurrent.atomic
AtomicIntegerArray
Integer
AtomicIntegerArray
Unsafe
arrayBaseOffset
arrayIndexScale
base
scale
getAndAdd
checkedByteOffset

CAS 操作
介绍
这部分主要为 CAS 相关操作的方法。
/**
* CAS
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
/**
* CAS
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
什么是 CAS? CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg。
Unsafe
compareAndSwapXXX
cmpxchg
典型应用
在 JUC 包的并发工具类中大量地使用了 CAS 操作,像在前面介绍 synchronized 和 AQS 的文章中也多次提到了 CAS,其作为乐观锁在并发工具类中广泛发挥了作用。在 Unsafe 类中,提供了 compareAndSwapObject、compareAndSwapInt、compareAndSwapLong 方法来实现的对 Object、int、long 类型的 CAS 操作。以 compareAndSwapInt 方法为例:
synchronized
AQS
Unsafe
compareAndSwapObject
compareAndSwapInt
compareAndSwapLong
Object
int
long
compareAndSwapInt
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
参数中 o 为需要更新的对象,offset 是对象 o 中整形字段的偏移量,如果这个字段的值与 expected 相同,则将字段的值设为 x 这个新值,并且此更新是不可被中断的,也就是一个原子操作。下面是一个使用 compareAndSwapInt 的例子:
o
offset
o
expected
x
compareAndSwapInt
private volatile int a;
public static void main(String[] args){
CasTest casTest=new CasTest();
new Thread(()->{
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(()->{
for (int i = 5 ; i <10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
}
private void increment(int x){
while (true){
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
private volatile int a;
public static void main(String[] args){
CasTest casTest=new CasTest();
new Thread(()->{
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(()->{
for (int i = 5 ; i <10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
}
private void increment(int x){
while (true){
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
运行代码会依次输出:
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9
如果你把上面这段代码贴到 IDE 中运行,会发现并不能得到目标输出结果。有朋友已经在 Github 上指出了这个问题:issue#2650。下面是修正后的代码:
// 将递增和打印操作封装在一个原子性更强的方法内
private void incrementAndPrint(int targetValue) {
while (true) {
int currentValue = a; // 读取当前 a 的值
// 如果当前值已经达到或超过目标值,说明已被其他线程处理,跳过
if (currentValue >= targetValue) {
return;
}
// 尝试 CAS 操作:如果当前值等于 targetValue - 1,则原子地设置为 targetValue
if (currentValue == targetValue - 1) {
if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) {
// CAS 成功后立即打印,确保打印的就是本次设置的值
System.out.print(targetValue + " ");
return;
}
}
// CAS 失败,重新读取并重试
}
}
// 将递增和打印操作封装在一个原子性更强的方法内
private void incrementAndPrint(int targetValue) {
while (true) {
int currentValue = a; // 读取当前 a 的值
// 如果当前值已经达到或超过目标值,说明已被其他线程处理,跳过
if (currentValue >= targetValue) {
return;
}
// 尝试 CAS 操作:如果当前值等于 targetValue - 1,则原子地设置为 targetValue
if (currentValue == targetValue - 1) {
if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) {
// CAS 成功后立即打印,确保打印的就是本次设置的值
System.out.print(targetValue + " ");
return;
}
}
// CAS 失败,重新读取并重试
}
}
在上述例子中,我们创建了两个线程,它们都尝试修改共享变量 a。每个线程在调用 incrementAndPrint(targetValue) 方法时:
incrementAndPrint(targetValue)
会先读取 a 的当前值 currentValue。
currentValue
检查 currentValue 是否等于 targetValue - 1(即期望的前一个值)。
currentValue
targetValue - 1
如果条件满足,则调用 unsafe.compareAndSwapInt() 尝试将 a 从 currentValue 更新到 targetValue。
unsafe.compareAndSwapInt()
a
currentValue
targetValue
如果 CAS 操作成功(返回 true),则打印 targetValue 并退出循环。
targetValue
如果 CAS 操作失败,说明有其他线程同时竞争,此时会重新读取 currentValue 并重试,直到成功为止。
currentValue
这种机制确保了每个数字(从 1 到 9)只会被成功设置并打印一次,并且是按顺序进行的。

需要注意的是:
自旋逻辑: compareAndSwapInt 方法本身只执行一次比较和交换操作,并立即返回结果。因此,为了确保操作最终成功(在值符合预期的情况下),我们需要在代码中显式地实现自旋逻辑(如 while(true) 循环),不断尝试直到 CAS 操作成功。
compareAndSwapInt
while(true)
AtomicInteger 的实现: JDK 中的 java.util.concurrent.atomic.AtomicInteger 类内部正是利用了类似的 CAS 操作和自旋逻辑来实现其原子性的 getAndIncrement(), compareAndSet() 等方法。直接使用 AtomicInteger 通常是更安全、更推荐的做法,因为它封装了底层的复杂性。
AtomicInteger
java.util.concurrent.atomic.AtomicInteger
getAndIncrement()
compareAndSet()
AtomicInteger
ABA 问题: CAS 操作本身存在 ABA 问题(一个值从 A 变为 B,再变回 A,CAS 检查时会认为值没有变过)。在某些场景下,如果值的变化历史很重要,可能需要使用 AtomicStampedReference 来解决。但在本例的简单递增场景中,ABA 问题通常不构成影响。
AtomicStampedReference
CPU 消耗: 长时间的自旋会消耗 CPU 资源。在竞争激烈或条件长时间不满足的情况下,可以考虑加入更复杂的退避策略(如 Thread.sleep() 或 LockSupport.parkNanos())来优化。
Thread.sleep()
LockSupport.parkNanos()
线程调度
介绍
Unsafe 类中提供了 park、unpark、monitorEnter、monitorExit、tryMonitorEnter 方法进行线程调度。
Unsafe
park
unpark
monitorEnter
monitorExit
tryMonitorEnter
//取消阻塞线程
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);
//取消阻塞线程
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 可以终止一个挂起的线程,使其恢复正常。
park
unpark
park
park
unpark
此外,Unsafe 源码中 monitor 相关的三个方法已经被标记为 deprecated,不建议被使用:
Unsafe
monitor
deprecated
//获得对象锁
@Deprecated
public native void monitorEnter(Object var1);
//释放对象锁
@Deprecated
public native void monitorExit(Object var1);
//尝试获得对象锁
@Deprecated
public native boolean tryMonitorEnter(Object var1);
//获得对象锁
@Deprecated
public native void monitorEnter(Object var1);
//释放对象锁
@Deprecated
public native void monitorExit(Object var1);
//尝试获得对象锁
@Deprecated
public native boolean tryMonitorEnter(Object var1);
monitorEnter 方法用于获得对象锁,monitorExit 用于释放对象锁,如果对一个没有被 monitorEnter 加锁的对象执行此方法,会抛出 IllegalMonitorStateException 异常。tryMonitorEnter 方法尝试获取对象锁,如果成功则返回 true,反之返回 false。
monitorEnter
monitorExit
monitorEnter
IllegalMonitorStateException
tryMonitorEnter
true
false
典型应用
Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer (AQS),就是通过调用 LockSupport.park() 和 LockSupport.unpark() 实现线程的阻塞和唤醒的,而 LockSupport 的 park、unpark 方法实际是调用 Unsafe 的 park、unpark 方式实现的。
AbstractQueuedSynchronizer
LockSupport.park()
LockSupport.unpark()
LockSupport
park
unpark
Unsafe
park
unpark
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
LockSupport 的 park 方法调用了 Unsafe 的 park 方法来阻塞当前线程,此方法将线程阻塞后就不会继续往后执行,直到有其他线程调用 unpark 方法唤醒当前线程。下面的例子对 Unsafe 的这两个方法进行测试:
LockSupport
park
Unsafe
park
unpark
Unsafe
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("subThread try to unpark mainThread");
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("park main mainThread");
unsafe.park(false,0L);
System.out.println("unpark mainThread success");
}
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("subThread try to unpark mainThread");
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("park main mainThread");
unsafe.park(false,0L);
System.out.println("unpark mainThread success");
}
程序输出为:
park main mainThread
subThread try to unpark mainThread
unpark mainThread success
park main mainThread
subThread try to unpark mainThread
unpark mainThread success
程序运行的流程也比较容易看懂,子线程开始运行后先进行睡眠,确保主线程能够调用 park 方法阻塞自己,子线程在睡眠 5 秒后,调用 unpark 方法唤醒主线程,使主线程能继续向下执行。整个流程如下图所示:
park
unpark

Class 操作
介绍
Unsafe 对 Class 的相关操作主要包括类加载和静态变量的操作方法。
Unsafe
Class
静态属性读取相关的方法
//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要初始化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);
//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要初始化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);
创建一个包含静态属性的类,进行测试:
@Data
public class User {
public static String name="Hydra";
int age;
}
private void staticTest() throws Exception {
User user=new User();
// 也可以用下面的语句触发类初始化
// 1.
// unsafe.ensureClassInitialized(User.class);
// 2.
// System.out.println(User.name);
System.out.println(unsafe.shouldBeInitialized(User.class));
Field sexField = User.class.getDeclaredField("name");
long fieldOffset = unsafe.staticFieldOffset(sexField);
Object fieldBase = unsafe.staticFieldBase(sexField);
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println(object);
}
@Data
public class User {
public static String name="Hydra";
int age;
}
private void staticTest() throws Exception {
User user=new User();
// 也可以用下面的语句触发类初始化
// 1.
// unsafe.ensureClassInitialized(User.class);
// 2.
// System.out.println(User.name);
System.out.println(unsafe.shouldBeInitialized(User.class));
Field sexField = User.class.getDeclaredField("name");
long fieldOffset = unsafe.staticFieldOffset(sexField);
Object fieldBase = unsafe.staticFieldBase(sexField);
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println(object);
}
运行结果:
false
Hydra
false
Hydra
在 Unsafe 的对象操作中,我们学习了通过 objectFieldOffset 方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用 staticFieldOffset 方法。在上面的代码中,只有在获取 Field 对象的过程中依赖到了 Class,而获取静态变量的属性时不再依赖于 Class。
Unsafe
objectFieldOffset
staticFieldOffset
Field
Class
Class
在上面的代码中首先创建一个 User 对象,这是因为如果一个类没有被初始化,那么它的静态属性也不会被初始化,最后获取的字段属性将是 null。所以在获取静态属性前,需要调用 shouldBeInitialized 方法,判断在获取前是否需要初始化这个类。如果删除创建 User 对象的语句,运行结果会变为:
User
null
shouldBeInitialized
true
null
true
null
使用 defineClass 方法允许程序在运行时动态地创建一个类
defineClass
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader,ProtectionDomain protectionDomain);
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader,ProtectionDomain protectionDomain);
在实际使用过程中,可以只传入字节数组、起始字节的下标以及读取的字节长度,默认情况下,类加载器(ClassLoader)和保护域(ProtectionDomain)来源于调用此方法的实例。下面的例子中实现了反编译生成后的 class 文件的功能:
ClassLoader
ProtectionDomain
private static void defineTest() {
String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
File file = new File(fileName);
try(FileInputStream fis = new FileInputStream(file)) {
byte[] content=new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod("getAge").invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void defineTest() {
String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
File file = new File(fileName);
try(FileInputStream fis = new FileInputStream(file)) {
byte[] content=new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod("getAge").invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
}
在上面的代码中,首先读取了一个 class 文件并通过文件流将它转化为字节数组,之后使用 defineClass 方法动态的创建了一个类,并在后续完成了它的实例化工作,流程如下图所示,并且通过这种方式创建的类,会跳过 JVM 的所有安全检查。
class
defineClass

除了 defineClass 方法外,Unsafe 还提供了一个 defineAnonymousClass 方法:
defineClass
defineAnonymousClass
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
使用该方法可以用来动态的创建一个匿名类,在 Lambda 表达式中就是使用 ASM 动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在 JDK 15 发布的新特性中,在隐藏类(Hidden classes)一条中,指出将在未来的版本中弃用 Unsafe 的 defineAnonymousClass 方法。
Lambda
Hidden classes
Unsafe
defineAnonymousClass
典型应用
Lambda 表达式实现需要依赖 Unsafe 的 defineAnonymousClass 方法定义实现相应的函数式接口的匿名类。
Unsafe
defineAnonymousClass
系统信息
介绍
这部分包含两个获取系统相关信息的方法。
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();
典型应用
这两个方法的应用场景比较少,在 java.nio.Bits 类中,在使用 pageCount 计算所需的内存页的数量时,调用了 pageSize 方法获取内存页的大小。另外,在使用 copySwapMemory 方法拷贝内存时,调用了 addressSize 方法,检测 32 位系统的情况。
java.nio.Bits
pageCount
pageSize
copySwapMemory
addressSize
总结
在本文中,我们首先介绍了 Unsafe 的基本概念、工作原理,并在此基础上,对它的 API 进行了说明与实践。相信大家通过这一过程,能够发现 Unsafe 在某些场景下,确实能够为我们提供编程中的便利。但是回到开头的话题,在使用这些便利时,确实存在着一些安全上的隐患,在我看来,一项技术具有不安全因素并不可怕,可怕的是它在使用过程中被滥用。尽管之前有传言说会在 Java9 中移除 Unsafe 类,不过它还是照样已经存活到了 Java16。按照存在即合理的逻辑,只要使用得当,它还是能给我们带来不少的帮助,因此最后还是建议大家,在使用 Unsafe 的过程中一定要做到使用谨慎使用、避免滥用。
Unsafe
Unsafe
Unsafe
Unsafe
写在最后
如果内容对你有帮助的话,欢迎顺手给 JavaGuide 点一个免费的 Star 支持一下:GitHub | Gitee。
JavaGuide 已持续维护近七年,累计 6100+ 次提交,来自 620+ 位贡献者共同完善。你的 Star、反馈和 PR,都是这个项目继续更新的动力。
如果你正在准备后端/AI 应用开发面试,也可以了解一下我的知识星球,里面包括后端和 AI 实战项目、简历优化、一对一提问和高频考点资料,已经持续维护六年。