简介

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
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐