Loading

Unsafe类详解

本博客系列是学习并发编程过程中的记录总结。由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅。


@

Unsafe类

java版本JDK1.8
本文首发于CSDN
主要参考Java多线程进阶(十二)—— J.U.C之atomic框架:Unsafe类

Unsafe简介

在正式的开讲 juc-atomic框架系列之前,有必要先来了解下Java中的Unsafe类。

Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。

我们进入到Unsafe类中你就会发现,他的方法都是native方法,他们调用本地接口(JNI)访问本地C++实现库来实现功能。

Unsafe使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。

其实咱们在之前就见过它很多次了。

J.U.C中的许多CAS方法,内部其实都是Unsafe类在操作。

比如AtomicBooleancompareAndSet方法:

public final boolean compareAndSet(boolean expect, boolean update) {
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

其中它的本质就是调用了unsafe.compareAndSwapInt方法。

//(如果对象中的字段值与期望值相等,则将字段值修改为x,然后返回true;否则返回
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);false):

它是一个native方法。

Unsafe类中CAS方法都是native方法,需要通过CAS原子指令完成。在讲AQS时,里面有许多涉及CLH队列的操作,其实就是通过Unsafe类完成的指针操作。

Unsafe对象的创建

Unsafe是一个final类,不能被继承,也没有公共的构造器,它使用了单例模式,只能通过工厂方法getUnsafe获得Unsafe的单例。

/**
Unsafe部分源码
*/
public final class Unsafe {
    private static final Unsafe theUnsafe;
    private Unsafe() {
    }

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        //限制了调用该方法的类的类加载器必须为BootstrapClassLoader
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
}

但是getUnsafe方法限制了调用该方法的类的类加载器必须为*BootstrapClassLoader*

Java中的类加载器可以大致划分为以下三类:

类加载器名称 作用
Bootstrap类加载器(Bootstrap ClassLoader) 主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是JVM自身的一部分,它负责将 【JDK的安装目录】/lib路径下的核心类库,如rt.jar
扩展类加载器(Extension ClassLoader) 该加载器负责加载【JDK的安装目录】jrelibext目录中的类库,开发者可以直接使用该加载器
系统类加载器(Application ClassLoader) 负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,也是默认的类加载器

所以在用户代码中直接调用getUnsafe方法,会抛出异常。因为用户自定义的类一般都是由系统类加载器加载的。

但是,是否就真的没有办法获取到Unsafe实例了呢?当然不是,要获取Unsafe对象的方法很多,这里给出一种通过反射的方法:

private static Unsafe getUnsafe(){
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        return unsafe;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

但是,除非对Unsafe的实现非常清楚,否则应尽量避免直接使用Unsafe来进行操作。

再从Unsafe的功能入手,它有着下面这几个功能:

  • 读写相关
  • 内存操作
  • 操纵对象属性
  • 操纵数组元素
  • 线程挂起与恢复
  • CAS
  • 内存操作

我们一项项的分析它。

一、读写相关(包括普通读写,volatile读写,有序写入等)

普通读写

通过Unsafe可以读写一个类的属性,即使这个属性是私有的,也可以对这个属性进行读写。

读写一个Object属性的相关方法

public native int getInt(Object var1, long var2);

public native void putInt(Object var1, long var2, int var4);

getInt用于从对象的指定偏移地址处读取一个int。putInt用于在对象指定偏移地址处写入一个int。其他的primitive type也有对应的方法。

Unsafe还可以直接在一个地址上读写

public native byte getByte(long var1);

public native void putByte(long var1, byte var3);

getByte用于从指定内存地址处开始读取一个byte。putByte用于从指定内存地址写入一个byte。其他的primitive type也有对应的方法。

volatile读写

普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。

public native int getIntVolatile(Object var1, long var2);

public native void putIntVolatile(Object var1, long var2, int var4);

getIntVolatile方法用于在对象指定偏移地址处volatile读取一个int。putIntVolatile方法用于在对象指定偏移地址处volatile写入一个int。

volatile读写相对普通读写是更加昂贵的,因为需要保证可见性和有序性,而与volatile写入相比putOrderedXX写入代价相对较低,putOrderedXX写入不保证可见性,但是保证有序性,所谓有序性,就是保证指令不会重排序。

有序写入

有序写入只保证写入的有序性,不保证可见性,就是说一个线程的写入不保证其他线程立马可见。

public native void putOrderedObject(Object var1, long var2, Object var4);

public native void putOrderedInt(Object var1, long var2, int var4);

public native void putOrderedLong(Object var1, long var2, long var4);

二、内存操作(包括分配内存、释放内存等)

这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。

//分配内存, 相当于C++的malloc函数  
public  native  long  allocateMemory(long bytes);
//扩充内存  
public  native  long  reallocateMemory(long address, 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); 
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等  
public  native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等  
public  native  void  putObject(Object o, long offset, Object x);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)  
public  native  byte  getByte(long address); 
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)  
public  native  void  putByte(long address, byte x);

getXXX和putXXX包含了各种基本类型的操作。

利用copyMemory方法,我们可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现clone方法,当然这通用的方法只能做到对象浅拷贝。

三、操纵对象属性

这部分包括了staticFieldOffset(静态域偏移)、defineClass(定义类)、defineAnonymousClass(定义匿名类)、ensureClassInitialized(确保类初始化)、objectFieldOffset(对象域偏移)等方法。

通过这些方法我们可以获取对象的指针,通过对指针进行偏移,我们不仅可以直接修改指针指向的数据(即使它们是私有的),甚至可以找到JVM已经认定为垃圾、可以进行回收的对象。

四、操纵数组元素

1. unsafe操作数组相关方法介绍

//获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);

//获取数组中元素的增量地址
public native int arrayIndexScale(Class arrayClass);

//对某个地址的Object赋值
public native void putObjectVolatile(Object o, long offset, Object x);

//获取某个地址的Object的值
public native Object getObjectVolatile(Object o, long offset);

这部分包括了arrayBaseOffset(获取数组第一个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等方法。arrayBaseOffsetarrayIndexScale配合起来使用,就可以定位数组中每个元素在内存中的位置。

由于Java的数组最大值为nteger.MAX_VALUE,使用Unsafe类的内存分配方法可以实现超大数组。实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。

五、线程挂起与恢复

public native void unpark(Object var1);

public native void park(boolean var1, long var2);

public native void monitorEnter(Object var1);

public native void monitorExit(Object var1);

public native boolean tryMonitorEnter(Object var1);

这部分包括了parkunpark等方法。

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。

monitorEnter方法和monitorExit方法用于加锁,Java中的synchronized锁就是通过这两个指令来实现的。

六、CAS

JUC中大量运用了CAS操作,可以说CAS操作是JUC的基础,因此CAS操作是非常重要的。Unsafe中提供了int,long和Object的CAS操作:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

1.什么是CAS?

CAS即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,对此Unsafe提供了一系列的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg

2.典型应用

CAS在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用。

CAS原理详见文章【并发编程的基石】CAS机制 (compareAndSwap)

七、内存屏障

public native void loadFence();

public native void storeFence();

public native void fullFence();

loadFence:保证在这个屏障之前的所有读操作都已经完成。
storeFence:保证在这个屏障之前的所有写操作都已经完成。
fullFence:保证在这个屏障之前的所有读写操作都已经完成。

这是在Java 8新引入的三个内存屏障函数,用于定义内存屏障,避免代码重排序。

posted @ 2022-02-02 21:18  程序员小小宇  阅读(475)  评论(0编辑  收藏  举报