推荐链接:
总结——》【Java】
总结——》【Mysql】
总结——》【Redis】
总结——》【Kafka】
总结——》【Spring】
总结——》【SpringBoot】
总结——》【MyBatis、MyBatis-Plus】
Java——》Unsafe源码分析
一、概念
并发作为 Java 中非常重要的一部分,其内部大量使用了 Unsafe 类,它为 java.util.concurrent 包中的类提供了底层支持。然而 Unsafe 并不是 JDK 的标准,它是 Sun 的内部实现,存在于 sun.misc 包中,在 Oracle 发行的 JDK 中并不包含其源代码。
虽然我们在一般的并发编程中不会直接用到 Unsafe,但是很多 Java 基础类库与诸如 Netty、Cassandra 和 Kafka 等高性能库都采用它,它在提升 Java 运行效率、增强 Java 语言底层操作能力方面起了很大作用。
二、作用
Unsafe 为调用者提供执行非安全操作的能力,由于返回的 Unsafe 对象可以读写任意的内存地址数据,调用者应该小心谨慎的使用改对象,一定不用把它传递到非信任代码。该类的大部分方法都是非常底层的操作,并牵涉到一小部分典型的机器都包含的硬件指令,编译器可以对这些进行优化。
- 绕过 JVM 直接修改内存(对象)
- 使用硬件 CPU 指令实现 CAS 原子操作
三、源码
本文使用 OpenJDK(jdk8u)中 Unsafe 的源码
1、获取实例
public final class Unsafe {
// 本地静态对象,在静态块中初始化
private static final Unsafe theUnsafe;
// 本地静态方法,在静态块中执行
private static native void registerNatives();
// 静态块
static {
registerNatives();
Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
theUnsafe = new Unsafe();
}
// 私有构造函数,该类实例是单例的,不能实例化,只能通过 getUnsafe() 方法获取实例
private Unsafe() {}
// 静态单例方法,获取实例
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
}
1)@CallerSensitive
说明该方法的调用者不是由系统类加载器(bootstrap classloader)加载,则将抛出 SecurityException。所以默认情况下,应用代码调用此方法将抛出异常。
2)通过反射获取 Unsafe 实例
Field f= Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
U= (Unsafe) f.get(null);
此处通过反射获取类的静态字段,这样就绕过了 getUnsafe() 的安全限制。也可以通过反射获取构造方法再实例化,但这样违法了该类单例的原则,并且在使用上可能会有其它问题,所以不建议这样做。
2、获取指定变量的值
// 获取指定变量的值
public native int getInt(Object o, long offset);
// 获取指定字段的偏移地址
public native long objectFieldOffset(Field f);
// 获取指定静态字段的位置
public native Object staticFieldBase(Field f);
// 获取指定静态字段在该类的偏移地址
public native long staticFieldOffset(Field f);
// 获取指定数组类第一个元素在内存中的偏移地址
public native int arrayBaseOffset(Class arrayClass);
// 获取指定数组类的每个元素在内存中的所占用的字节
public native int arrayIndexScale(Class arrayClass);
3、更新指定变量的值
// 更新指定变量的值
// o :java堆对象,可以为null
// offset :指定变量在该对象中偏移地址,如果o为null则是内存的绝对地址
public native void putInt(Object o, long offset, int x);
// 以下代码和上面方法差不多,只是针对不同的数据类型
public native void putObject(Object o, long offset, Object x);
public native boolean getBoolean(Object o, long offset);
public native void putBoolean(Object o, long offset, boolean x);
public native byte getByte(Object o, long offset);
public native void putByte(Object o, long offset, byte x);
public native short getShort(Object o, long offset);
public native void putShort(Object o, long offset, short x);
public native char getChar(Object o, long offset);
public native void putChar(Object o, long offset, char x);
public native long getLong(Object o, long offset);
public native void putLong(Object o, long offset, long x);
public native float getFloat(Object o, long offset);
public native void putFloat(Object o, long offset, float x);
public native double getDouble(Object o, long offset);
public native void putDouble(Object o, long offset, double x);
4、分配本地内存
提供了动态获取/释放本地方法区内存的功能
/**
* 分配指定大小的一块本地内存。分配的这块内存不会初始化,它们的内容通常是没用的数据
* 返回的本地指针不会是 0,并且该内存块是连续的。调用 freeMemory 方法可以释放此内存,调用
* reallocateMemory 方法可以重新分配
*/
public native long allocateMemory(long bytes);
/**
* 重新分配一块指定大小的本地内存,超出老内存块的字节不会被初始化,它们的内容通常是没用的数据
* 当且仅当请求的大小为 0 时,该方法返回的本地指针会是 0。
* 该内存块是连续的。调用 freeMemory 方法可以释放此内存,调用 reallocateMemory 方法可以重新分配
* 参数 address 可以是 null,这种情况下会分配新内存(和 allocateMemory 一样)
*/
public native long reallocateMemory(long address, long bytes);
/**
* 将给定的内存块的所有字节设置成固定的值(通常是 0)
* 该方法通过两个参数确定内存块的基准地址,就像在 getInt(Object,long) 中讨论的,它提供了 double-register 地址模型
* 如果引用的对象是 null, 则 offset 会被当成绝对基准地址
* 该写入操作是按单元写入的,单元的字节大小由地址和长度参数决定,每个单元的写入是原子性的。如果地址和长度都是 8 的倍数,则一个单元为 long
* 型(一个单元 8 个字节);如果地址和长度都是 4 的倍数,则一个单元为 int 型(一个单元 4 个字节);
* 如果地址和长度都是 2 的倍数,则一个单元为 short 型(一个单元 2 个字节);
*/
public native void setMemory(Object o, long offset, long bytes, byte value);
//将给定的内存块的所有字节设置成固定的值(通常是 0)
//就像在 getInt(Object,long) 中讨论的,该方法提供 single-register 地址模型
public void setMemory(long address, long bytes, byte value) {
setMemory(null, address, bytes, value);
}
//复制指定内存块的字节到另一内存块
//该方法的两个基准地址分别由两个参数决定
public native void copyMemory(Object srcBase, long srcOffset,
Object destBase, long destOffset,
long bytes);
//复制指定内存块的字节到另一内存块,但使用 single-register 地址模型
public void copyMemory(long srcAddress, long destAddress, long bytes) {
copyMemory(null, srcAddress, null, destAddress, bytes);
}
//释放通过 allocateMemory 或者 reallocateMemory 获取的内存,如果参数 address 是 null,则不做任何处理
public native void freeMemory(long address);
1)Java函数对应的C函数
| Java函数 | 对应的C函数 | 描述 |
|---|---|---|
| allocateMemory | malloc | 用于分配一个全新的未使用的连续内存,但该内存不会初始化,即不会被清零 |
| reallocateMemory | realloc | 用于内存的缩容或扩容,有两个参数,从 malloc 返回的地址和要调整的大小,该函数和 malloc 一样,不会初始化,它能保留之前放到内存里的值,很适合用于扩容 |
| freeMemory | free | 用于释放内存,该方法只有一个地址参数,那它如何知道要释放多少个字节呢?其实在 malloc 分配内存的时候会多分配 4 个字节用于存放该块的长度,比如 malloc(10) 其实会花费 14 个字节。理论上讲能分配的最大内存是 4G(2^32-1)。在 hotspot 虚拟机的设计中,数组对象也有 4 个字节用于存放数组长度,那么在 hotspot 中,数组的最大长度就是 2^32-1,这样 free 函数只要读取前 4 个字节就知道要释放多少内存了(10+4) |
| setMemory | memset | 一般用于初始化内存,可以设置初始化内存的值,一般初始值会设置成 0,即清零操作 |
2)示例
// 申请内存
long address=U.allocateMemory(10);
// 写入内存
U.setMemory(address,10,(byte)1);
/**
* 1的二进制码为00000001,int为四个字节,U.getInt将读取四个字节,
* 读取的字节为00000001 00000001 00000001 00000001
*/
int i=0b00000001000000010000000100000001;
// 读取内存
System.out.println(i==U.getInt(address));
// 释放内存
U.freeMemory(address);
5、获取指针的大小
基本类型不是用指针表示的,它是直接存储的值。
一般情况下,我们会说在 Java 中,基本类型是值传递,对象是引用传递。
Java 官方的表述是在任何情况下 Java 都是值传递。
基本类型是传递值本身,对象类型是传递指针的值。
| 虚拟机 | 指针大小 |
|---|---|
| 32 位 | 4 |
| 64 位 | 默认:8 |
| 压缩:4(-XX:-UseCompressedOops) | |
32 位虚拟机: 4
64 位虚拟机: 8(默认),开启指针压缩功能(**-XX:-UseCompressedOops)**4
/**
* 获取本地指针所占用的字节大小,值为 4 或者 8。其他基本类型的大小由其内容决定
*/
public native int addressSize();
/** The value of {@code addressSize()} */
public static final int ADDRESS_SIZE = theUnsafe.addressSize();
/**
* 本地内存页大小,值为 2 的 N 次方
*/
public native int pageSize();
6、CAS相关
这几个方法应该是最常用的方法了,用于实现原子性的 CAS 操作,这些操作可以避免加锁,一般情况下,性能会更好, java.util.concurrent 包下很多类就是用的这些 CAS 操作而没有用锁。
/**
* 如果变量的值为预期值,则更新变量的值,该操作为原子操作
* 如果修改成功则返回true
*/
public final native boolean compareAndSwapObject(Object o, long offset,
Object expected,
Object x);
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
public final native boolean compareAndSwapLong(Object o, long offset,
long expected,
long x);
//下面的方法包含基于 CAS 的 Java 实现,用于不支持本地指令的平台
/**
* 在给定的字段或数组元素的当前值原子性的增加给定的值
* @param o 字段/元素所在的对象/数组
* @param offset 字段/元素的偏移
* @param delta 需要增加的值
* @return 原值
* @since 1.8
*/
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
/**
* 将给定的字段或数组元素的当前值原子性的替换给定的值
* @param o 字段/元素所在的对象/数组
* @param offset 字段/元素的偏移
* @param newValue 新值
* @return 原值
* @since 1.8
*/
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
public final long getAndSetLong(Object o, long offset, long newValue) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, newValue));
return v;
}
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
}
7、线程相关
阻塞和释放当前线程,java.util.concurrent 中的锁就是通过这两个方法实现线程阻塞和释放的。
/**
* 释放当前阻塞的线程。如果当前线程没有阻塞,则下一次调用 park 不会阻塞。这个操作是"非安全"的
* 是因为调用者必须通过某种方式保证该线程没有被销毁
*
*/
public native void unpark(Object thread);
/**
* 阻塞当前线程,当发生如下情况时返回:
* 1、调用 unpark 方法
* 2、线程被中断
* 3、时间过期
* 4、spuriously
* 该操作放在 Unsafe 类里没有其它意义,它可以放在其它的任何地方
*/
public native void park(boolean isAbsolute, long time);
8、CPU相关
/**
*获取一段时间内,运行的任务队列分配到可用处理器的平均数(平常说的 CPU 使用率)
*
*/
public native int getLoadAverage(double[] loadavg, int nelems);
9、内存屏障相关
这是实现内存屏障的几个方法,类似于 volatile 的语意,保证内存可见性和禁止重排序。这几个方法涉及到 JMM(Java 内存模型)。
//确保该栏杆前的读操作不会和栏杆后的读写操作发生重排序
public native void loadFence();
//确保该栏杆前的写操作不会和栏杆后的读写操作发生重排序
public native void storeFence();
//确保该栏杆前的读写操作不会和栏杆后的读写操作发生重排序
public native void fullFence();
//抛出非法访问错误,仅用于VM内部
private static void throwIllegalAccessError() {
throw new IllegalAccessError();
}