Unsafe 使用详解

深入理解Java Unsafe:底层操作与并发编程关键
本文详细探讨了Java的Unsafe工具类,涉及单例获取、CAS乐观锁、数组操作、内存分配与管理、线程调度,以及内存屏障等内容,揭示了它在并发编程中的核心作用和潜在风险。

Unsafe是jdk提供的一个直接访问操作系统资源的工具类(底层c++实现),它可以直接分配内存,内存复制,copy,提供cpu级别的CAS乐观锁等操作。Unsafe位于sun.misc包下,jdk中的并发编程包juc(java.util.concurrent)基本全部靠Unsafe实现,由此可见其重要性。它的目的是为了增强java语言直接操作底层资源的能力,无疑带来很多方便。但是,使用的同时就得额外小心!它的总体作用如下:
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

CAS译为Compare And Swap,它是乐观锁的一种实现。假设内存值为o,预期值为expect,想要更新成得值为x,当且仅当内存值o等于预期值expect时,才将o更新为x。 这样可以有效避免多线程环境下的同步问题。
在unsafe中,实现CAS算法通过cpu的原子指令cmpxchg实现,它对应的方法如下:

@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. 其它操作

Unsafe还提供了操作系统级别的方法如获取内存页的大小public native int pageSize();,获取系统指针大小public native int addressSize();
jdk8还加入了新的方法,内存屏障,它的目的是为了防止指令重排序(编译器为了优化速度,会在保证单线程不出错的情况下将某些代码的顺序调换,比如先分配内存,或者先返回引用等,这样在多线程环境下就会出错),jdk1.8引入的StampedLock就是基于此实现的乐观读写锁。:

//内存屏障,禁止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"));
    }
}
<think> 嗯,用户想要详细了解Java中的Unsafe操作。用户提供了5个站内引用,都是关于Unsafe类的解析资料。这些引用内容相当全面,涵盖了Unsafe的定义、功能、风险以及应用场景。 从用户的问题来看,ta应该是有一定Java基础的开发者,可能正在学习底层机制或者遇到需要直接操作内存的场景。用户特意强调要"详细解析",说明ta不需要基础介绍,而是希望深入理解其原理和使用方法。 Unsafe类确实是个值得深入探讨的话题。它是Java留给开发者的后门,提供了类似C语言指针操作的能力。但引用[1]和[4]都强调了它的"不安全"特性——会破坏Java的内存安全模型。 考虑从这几个维度来组织回答: 首先需要解释什么是Unsafe类,它在Java生态中的特殊地位。引用[2]提到它在rt.jar中,全名sun.misc.Unsafe,这个细节很重要。然后要说明获取Unsafe实例的特殊方式,因为它的构造函数是私有的。 关于核心功能部分,引用[2][3][4]提供了很好的框架。内存操作最基础也最危险,包括直接分配/释放内存;类操作涉及绕过构造器创建实例;对象操作可以突破访问限制;线程调度能直接挂起/恢复线程;CAS操作则是并发包的基础。 特别注意到引用[5]提到的有趣特性——抛出checked异常而不声明,这个冷知识值得单独列出。每个功能点都需要强调其风险,比如内存泄漏、指针错误等。 应用场景方面,引用[2]提到Netty/Hadoop/Kafka都在使用,但必须说明这些是框架层面的谨慎使用。最后安全警示要突出,引用[1][4]都警告了滥用后果。 在整理相关问题拓展时,重点考虑实际开发中可能遇到的困惑:如何合法获取实例?为什么框架要用它?替代方案有哪些?这些都是开发者最常问的。 最后需要反复检查:所有技术细节是否准确?风险警示是否足够醒目?是否避免了示例代码可能带来的误导?毕竟Unsafe的误用代价很高。 </think> 以下是对Java `Unsafe` 类的详细解析,结合其核心功能、使用风险及典型场景: --- ### **一、Unsafe 类概述** 1. **定义与定位** `sun.misc.Unsafe` 是位于 `rt.jar` 中的非公开类,提供**绕过JVM安全检查**的底层操作能力,允许直接操作内存、线程和对象[^1][^2][^4]。 - 所有方法均为 `native`(通过JNI调用C++实现)。 - 破坏Java的安全设计,故称“不安全”。 2. **获取实例** 由于构造函数私有化,需通过反射获取: ```java Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); ``` --- ### **二、核心功能解析[^2][^3][^4]** #### 1. **内存操作(类似C指针)** - **分配/释放堆外内存** ```java long address = unsafe.allocateMemory(1024); // 分配1KB内存 unsafe.setMemory(address, 1024, (byte) 0); // 初始化归零 unsafe.freeMemory(address); // 手动释放 ``` - **直接读写内存** ```java unsafe.putInt(address, 0x12345678); // 写入4字节整数 int value = unsafe.getInt(address); // 读取整数 ``` > ⚠️ 风险:内存泄漏、非法地址访问导致JVM崩溃。 #### 2. **对象与类操作** - **绕过构造器创建实例** ```java MyClass obj = (MyClass) unsafe.allocateInstance(MyClass.class); ``` - **修改字段值(无视访问权限)** ```java Field field = MyClass.class.getDeclaredField("secret"); long offset = unsafe.objectFieldOffset(field); unsafe.putInt(obj, offset, 100); // 强制修改private字段 ``` #### 3. **数组操作** - **获取数组元素偏移** ```java int baseOffset = unsafe.arrayBaseOffset(int[].class); // 首元素地址偏移 int indexScale = unsafe.arrayIndexScale(int[].class); // 元素间隔 long offset = baseOffset + indexScale * index; // 计算元素地址 ``` #### 4. **并发控制** - **CAS原子操作** ```java unsafe.compareAndSwapInt(obj, offset, expect, update); ``` - **内存屏障** ```java unsafe.loadFence(); // 读屏障 unsafe.storeFence(); // 写屏障 ``` #### 5. **异常操纵** ```java unsafe.throwException(new IOException()); // 抛出checked异常而不声明[^5] ``` --- ### **三、典型应用场景** 1. **高性能框架** - Netty:堆外内存(DirectByteBuffer)分配管理。 - Disruptor:无锁环形队列(CAS更新指针)。 - Kafka:高效序列化(直接操作内存)。 2. **绕过语言限制** - 动态生成类(结合 `defineClass`)。 - 对象浅拷贝(直接复制内存)。 --- ### **四、严重风险与限制[^1][^4]** 1. **移植性问题** `Unsafe` 类非Java标准,不同JDK实现可能变动或移除。 2. **内存安全** 直接内存操作易导致泄漏、越界访问(触发Segment Fault)。 3. **破坏封装性** 无视访问修饰符修改字段,破坏设计约束。 4. **版本兼容性** JDK9+ 中 `Unsafe` 被逐步迁移至 `jdk.unsupported` 模块,未来可能禁用。 --- ### **五、替代方案** | 功能 | 官方替代方案 | |-----------------|------------------------------| | 堆外内存 | `ByteBuffer.allocateDirect()`| | CAS操作 | `AtomicInteger` 等原子类 | | 内存屏障 | `VarHandle` (JDK9+) | | 字段偏移量操作 | `MethodHandles.Lookup` | > **结论**:`Unsafe` 是Java的“终极后门”,仅应在**高性能基础库**开发中由资深开发者谨慎使用,应用层代码应优先选择标准API[^2][^3]。 --- ### **相关问题** 1. **如何合法获取 `Unsafe` 实例而不触发SecurityManager拦截?** 2. **为什么 `Unsafe` 在JDK9+中被标记为“不推荐使用”?未来会被移除吗?** 3. **堆外内存(DirectByteBuffer)与 `Unsafe.allocateMemory()` 有何性能差异?** 4. **`Unsafe` 如何实现比标准锁更高性能的无锁数据结构?** 5. **有哪些安全机制可以防止滥用 `Unsafe` 类?** [^1]: Java Unsafe应用解析(核心功能与风险) [^2]: Java Unsafe技术解析(内存/类/并发操作) [^3]: Unsafe源码解析与实战场景(底层能力揭秘) [^4]: Unsafe的低级别操作与危险性 [^5]: 通过Unsafe抛出checked异常的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值