Unsafe 使用详解

Unsafe 使用详解Unsafe 是 jdk 提供的一个直接访问操作系统资源的工具类 底层 c 实现 它可以直接分配内存 内存复制 copy 提供 cpu 级别的 CAS 乐观锁等操作 Unsafe 位于 sun misc 包下 jdk 中的并发编程包 juc java util concurrent 基本全部靠 Unsafe 实现 由此可见其重要性 它的目的是为了增强 java 语言直接操作底层资源的能力 无疑带来很多方便 但是 使用的同时就得额外小心 它的总体作用如下 1 获取 Unsafe 对象 Unsafe 被设计为单例 并且只允许被引导类加载器

1. 获取Unsafe对象

Unsafe被设计为单例,并且只允许被引导类加载器(BootstrapClassLoader)加载的类使用:

@CallerSensitive public static Unsafe getUnsafe() { 
    Class<?> caller = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(caller.getClassLoader())) throw new SecurityException("Unsafe"); return theUnsafe; } 

所以我们自己写的类是无法直接通过Unsafe.getUnsafe()获取的。所以只能通过反射直接new一个或者将其内部静态成员变量theUnsafe获取出来:

public static void main(String[] args) throws Exception{ 
    Class<Unsafe> unsafeClass = Unsafe.class; //方法一:通过反射构造一个Unsafe对象 Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor(); constructor.setAccessible(true); Unsafe unsafe1 = constructor.newInstance(); System.out.println(unsafe1); //方法二:获取内部静态成员变量 Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe2 = (Unsafe) theUnsafe.get(null); System.out.println(unsafe2); } 

可以得到以下结果:

sun.misc.Unsafe@3b192d32 sun.misc.Unsafe@12bb4df8 

现在我们能够在自己代码里面使用Unsafe了,接下来看下它的使用以及jdk使用操作的。

2. CAS

@ForceInline public final boolean compareAndSwapObject(Object o, long offset, Object expected, Object x) { 
    return theInternalUnsafe.compareAndSetObject(o, offset, expected, x); } @ForceInline public final boolean compareAndSwapInt(Object o, long offset, int expected, int x) { 
    return theInternalUnsafe.compareAndSetInt(o, offset, expected, x); } @ForceInline public final boolean compareAndSwapLong(Object o, long offset, long expected, long x) { 
    return theInternalUnsafe.compareAndSetLong(o, offset, expected, x); } 

简单介绍下它使用的参数:

  • 第一个参数 o 为内存中要操作的对象
  • 第二个参数 offset 为要操作的值的内存地址偏移量
  • 第三个参数 expect 为预期值
  • 第四个参数 x 为想要更新成的值

为了方便理解,举个栗子。类User有一个成员变量name。我们new了一个对象User后,就知道了它(User对象)在内存中的起始值,而成员变量name在对象中的位置偏移是固定的。这样通过这个起始值和这个偏移量就能够定位到成员变量name在内存中的具体位置
所以我们现在的问题就是如何得出name在对象User中的偏移量,Unsafe自然也提供了相应的方法:

@ForceInline public long objectFieldOffset(Field f) { 
    return theInternalUnsafe.objectFieldOffset(f); } @ForceInline public long staticFieldOffset(Field f) { 
    return theInternalUnsafe.staticFieldOffset(f); } 

他们分别为获取成员变量和静态成员变量的方法,所以我们可以使用unsafe直接更新内存中的值:

import sun.misc.Unsafe; import java.lang.reflect.*; public class UnsafeDemo { 
    public static void main(String[] args) throws Exception { 
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); User user = new User("jsbintask"); long nameOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("name")); boolean res1 = unsafe.compareAndSwapObject(user, nameOffset, "jsbintask1", "jsbintask2"); System.out.println(res1+", 第一次更新后的值:" + user.getName()); boolean res2 = unsafe.compareAndSwapObject(user, nameOffset, "jsbintask", "jsbintask2"); System.out.println(res2+", 第二次更新后的值:" + user.getName()); } public static class User { 
    private String name; public User(String name) { 
    this.name = name; } public String getName() { 
    return name; } } } 

输出:

false, 第一次更新后的值:jsbintask true, 第二次更新后的值:jsbintask2 

因为内存中name的值为”jsbintask”,而第一次使用compareAndSwapObject方法预期值为”jsbintask1″,这显然是不相等的,所以第一次更新失败,返回false。第二次我们传入了正确的预期值,返回true,更新成功!

如果我们分析juc包下的Atomic开头的原子类就会发现,它内部的原子操作全部来源于unsafe的CAS方法,比如AtomicInteger的getAndIncrement方法,内部直接调用unsafe的getAndAddInt方法,它的实现原理为:cas失败,就循环,直到成功为止,这就是我们所说的自旋锁!

3. 数组操作

对于数组,Unsafe提供了特别的方法返回不同类型数组在内存中的偏移量:

@ForceInline public int arrayBaseOffset(Class<?> arrayClass) { 
    return theInternalUnsafe.arrayBaseOffset(arrayClass); } @ForceInline public int arrayIndexScale(Class<?> arrayClass) { 
    return theInternalUnsafe.arrayIndexScale(arrayClass); } 

arrayBaseOffset方法返回数组在内存中的偏移量,这个值是固定的。arrayIndexScale返回数组中的每一个元素的内存地址换算因子。举个栗子,double数组(注意不是包装类型)每个元素占用8个字节,所以换算因子为8,int类型则为4,通过这两个方法我们就能定位数组中每个元素的内存地址,从而赋值,下面代码演示:

import sun.misc.Unsafe; import java.lang.reflect.*; import java.util.*; public class UnsafeDemo { 
    public static void main(String[] args) throws Exception { 
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); Integer[] integers = new Integer[10]; // 打印数组的原始值 System.out.println(Arrays.toString(integers)); // 获取Integer数组在内存中的固定的偏移量 long arrayBaseOffset = unsafe.arrayBaseOffset(Integer[].class); System.out.println(unsafe.arrayIndexScale(Integer[].class)); System.out.println(unsafe.arrayIndexScale(double[].class)); // 将数组中第一个元素的更新为100 unsafe.putObject(integers, arrayBaseOffset, 100); // 将数组中第五个元素更新为50 注意 引用类型占用4个字节,所以内存地址 需要 4 * 4 = 16 unsafe.putObject(integers, arrayBaseOffset + 16, 50); // 打印更新后的值 System.out.println(Arrays.toString(integers)); } } 

输出:

[null, null, null, null, null, null, null, null, null, null] 4 8 [100, null, null, null, 50, null, null, null, null, null] 

我们通过获取Integer数组的内存偏移量,结合换算因子将第一个元素,第五个元素分别替换为了100,50。验证了我们的说法。

设置值,不管java的访问限制。

4. 内存分配

Unsafe还给我们提供了直接分配内存,释放内存,拷贝内存,内存设置等方法,值得注意的是,这里的内存指的是堆外内存!它是不受jvm内存模型掌控的,所以使用需要及其小心:

//分配内存, 相当于C++的malloc函数 public native long allocateMemory(long bytes); //释放内存 public native void freeMemory(long address); //在给定的内存块中设置值 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); //为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); 

我们可以写一段代码验证一下:

public static void main(String[] args) throws Exception { 
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); // 分配 10M的堆外内存 long _10M_Address = unsafe.allocateMemory(1 * 1024 * 1024 * 10); // 将10M内存的 前面1M内存值设置为10 unsafe.setMemory(_10M_Address, 1 * 1024 * 1024 * 1, (byte) 10); // 获取第1M内存的值: 10 System.out.println(unsafe.getByte(_10M_Address + 1000)); // 获取第1M内存后的值: 0(没有设置) System.out.println(unsafe.getByte(_10M_Address + 1 * 1024 * 1024 * 5)); } 

我们分配了10M内存,并且将前1M内存的值设置为了10,取出了内存中的值进行比较,验证了unsafe的方法。

堆外内存不受jvm内存模型掌控,在nio(netty,mina)中大量使用对外内存进行管道传输,copy等,使用它们的好处如下:

  • 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
  • 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

而在jdk中,堆外内存对应的类为DirectByteBuffer,它内部也是通过unsafe分配的内存:

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; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; } 

这里值得注意的是,对外内存的回收借助了Cleaner这个类。

5. 线程调度

通过Unsafe还可以直接将某个线程挂起,这和调用Object.wait()方法作用是一样的,但是效率确更高!

@ForceInline public void unpark(Object thread) { 
    theInternalUnsafe.unpark(thread); } @ForceInline public void park(boolean isAbsolute, long time) { 
    theInternalUnsafe.park(isAbsolute, time); } 

我们熟知的AQS(AbstractQueuedSynchronizer)内部挂起线程使用了LockSupport,而LockSupport内部依旧使用的是Unsafe:

public static void unpark(Thread thread) { 
    if (thread != null) U.unpark(thread); } 

6. 其它操作

//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); //内存屏障,禁止load、store操作重排序 public native void fullFence(); 

另外,jdk1.8引入了lambda表达式,它其实会帮我们调用Unsafe的public native Class
defineAnonymousClass(Class
var1, byte[] var2, Object[] var3);方法生成匿名内部类
,如下面的代码:

public class UnsafeTest2 { 
    public static void main(String[] args) { 
    Function<String, Integer> function = Integer::parseInt; System.out.println(function.apply("100")); } } 
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/208253.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午12:00
下一篇 2026年3月19日 下午12:00


相关推荐

  • Matlab 非线性有约束规划的粒子群算法「建议收藏」

    Matlab 非线性有约束规划的粒子群算法「建议收藏」粒子群算法的基本认识简单介绍:通过群体中个体之间的协作和信息共享来寻找最优解。适用于连续函数极值问题,对于非线性,多峰问题均有较强的全局搜索能力。主要掌握两点1.粒子的速度和位置速度代表移动的快慢,位置代表移动的方向。位置对应每个自变量,速度一般设置为变量范围的10%~20%。2.粒子的更新规则具体实例下面展示matlab代码。clear;close;clc%%约束条件和目标函数构建fun=@(x)x(1)^2+x(2)^2+x(3)^2+8;bind1

    2022年6月1日
    46
  • java实现十进制转十六进制_十进制转十六进制java代码

    java实现十进制转十六进制_十进制转十六进制java代码基础练习十六进制转十进制时间限制:1.0s内存限制:512.0MB问题描述  从键盘输入一个不超过8位的正的十六进制数字符串,将它转换为正的十进制数后输出。注:十六进制数中的10~15分别用大写的英文字母A、B、C、D、E、F表示。样例输入FFFF样例输出65535太奇葩了,拿到这道题受上道题的影响,自己写了进制转化函数,结果,25分。。。。imp…

    2025年5月27日
    6
  • C语言运行余弦定理

    C语言运行余弦定理笔者运用 CFree 编译器 实现了余弦定理 代码如下 include stdio h include math h intmain floata b rad 要求 rad 为弧度制 scanf f f f amp a amp b amp rad floatc 待求解的第三边 c pow pow a 2 pow math h stdio h

    2026年3月16日
    2
  • 16天记住7000考研单词

    16天记住7000考研单词16天记住7000考研单词(第一天)1.WithmyownearsIclearlyheardtheheartbeatofthenuclearbomb.我亲耳清楚地听到原子弹的心脏的跳动。2.Nextyearthebeardedbearwillbearadearbabyintherear.明年,长胡子的熊将在后方产一头可爱的小崽.

    2022年5月29日
    37
  • 电脑硬盘分区表丢失变成了未分配怎么找回【分区恢复】

    电脑硬盘分区表丢失变成了未分配怎么找回【分区恢复】

    2026年3月13日
    3
  • class, classloder, dex 详解

    class, classloder, dex 详解

    2022年3月12日
    47

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号