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类在操作。
比如AtomicBoolean
的compareAndSet
方法:
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
(获取数组中元素的增量地址)等方法。arrayBaseOffset
与arrayIndexScale
配合起来使用,就可以定位数组中每个元素在内存中的位置。
由于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);
这部分包括了park
、unpark
等方法。
将一个线程进行挂起是通过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新引入的三个内存屏障函数,用于定义内存屏障,避免代码重排序。