Java基础(九)Unsafe类
获取成员变量在内存中的偏移量:objectFieldOffset(Field);获取静态变量在内存中的偏移量:staicFieldOffset(Field);设定指定内存处的值:putInt(obj, address, val);参数1:要操作的对象。如果是直接操作内存地址,这个参数可以为 null。参数2:相对于参数1的内存偏移量,偏移量从参数1在内存中的起始位置开始。如果参数1为null,则此
简介
Unsafe类是Java中一个非常强大的工具类,它提供了许多底层的操作,可以直接操纵内存、对象、线程。通常建议仅在实现底层库或需要非常高性能的场景中使用。
注意事项:
- 安全性:Unsafe的操作不受Java的安全机制保护,使用不当可能导致程序崩溃或数据损坏。
- 可移植性:Unsafe是特定于JVM实现的,不同JVM可能行为不同。
- 访问限制:从Java 9开始,Unsafe类受到更严格的访问限制,建议使用更安全的替代方案
使用方式
获取Unsafe类的实例
Unsafe类只允许被根类加载器加载的类来使用,如果被应用加载器加载的类中使用,需要使用反射。
案例:通过反射来获取Unsafe类的实例
案例:
@Test
public void test2() throws Exception {
// Unsafe类使用了单例模式,它通过一个私有的静态字段theUnsafe
// 提供单一的实例,在虚拟机中只有一个Unsafe实例。
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println("unsafe = " + unsafe); // sun.misc.Unsafe@26f0a63f
assert unsafe != null;
}
通过Unsafe类来操作内存 堆外内存
通过Unsafe类操作的是堆外内存。堆外内存是属于操作系统而不属于jvm的内存,堆外内存避免了堆内堆外内存拷贝这一步,更加高效,但是因为不属于jvm,所以需要使用者自己来释放内存。
案例1:分配内存、释放内存
@Test
public void test3() throws Exception {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
// 分配内存
int memorySize = 4;
long address = unsafe.allocateMemory(memorySize);
// 初始化内存
// setMemory方法的参数
// 参数1:要操作的对象。如果是直接操作内存地址,这个参数可以为 null。
// 参数2:相对于参数1的内存偏移量,偏移量从参数1在内存中的起始位置开始。
// 如果参数1为null,则此偏移量为绝对地址。
// 参数3:要设置的内存字节数。
// 参数4:要填充的字节值。
unsafe.setMemory(null, address, memorySize, (byte) 1);
// 验证设置的内存
for (int i = 0; i < memorySize; i++) {
byte aByte = unsafe.getByte(address + i);
assert aByte == 1;
}
// 释放内存
unsafe.freeMemory(address);
}
案例2:重新分配内存
重新分配内存主要用于数组扩容等地方。
@Test
public void test3_1() throws Exception {
// 获取unsafe实例
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
int memorySize = 4;
long address = unsafe.allocateMemory(memorySize);
// 重新分配内存,
// reallocateMemory函数
// 参数1:内存地址
// 参数2:内存大小
long newAddress = unsafe.reallocateMemory(address, memorySize * 10);
// 释放内存,注意,同一块内存不能被释放两次
unsafe.freeMemory(newAddress);
}
系统操作
@Test
public void testAddressSize() {
// 返回当前系统指针的大小,通常在64位系统上返回8,表示64位指针,在32位系统上返回 4,
// 不过要注意,64位的虚拟机在堆内存小于32G的情况下默认开启指针压缩,所以虚拟机的指针
// 通常是32位
System.out.println("unsafe.addressSize() = " + unsafe.addressSize()); // 8
}
@Test
public void testPageSize() {
// 返回当前系统内存页的大小
System.out.println("unsafe.pageSize() = " + unsafe.pageSize()); // 16384 = 16K
}
设置指定内存处的值
案例:这里演示putInt、getInt方法,而且内存位置和对象的属性没有关系,是之前使用allocateMemory方法分配的内存。通常情况下,设置指定内存处的值,是设置某个对象的成员变量,随后演示这种操作
@Test
public void test4() throws Exception {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
// 分配内存
int memorySize = 4;
long address = unsafe.allocateMemory(memorySize);
// 设置指定内存处的值
// putInt方法的参数:
// 参数1:要操作的对象。如果是直接操作内存地址,这个参数可以为 null。
// 参数2:相对于参数1的内存偏移量,偏移量从参数1在内存中的起始位置开始。
// 如果参数1为null,则此偏移量为绝对地址。
// 参数3:要设置的值
unsafe.putInt(null, address, 123);
// 验证
// getInt方法的参数:
// 参数1:要操作的对象。如果是直接操作内存地址,这个参数可以为 null。
// 参数2:相对于参数1的内存偏移量,偏移量从参数1在内存中的起始位置开始。
// 如果参数1为null,则此偏移量为绝对地址。
int anInt = unsafe.getInt(null, address);
assert anInt == 123;
// 释放内存
unsafe.freeMemory(address);
}
操作对象
基本步骤:
- 获取对象中成员变量的Field对象
- 通过unsafe提供的方法获取这个Field的内存偏移量,objectFieldOffset方法。
- 调用putXXX方法,设置变量的值,例如,putInt。
案例:通过unsafe设置字符串的哈希值
public void test5() throws Exception {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
// 第一步:创建一个字符串对象
String str = "hello";
// 第二步:获取字符串对象中存储哈希值的字段
Field stringHashField = String.class.getDeclaredField("hash");
// 第三步:获取字段在对象中的偏移量
long offset = unsafe.objectFieldOffset(stringHashField);
System.out.println("offset = " + offset); // 16
// 第四步:设置字段的值
unsafe.putInt(str, offset, 123);
assert str.hashCode() == 123;
}
操作数组
操作数组的api主要有两个:
- arrayBaseOffset:返回数组类型的第一个元素的偏移地址。它操作的是某一个类型的数组,例如int类型,而不是单个数组实例
- arrayIndexScale:返回数组中每个元素的大小。在使用时,arrayBaseOffset + (arrayIndexScale * i),就可以操作数组中指定位置的元素
案例:
@Test
public void test9() throws Exception {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
// 获取int类型的数组在内存中的偏移量。
int intArrayBaseOffset = unsafe.arrayBaseOffset(int[].class);
// 获取int类型的数组的元素大小
int intArrayBaseScale = unsafe.arrayIndexScale(int[].class);
System.out.println("int类型的数组在内存中的偏移量 = " + intArrayBaseOffset); // 16
System.out.println("int类型的数组的元素大小 = " + intArrayBaseScale); // 4
// 设置数组中元素的值
int len = 5;
int[] arr1 = new int[len];
int[] arr2 = new int[len];
for (int i = 0; i < len; i++) {
unsafe.putInt(arr1, intArrayBaseOffset + (long) i * intArrayBaseScale, i * 10);
unsafe.putInt(arr2, intArrayBaseOffset + (long) i * intArrayBaseScale, i * 20);
}
for (int i = 0; i < len; i++) {
assert arr1[i] == i * 10;
assert arr2[i] == i * 20;
}
}
cas操作
CAS,比较并交换, 是一种无锁的原子操作,用于实现线程安全的更新操作而不使用锁。它通过比较内存中的值与预期值,如果相等则更新为新值,否则不更新。
案例:多个线程,使用cas操作,同时修改某个位置的值。通过结果可以看到,最终只有一个线程修改成功了
private volatile int i = 0;
@Test
public void test11() throws Exception {
// 获取 Unsafe 实例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
// 获取字段i的内存偏移量
Field fieldI = UnsafeTest.class.getDeclaredField("i");
long fieldIOffset = unsafe.objectFieldOffset(fieldI); // 只能获取成员变量的内存偏移量
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
executorService.submit(() -> {
// 参数1:操作哪个对象
// 参数2:操作对象上的哪个字段
// 参数3:预期值
// 参数4:新值
boolean b = unsafe.compareAndSwapInt(this, fieldIOffset, 0, 1);
// 可以看到,三个线程,通过cas操作,修改变量的值,只有一个可以操作成功,其它两个都返回false
// b = true
// b = false
// b = false
System.out.println("b = " + b);
});
}
assert i == 1;
}
线程操作
线程操作主要是park、unpark方法,它们负责阻塞和唤醒线程。
案例1:子线程使用park方法阻塞,主线程唤醒子线程
@Test
public void test12() throws Exception {
// 获取 Unsafe 实例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Thread thread = new Thread(() -> {
System.out.println("子线程 park");
unsafe.park(false, 0L);
System.out.println("子线程 unpark");
});
thread.start();
Thread.sleep(1000);
System.out.println("主线程解除子线程的park");
unsafe.unpark(thread);
}
执行结果:
子线程 park
主线程解除子线程的park
子线程 unpark
案例2:使用park方法,阻塞子线程3秒
public static void main (String[] args) throws Exception {
// 获取 Unsafe 实例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
// 阻塞子线程3秒
Thread thread = new Thread(() -> {
System.out.println(System.currentTimeMillis() + " 子线程 park"); // 1733794663331 子线程 park
// 参数1:boolean isAbsolute,指示时间参数是否为绝对时间。
// 参数2:long time,时间参数,单位为纳秒。如果 isAbsolute 为 true,则表示绝对时间;否则表示相对时间。
unsafe.park(false, TimeUnit.SECONDS.toNanos(3));
System.out.println(System.currentTimeMillis() + " 子线程 unpark"); // 1733794666332 子线程 unpark
});
thread.start();
}
内存屏障
内存屏障是一个CPU指令,它的作用是确保一些特定操作的执行顺序,避免编译器和CPU的指令重排序,确保在多线程环境下的内存可见性和顺序性。例如,在内存屏障前后,写操作必须有序,内存屏障后的写操作不可以在内存屏障前执行。
优点:内存屏障不会像锁那样引入线程的阻塞和上下文切换,因此在某些场景下可以显著提高性能。
unsafe类中的内存屏障:
- loadFence:读屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前。
- storeFence:写屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前。确保在 storeFence 之前的所有写操作,在storeFence之后的写入操作之前对其他线程可见
- fullFence:读写屏障,禁止load、store操作重排序
案例1:队列 线程不安全
这里先写一个单线程的队列,演示队列的基本操作,在随后会使用内存屏障和cas操作让它线程安全
public class NonSyncQueue<T> {
// 头节点,这里是一个dummyHead,避免在操作中额外判断头节点
private Node head;
// 尾结点
private Node tail;
public NonSyncQueue() {
head = tail = new Node(null, null); // dummyHead
}
// 入队:将新节点挂载到尾结点之后
public void enqueue(T item) {
tail.next = new Node(item, null);
tail = tail.next;
}
// 出队:指向头结点的指针后移
public T dequeue() {
if (head == tail) {
return null;
}
Node realHead = head.next;
T value = realHead.value;
head = realHead;
return value;
}
/**
* 队列中的节点
*/
private class Node {
public T value;
public Node next;
public Node() { }
public Node(T value, Node next) {
this.value = value;
this.next = next;
}
}
}
测试:
@Test
public void test1() {
NonSyncQueue<Integer> queue = new NonSyncQueue<>();
// 入队
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
// 出队
assert queue.dequeue() == 1;
assert queue.dequeue() == 2;
assert queue.dequeue() == 3;
assert queue.dequeue() == null;
}
总结:队列,先进先出。先进,挂载到尾部,先出,头部出队,底层是一个单向链表,持有头节点和尾结点的实例。
案例2:队列 线程安全
基于cas操作和内存屏障,设计一个线程安全的队列
public class SyncQueue<T> {
private volatile Node head;
private volatile Node tail;
public SyncQueue() {
head = tail = new Node();
}
// 入队
public void enqueue(T value) throws NoSuchFieldException {
Node node = new Node(value, null);
while (true) {
Node currentTail = tail;
// 如果cas操作成功地将新节点挂在到了尾结点上
if (unsafe.compareAndSwapObject(currentTail, nodeNextFieldOffset, null, node)) {
// 写屏障
unsafe.storeFence();
// 尾结点后移
tail = node;
return;
}
}
}
// 出队
public T dequeue() {
while (true) {
Node currentHead = head;
Node next = currentHead.next;
if (currentHead == head) { // 确保多线程环境下的一致性
if (next == null) {
return null;
} else {
unsafe.loadFence(); // 确保读取next.value发生在读取next的后面
T value = next.value;
if (unsafe.compareAndSwapObject(this, headFieldOffset, currentHead, next)) { // head指针后移
currentHead.next = null; // help GC
next.value = null;
return value;
}
}
}
}
}
private class Node {
public T value;
public Node next;
public Node() { }
public Node(T value, Node next) {
this.value = value;
this.next = next;
}
}
private static final Unsafe unsafe;
private static final Field headField;
private static final Field tailField;
private static final Field nodeNextField;
private static final long headFieldOffset;
private static final long tailFieldOffset;
private static final long nodeNextFieldOffset;
@Override
public String toString() {
if (head == tail) {
return "";
}
StringBuilder sBuilder = new StringBuilder();
Node node = head.next;
while (node != null) {
sBuilder.append(node.value).append(" ");
node = node.next;
}
return sBuilder.toString();
}
static {
try {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
unsafe = (Unsafe) theUnsafeField.get(null);
headField = SyncQueue.class.getDeclaredField("head");
tailField = SyncQueue.class.getDeclaredField("tail");
nodeNextField = SyncQueue.Node.class.getDeclaredField("next");
headFieldOffset = unsafe.objectFieldOffset(headField);
tailFieldOffset = unsafe.objectFieldOffset(tailField);
nodeNextFieldOffset = unsafe.objectFieldOffset(nodeNextField);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
测试:多个线程同时向队列中添加数据,然后再使用多个线程从队列中取数据,确认取出来的数据是之前添加进去的。
@Test
public void test1() {
SyncQueue<Integer> queue = new SyncQueue<>();
// 准备测试数据
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
list.add(i);
}
// 入队
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 1; i <= 100; i++) {
final int finalI = i;
pool.submit(() -> {
try {
queue.enqueue(finalI);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
});
}
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("queue = " + queue);
// 出队,验证
Set<Integer> set = new ConcurrentSkipListSet<>(list);
for (int i = 0; i < 100; i++) {
pool.execute(() -> {
Integer val = queue.dequeue();
assert set.contains(val);
set.remove(val);
});
}
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
assert set.isEmpty();
pool.shutdown();
}
总结:
- cas操作,要考虑操作失败之后怎么办?
- 在两个写操作或读操作之间加入屏障,
- cas操作之前,加入一些判断,确保当前操作的变量没有被其它线程修改
其它使用方式
putOrderedObject方法
putOrderedObject方法:以有序的方式,修改指定对象的值,这种操作是非阻塞的,并且在某些平台上可能比普通的写操作更快。它通常操作volatile类型的变量,因为它本身会有可见性问题。
案例:
private volatile String value = "aaa";
@Test
public void test15() throws Exception {
// 获取 Unsafe 实例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Field valueField = this.getClass().getDeclaredField("value");
long valueFieldOffset = unsafe.objectFieldOffset(valueField);
assert "aaa".equals(value);
unsafe.putOrderedObject(this, valueFieldOffset, "bbb");
assert "bbb".equals(value);
}
总结
Unsafe类的API总结:
- 获取成员变量在内存中的偏移量:objectFieldOffset(Field);
- 获取静态变量在内存中的偏移量:staticFieldOffset(Field);
- 设定指定内存处的值:putInt(obj, address, val);
- 参数1:要操作的对象。如果是直接操作内存地址,这个参数可以为 null。
- 参数2:相对于参数1的内存偏移量,偏移量从参数1在内存中的起始位置开始。如果参数1为null,则此偏移量为绝对地址。
- 参数3:要设置的值
引用:
- https://www.cnblogs.com/throwable/p/9139947.html
- https://cloud.tencent.com/developer/article/1124658
- https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
- https://www.cnblogs.com/mickole/articles/3757278.html
更多推荐


所有评论(0)