目录

T の weblog

X

Java中的Unsafe类

这是一个普通类的创建

public static void main(String[] args) throws InterruptedException {
        Person person = new Person();
    }

仅仅只需要一行代码就完成了对象的实例化

这是Unsafe类的创建

public class UnsafeInstance {
    
    public static Unsafe reflectGetUnsafe(){
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
    }
    
}

通过一个静态方法的调用,而方法中通过反射的方式创建出来Unsafe对象,为什么它需要通过这样的方式创建出来?

1. Unsafe的作用

要知道Unsafe为何这么难以创建,我们先要知道开发者为何对它进行这样的设计。

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等。这些方法在提升Java效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有类似C语言操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确的使用Unsafe类会使得程序出错的概率变大很多,使得Java这种安全的语言变得不再安全,因此对于Unsafe的使用一定要慎重。

这是很多博客都会提到了,Unsafe在给Java带来操作内存空间能力的同时,也带来了风险性,所以给Unsafe的实例化增加门槛。

但是如果我们需要一个它,怎么获得呢?

2. 怎么得到一个Unsafe对象?

2.1 加载Unsafe相关方法的类

从getUnsafe方法的使用限制条件出发,通过Java命令行命令 -Xbootclasspath/a 把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获得Unsafe实例。

2.2 通过反射获取单例对象theUnsafe

就如开头举例的那样,通过反射的方式获取到 Unsafe对象。

public class UnsafeInstance {
    
    public static Unsafe reflectGetUnsafe(){
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
    }
    
}

现在你的手上有一个锤子(Unsafe)了,我们去哪找点钉子敲敲

3. Unsafe能干嘛

3.1 内存操作

这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。
如果你熟悉Netty,就是那个简化了Java中的NIO实现的Netty

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
image.png

在当中便是通过Unsafe去申请堆外内存。

那么在Unsafe类中,有哪些方法对应这些操作?

allocateMemory() 分配内存
reallocateMemory() 扩充内存
freeMemory() 释放内存
setMemory() 在给定的内存块中设置值
copyMemory() 内存拷贝
等等等

通常,我们在Java中创建的对象都处于堆内内存(heap) 中,堆内内存是由JVM所管控的Java进程内存,并且他们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆外内存。与之对应的使堆外内存,存在于JVM管控之外的内存区域,Java中堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法

3.2 为什么我们需要使用堆外内存?

其中一个原因便是对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿时间对应用的影响。

提升程序I/O操作的性能。通常在I/O通信的过程中,会存在于堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

这里可以引申阅读一下0拷贝

因为这样的特性,所以在Netty、MIMA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。

当说到Netty的时候,这些东西会被着重拿出来讲。

3.3 CAS相关

如果你清楚AtomicInteger的话,那么应该知道它实现的原理

在AtomicInteger的实现中,静态资源valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFiledOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicOffset对象中的value的内存地址,从而根据CAS实现对value字段的原子操作。

3.4 线程调度

包括线程挂起、恢复、锁机制等方法。

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);

方法parkunpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一只阻塞直到超时或者中断等条件出现unpark可以终止一个挂起的线程,使其恢复正常。

这种对于线程的操作在Java中的应用便是Java锁同步器框架AbstractQueuedSynchronizer,就是通过调用LockSupport.park()LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现的。

3.5 内存屏障

在Java中引入,用于定义内存屏障(是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才开始之后的操作),也就是防止指令重排

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

3.5.1 内存屏障在代码中给我们带来了什么?

在Java8中引入了一种锁机制,StampedLock,它可以堪称读写锁的一个改进版本。StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程长时间处于空阻塞状态。由于StampedLock提供的乐观读锁不会阻塞写程序获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不一致问题,所以当使用StampedLock的乐观读锁时,需要遵从如下图用力中使用的模式来确保数据的一致性。
image.png
代码如下:

public class Point {

    private double x,y;

    private final StampedLock sl = new StampedLock();

    void move(double deltaX,double deltaY){
        long stamp = sl.writeLock();//使用写锁-独占模式

        try {
            x += deltaX;
            y += deltaY;
        }finally {
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin(){
        long stamp = sl.tryOptimisticRead();

        double currentX = x ,currentY = y;
        if (!sl.validate(stamp)){
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            }finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

}

如上图用例所示计算坐标点Point对象,包含点移动方法move及计算此点到原点的距离的方法distanceFromOrigin。在方法distanceFromOrigin中,首先,通过tryOptimisticRead方法获取乐观读标记;

然后从主内存中加载点的坐标值 (x,y);而后通过StampedLock的validate方法校验锁状态,判断坐标点(x,y)从主内存加载到线程工作内存过程中,主内存的值是否已被其他线程通过move方法修改,如果validate返回值为true,证明(x, y)的值未被修改,可参与后续计算;否则,需加悲观读锁,再次从主内存加载(x,y)的最新值,然后再进行距离计算。其中,校验锁状态这步操作至关重要,需要判断锁状态是
否发生改变,从而判断之前copy到线程工作内存中的值是否与主内存的值存在不一致。

下图为StampedLock.validate方法的源码实现,通过锁标记与相关常量进行位运算、比较来校验锁状态,在校验逻辑之前,会通过Unsafe的loadFence方法加入一个load内存屏障,目的是避免上图用例中步骤②和StampedLock.validate中锁状态校验运算发生重排序导致锁状态校验不准确的问题。

image.png


标题:Java中的Unsafe类
作者:MingGH
地址:https://runnable.run/articles/2021/03/13/1615608883205.html