• 一篇文章理解 Java 中的 Unsafe 类


    前言

    本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

    本专栏目录结构和参考文献请见100个问题搞定Java虚拟机


    概述

    在本文中,我们将讨论 sun.misc.Unsafe

    该类为我们提供了底层的机制,这些机制旨在仅供核心 Java 库使用,而不是由标准用户使用。


    获取 Unsafe 的实例

    首先,为了能够使用 Unsafe 类,我们需要得到一个实例——鉴于该类仅为内部使用而设计,这并不简单。

    获取实例的方法是通过静态方法 getUnsafe()

    值得注意的是,默认情况下这将抛出异常:SecurityException。

    幸运的是,我们可以使用反射获得实例:

    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    unsafe = (Unsafe) f.get(null);
    
    • 1
    • 2
    • 3

    使用 Unsafe 实例化类

    假设我们有一个简单的类,其构造函数在创建对象时设置变量值:

    class InitializationOrdering {
        private long a;
    
        public InitializationOrdering() {
            this.a = 1;
        }
    
        public long getA() {
            return this.a;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    当我们使用构造函数初始化该对象时,getA()方法将返回 1 的值:

    InitializationOrdering o1 = new InitializationOrdering();
    assertEquals(o1.getA(), 1);
    
    • 1
    • 2

    但我们可以使用 Unsafe 的 allocateInstance() 方法。

    它只会为我们的类分配内存,不会调用构造函数:

    InitializationOrdering o3 
      = (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class);
     
    assertEquals(o3.getA(), 0);
    
    • 1
    • 2
    • 3
    • 4

    请注意,由于没有调用构造函数,getA() 方法返回了 long 类型的默认值——0。


    更改 private 字段

    假设我们有一个有 private 字段的类:

    class SecretHolder {
        private int SECRET_VALUE = 0;
    
        public boolean secretIsDisclosed() {
            return SECRET_VALUE == 1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用 Unsafe 的putInt()方法,我们可以更改私有SECRET_VALUE字段的值

    SecretHolder secretHolder = new SecretHolder();
    
    Field f = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
    unsafe.putInt(secretHolder, unsafe.objectFieldOffset(f), 1);
    
    assertTrue(secretHolder.secretIsDisclosed());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    一旦我们通过反射调用获得字段,我们可以使用 Unsafe 将其值更改为任何其他 int 值。


    抛出异常

    通过 Unsafe 调用的代码不会像常规 Java 代码那样被编译器以同样的方式检查。

    我们可以使用 throwException() 方法来抛出任何异常,而不会限制调用者必须处理该异常,即使它是受检的(checked)异常:

    @Test(expected = IOException.class)
    public void givenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt() {
        unsafe.throwException(new IOException());
    }
    
    • 1
    • 2
    • 3
    • 4

    抛出经过检查的 IOException 后,我们不需要捕获它,也不需要在方法声明中指定它。

    关于 Java 中异常的实现原理请参考我的博客——JVM是如何处理异常的?


    堆外内存

    如果应用程序在 JVM 上的可用内存用完了,我们最终可能会被迫频繁地 GC。

    理想情况下,我们想要一个特殊的内存区域,在堆外并且不受 GC 程序控制。

    Unsafe 类的 allocateMemory() 方法使我们能够从堆中分配大对象,同时 GC 和 JVM 不会看到和考虑这些内存。

    这可能会非常有用,但我们需要记住,当不再需要时,需要手动管理此内存,并使用 freeMemory() 正确回收。

    假设我们想创建巨大的堆外内存字节数组,我们可以使用 allocateMemory() 方法来实现:

    class OffHeapArray {
        private final static int BYTE = 1;
        private long size;
        private long address;
    
        public OffHeapArray(long size) throws NoSuchFieldException, IllegalAccessException {
            this.size = size;
            address = getUnsafe().allocateMemory(size * BYTE);
        }
    
        private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe) f.get(null);
        }
    
        public void set(long i, byte value) throws NoSuchFieldException, IllegalAccessException {
            getUnsafe().putByte(address + i * BYTE, value);
        }
    
        public int get(long idx) throws NoSuchFieldException, IllegalAccessException {
            return getUnsafe().getByte(address + idx * BYTE);
        }
    
        public long size() {
            return size;
        }
        
        public void freeMemory() throws NoSuchFieldException, IllegalAccessException {
            getUnsafe().freeMemory(address);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    OffHeapArray的构造函数中,我们初始化给定大小的数组。

    我们正在address字段中存储数组的开头地址。

    set()方法用来处理索引和存储在数组中的给定值。

    get()方法使用其索引检索字节值,该索引是数组起始地址的偏移量。

    接下来,我们可以使用其构造函数来分配堆外内存的数组:

    long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
    OffHeapArray array = new OffHeapArray(SUPER_SIZE);
    
    • 1
    • 2

    我们可以将 N 个字节值放入此数组中,然后检索这些值,将其求和以测试我们的寻址是否正常工作:

    int sum = 0;
    for (int i = 0; i < 100; i++) {
        array.set((long) Integer.MAX_VALUE + i, (byte) 3);
        sum += array.get((long) Integer.MAX_VALUE + i);
    }
    
    assertEquals(array.size(), SUPER_SIZE);
    assertEquals(sum, 300);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最后,我们需要通过调用freeMemory()将内存释放回操作系统。


    CompareAndSwap 操作

    来自 java.util.concurrent 软件包的非常高效的实现,如AtomicInteger,底层就是使用下面不安全的 compareAndSwap()方法,以提供最佳性能。

    与 Java 中的标准悲观同步机制相比(如 sychronized),这种结构广泛应用于无锁(lock-free)算法中,这些算法可以利用 CAS 处理器指令极大的加速我们的程序。

    我们可以使用 Unsafe 的 compareAndSwapLong() 方法构建基于 CAS 的计数器:

    class CASCounter {
        private Unsafe unsafe;
        private volatile long counter = 0;
        private long offset;
    
        private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe) f.get(null);
        }
    
        public CASCounter() throws Exception {
            unsafe = getUnsafe();
            offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
        }
    
        public void increment() {
            long before = counter;
            while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
                before = counter;
            }
        }
    
        public long getCounter() {
            return counter;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在 CASCounter 构造函数中,我们获取counter字段的地址,以便稍后在increment()方法中使用它。

    该字段需要声明为volatile,以便对正在编写和读取此值的所有线程可见。

    关于volatile请参考我的博客——volatile 的实现原理是什么?

    我们使用objectFieldOffset()方法来获取offset字段的内存地址。

    该类最重要的部分是increment()方法。

    我们在 while 循环中使用compareAndSwapLong()来增加之前获取的值,检查之前获取的值在我们获取后是否发生了变化。

    如果是这样,那么我们将重试该操作,直到我们成功。

    这里没有阻塞,这就是为什么这被称为无锁(lock-free)算法。

    我们可以通过从多个线程中增加共享计数器来测试我们的代码:

    int NUM_OF_THREADS = 1_000;
    int NUM_OF_INCREMENTS = 10_000;
    ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
    CASCounter casCounter = new CASCounter();
    
    IntStream.rangeClosed(0, NUM_OF_THREADS - 1)
      .forEach(i -> service.submit(() -> IntStream
        .rangeClosed(0, NUM_OF_INCREMENTS - 1)
        .forEach(j -> casCounter.increment())));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接下来,要断言(assert)计数器的状态是正确的,我们可以从中获取计数器值:

    assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());
    
    • 1

    Park/Unpark

    Unsafe API 中有两个方法,通常被 JVM 用来上下文切换线程。

    当线程等待某些操作时,JVM 可以使用 Unsafe 类的park()方法阻塞该线程。

    当线程被阻塞并需要再次运行时,JVM 使用 unpark()方法。

    这里举一个典型的例子,LockSupport 中的 park/unpark 底层就是调用的 Unsafe API 中的 park/unpark。

    关于 LockSupport 请参考我的博客——LockSupport 是什么?怎么用?

  • 相关阅读:
    龙格-库塔(Runge-Kutta)方法C++实现
    starrocks中unnest方法
    2023 NewStarCTF --- wp
    网络通信IO模型上
    【鸿蒙OH-v5.0源码分析之 Linux Kernel 部分】003 - vmlinux.lds 链接脚本文件源码分析
    HTML简介与常见标签
    centos8 安装nginx
    人工神经网络和卷积神经网络不同之处
    程序员有必要考个 985 非全日制研究生嘛?
    用 JHipster Azure Spring Apps 构建和部署 Spring 应用
  • 原文地址:https://blog.csdn.net/Shockang/article/details/125610298