Java魔法类之Unsafe源码分析
一、简介
Unsafe
类在JDK
源码中被广泛使用,在Spark
使用off-heap memory
时也会使用到,该类功能很强大,涉及到类加载机制(深入理解ClassLoader
工作机制),其实例一般情况是获取不到的,源码中的设计是采用单例模式,不是系统加载初始化就会抛出SecurityException
异常。
Java
不能直接访问操作系统底层,这个类的提供了一些绕开JVM
的更底层功能,基于它的实现可以提高效率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是Unsafe
的,它所分配的内存需要手动free
(不被GC
回收)。如果对Unsafe
类理解的不够透彻,就进行使用的话,就等于给自己挖了无形之坑,最为致命。
Unsafe
类,来源于sun.misc
包。该类封装了许多类似指针操作,可以直接进行内存管理、操纵对象、阻塞/唤醒线程等操作。Java
本身不直接支持指针的操作,所以这也是该类命名为Unsafe
的原因之一。
二、CAS
CAS
(compare-and-swap
):直译即比较并交换,提供原子化的读改写能力,是Java
并发中所谓lock-free
(无锁)机制的基础。JAVA 1.5
开始引入了CAS
,主要代码都放在JUC
的atomic
包下。
JUC
中的许多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
方法是个native
方法。(如果对象中的字段值与期望值相等,则将字段值修改为x
,然后返回true
;否则返回false
):
/**
* 比较o的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
*
* @param o 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
入参的含义如下:
参数名称 | 含义 |
---|---|
o |
需要修改的对象 |
offset |
需要修改的字段到对象头的偏移量(通过偏移量,可以快速定位修改的是哪个字段) |
expected |
期望值 |
x |
要设置的值 |
2.1 工作流程
CAS
是一种无锁算法,CAS
有3
个操作数,内存值V
,旧的预期值E
,要修改的新值N
。
- 当
E == V
时,修改V
值为N,返回
true`。 - 当
E != V
时,修改失败,返回false
。
2.2 底层实现
JAVA
中的CAS
操作都是通过sun
包下Unsafe
类实现,而Unsafe
类中的方法都是native
方法,由JVM
本地实现,是JNI
链接通过C++
代码实现。CAS
是CPU
指令级的操作,只有一步原子操作,所以非常快。
例如对int
变量的CAS
操作,就是通过调用Unsafe#compareAndSwapInt()
来完成。Hotsport
虚拟机对compareAndSwapInt()
的实现是通过调用Atomic::cmpxchg
方法完成,这个cmpxchg
在不同的操作系统和CPU
架构模式下都不一样,Linux_X86
架构下,CAS
最底层就是通过cmpxchgl
汇编指令来完成,但是因为CAS
保证了原子性没有保证可见性,所以Hotspot
在cmpxchgl
前加入了LOCK_IF_MP
判断是否为多核处理架构,如果是多核则在汇编指令前加入CPU
的Lock
前缀指令来保证可见性问题。所以Java
的CAS
机制既能保证原子性也能保证可见性。
2.3 缺陷
2.3.1 Unsafe中的CAS自旋方式缺陷
cmpxchgl
汇编指令本身没有自旋的功能,JDK
中原子类和unsafe
类提供的CAS
是有while
自旋操作的,但是如果在高并发场景下对共享变量修改时,会让大量的线程修改失败转而进行自旋,此时CPU
会因此大量的自旋从而CPU
开销变大,CPU
利用率降低。并且,JVM
在多核架构下还会添加Lock
前缀指令造成总线事务的攀升,总线事务嗅探也变得极为繁忙,总线带宽打满,进而造成总线风暴。
如果JVM
能支持处理器提供的pause
指令那么效率会有一定的提升,pause
指令有两个作用:
- 它可以延迟流水线执行指令(
de-p ip eline
),使CPU
不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 - 它可以避免在退出循环的时候因内存顺序冲突(
memory order violation
)而引起CPU
流水线被清空(CPU pipeline flush
),从而提高CPU
的执行效率。
2.3.2 只能保证一个共享变量的原子操作缺陷
当对一个共享变量执行操作时,可以使用循环CAS
的方式来保证原子操作,JDK
也提供了AtomicReference
类来优化这个问题,但是对多个共享变量操作时,循环CAS
就无法保证操作的原子性。
2.3.3 ABA问题
ABA
问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A
就会变成1A-2B-3A
。
JDK
提供了版本号机制AtomicStampedReference<V>
类来解决ABA
问题。
三、使用
3.1 Unsafe.allocateInstance
public class Test {
public static void main(String[] args) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
A o1 = new A(); // constructor
o1.a(); // prints 1
A o2 = A.class.newInstance(); // reflection
o2.a(); // prints 1
A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
o3.a(); // prints 0
}
static class A {
private long a; // not initialized value, default 0
public A() {
this.a = 1; // initialization
}
public long a() {
return this.a;
}
}
}
allocateInstance()
根本没有进入构造方法,对于单例模式,简直是噩梦。
3.2 内存修改,绕过安全检查器(Unsafe.objectFieldOffset)
public class Test {
public static void main(String[] args) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
Guard guard = new Guard();
guard.giveAccess(); // false, no access
// bypass
Field field = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(guard, unsafe.objectFieldOffset(field), 42); // memory corruption
guard.giveAccess(); // true, access granted
}
static class Guard {
private int ACCESS_ALLOWED = 1;
public boolean giveAccess() {
return 42 == ACCESS_ALLOWED;
}
}
}
通过获取目标对象的字段在内存中的offset
,并使用putInt()
方法,类的ACCESS_ALLOWED
被修改。在已知类结构的时候,数据的偏移总是可以获得的(与c++中的类中数据的偏移计算是一致的)。
3.3 sizeOf计算内存大小(Unsafe.getDeclaredFields和Unsafe.objectFieldOffset)
public class Test {
public static void main(String[] args) throws Exception {
Guard guard = new Guard();
sizeOf(guard); // 16, the size of guard
}
static class Guard {
private int ACCESS_ALLOWED = 1;
public boolean giveAccess() {
return 42 == ACCESS_ALLOWED;
}
}
public static long sizeOf(Object o) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
HashSet<Field> fields = new HashSet();
Class c = o.getClass();
while (c != Object.class) {
for (Field field : c.getDeclaredFields()) {
if ((field.getModifiers() & Modifier.STATIC) == 0) {
fields.add(field);
}
}
c = c.getSuperclass();
}
// get offset
long maxSize = 0;
for (Field field : fields) {
long offset = unsafe.objectFieldOffset(field);
if (offset > maxSize) {
maxSize = offset;
}
}
return ((maxSize / 8) + 1) * 8; // padding
}
}
算法的思路非常清晰:从底层子类开始,依次取出它自己和它的所有超类的非静态域,放置到一个HashSet
中(重复的只计算一次,Java
是单继承),然后使用objectFieldOffset()
获得一个最大偏移,最后还考虑了对齐。
3.4 实现Java浅复制
public class Test {
public static void main(String[] args) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
Guard guard = new Guard();
shallowCopy(guard);
}
private static Unsafe getUnsafe() {
return unsafe;
}
static Object shallowCopy(Object obj) throws Exception {
long size = sizeOf(obj);
long start = toAddress(obj);
long address = getUnsafe().allocateMemory(size);
getUnsafe().copyMemory(start, address, size);
return fromAddress(address);
}
static long toAddress(Object obj) {
Object[] array = new Object[]{obj};
long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
return normalize(getUnsafe().getLong(array, baseOffset));
}
static Object fromAddress(long address) {
Object[] array = new Object[] {null};
long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
getUnsafe().putLong(array, baseOffset, address);
return array[0];
}
static class Guard {
private int ACCESS_ALLOWED = 1;
public boolean giveAccess() {
return 42 == ACCESS_ALLOWED;
}
}
public static long sizeOf(Object o) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
HashSet<Field> fields = new HashSet();
Class c = o.getClass();
while (c != Object.class) {
for (Field field : c.getDeclaredFields()) {
if ((field.getModifiers() & Modifier.STATIC) == 0) {
fields.add(field);
}
}
c = c.getSuperclass();
}
// get offset
long maxSize = 0;
for (Field field : fields) {
long offset = unsafe.objectFieldOffset(field);
if (offset > maxSize) {
maxSize = offset;
}
}
return ((maxSize / 8) + 1) * 8; // padding
}
}
思路很简单,利用Unsafe.copyMemory()
,将老地址及其指向的对象的size
,拷贝到新的内存地址上。并且浅复制函数可以应用于任意java
对象,它的尺寸是动态计算的。(在实际测试的时候,执行unsafe.copyMemory
时,JVM
会输出hs_err_pid.log
日志然后挂掉,该问题还有待排查)
3.5 隐藏密码
一般密码都要存成byte[]
或者char[]
数组,为什么呢?
因为我们使用完了,可以直接将他们设为null
。但如果密码存在String
中,将其设为null
,密码实际任然存在在内存中,等到GC
后,才能被释放,就很不安全。
所以当我们把密码字段存储在String
中时,在密码字段使用完之后,最安全的做法是:将它的值覆盖。
很多不再需要的,但是又是比较机密的对象,想快点消灭证据,都可以通过这种方法来消除。
Field stringValue = String.class.getDeclaredField("value");
stringValue.setAccessible(true);
char[] mem = (char[]) stringValue.get(password);
for (int i = 0; i < mem.length; i++) {
mem[i] = '?';
}
3.6 多继承
在java
中没有多继承,除非我们能在这些不同的类互相强制转换。
long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
long strClassAddress = normalize(getUnsafe().getInt("", 4L));
getUnsafe().putAddress(intClassAddress + 36, strClassAddress);
上面这段代码将String
添加为int
的父类,所以我们转换的时候就不会报错了。
(String) (Object) (new Integer(666))
3.7 动态加载类
标准的动态加载类的方法是Class.forName()
(在编写jdbc程序时,记忆深刻),使用Unsafe
也可以动态加载java
的class
文件。
我们可以在程序运行是动态加载编译好的.class
文件,不明白ClassLoader
的可以参考<深入理解ClassLoader
工作机制>具体是怎么实现的呢?
// 读取一个class文件到byte数组中
byte[] classContents = getClassContent();
// 通过Unsafe.defineClass()来加载对应的Class
Class c = getUnsafe().defineClass(null, classContents, 0, classContents.length);
// 调用
c.getMethod("a").invoke(c.newInstance(), null); // 1
private static byte[] getClassContent() throws Exception {
File f = new File("/home/mishadoff/tmp/A.class");
FileInputStream input = new FileInputStream(f);
byte[] content = new byte[(int)f.length()];
input.read(content);
input.close();
return content;
}
动态加载、代理、切片等功能中可以应用。
3.8 快速序列化
我们都知道标准的Java Serializable
速度很慢,它还限制类必须有public
无参构造函数。Externalizable
相对好些,但它需要为序列化的类指定schema
。更流行的如kyro
,在小内存的情况下不适用。
序列化过程:
- 用反射构建对象的
schema
。 - 用
Unsafe
中的getLong
,getInt
,getObject
等方法来检索对象中字段的值。 - 增加对象对应的类的标示符,来标记序列化结果。
- 将结果写入文件或者输出流。(可以增加压缩来减小序列化结果)。
反序列化过程:
- 使用
Unsafe.allocateInstance()
来实例化一个被序列化的对象。(不需要执行构造函数) - 构建
schema
,同序列化过程中的第一步。 - 从文件或者输入流中读取所有的字段。
- 用
Unsafe
中的putLong
,putInt
,putObject
等方法来填充该对象。
在kyro
序列化中,也有一些使用Unsafe
的尝试:https://code.google.com/archive/p/kryo/issues/75
3.9 在非Java堆中分配内存
使用java
的new
会在堆中为对象分配内存,并且对象的生命周期内,会被JVM GC
管理。Unsafe
分配的内存,不受Integer.MAX_VALUE
的限制,并且分配在非堆内存,使用它时,需要非常谨慎:忘记手动回收时,会产生内存泄露;非法的地址访问时,会导致JVM
崩溃。在需要分配大的连续区域、实时编程(不能容忍JVM
延迟)时,可以使用它。java.nio
使用这一技术。Spark
中的Netty
也使用了这个技术。在Spark UnsafeMemoryAllocator
源码中我们可以看到其使用了Unsafe.allocateMemory()
并会抛出OOM
异常:
@Override
public MemoryBlock allocate(long size) throws OutOfMemoryError {
long address = Platform.allocateMemory(size);
MemoryBlock memory = new MemoryBlock(null, address, size);
if (MemoryAllocator.MEMORY_DEBUG_FILL_ENABLED) {
memory.fill(MemoryAllocator.MEMORY_DEBUG_FILL_CLEAN_VALUE);
}
return memory;
}
3.10 大数组
Java
的数组最大容量受常量Integer.MAX_VALUE
的限制,如果我们用直接申请内存的方式去创建数组,那么数组大小只会收到堆的大小的限制。
private static Unsafe unsafe;
public static void main(String[] args) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
// 设置数组大小为Integer.MAX_VALUE的2倍
long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);
System.out.println("Array size:" + array.size()); // 4294967294
int sum = 0;
for (int i = 0; i < 100; i++) {
array.set((long) Integer.MAX_VALUE + i, (byte) 3);
sum += array.get((long) Integer.MAX_VALUE + i);
}
System.out.println("Sum of 100 elements:" + sum); // 300
}
private static Unsafe getUnsafe() {
return unsafe;
}
static class SuperArray {
private final static int BYTE = 1;
private long size;
private long address;
public SuperArray(long size) {
this.size = size;
address = getUnsafe().allocateMemory(size * BYTE);
}
public void set(long i, byte value) {
getUnsafe().putByte(address + i * BYTE, value);
}
public int get(long idx) {
return getUnsafe().getByte(address + idx * BYTE);
}
public long size() {
return size;
}
}
输出结果:
Array size:4294967294
Sum of 100 elements:300
四、对象的创建
Unsafe
是一个final
类,不能被继承,也没有公共的构造器,只能通过工厂方法getUnsafe
获得Unsafe
的单例。以下是部分源码:
public final class Unsafe {
private static final Unsafe theUnsafe;
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
static {
registerNatives();
Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
theUnsafe = new Unsafe();
}
// ...
}
但是getUnsafe
方法限制了调用该方法的类的类加载器必须为Bootstrap ClassLoader
。
Java
中的类加载器可以大致划分为以下三类:
类加载器名称 | 作用 |
---|---|
Bootstrap ClassLoader ( Bootstrap 类加载器) |
主要加载的是JVM 自身需要的类,这个类加载使用C++ 语言实现的,是JVM 自身的一部分,它负责将【 JDK 的安装目录】/lib路径下的核心类库,如rt.jar |
Extension ClassLoader (扩展类加载器) |
该加载器负责加载【JDK 的安装目录】jre/lib/ext 目录中的类库,开发者可以直接使用该加载器 |
Application ClassLoader (系统类加载器) |
负责加载用户类路径(ClassPath )所指定的类,开发者可以直接使用该类加载器,也是默认的类加载器 |
所以在用户代码中直接调用getUnsafe
方法,会抛出异常。因为用户自定义的类一般都是由系统类加载器加载的。
但是,是否就真的没有办法获取到Unsafe
实例了呢?当然不是,要获取Unsafe
对象的方法很多,这里给出一种通过反射的方法:
// 通过反射实例化Unsafe
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
但是,除非对Unsafe
的实现非常清楚,否则应尽量避免直接使用Unsafe
来进行操作。
五、核心方法
Unsafe
类大部分都是native
方法,具体实现由JVM
完成:unsafe.cpp
概括的来说,Unsafe
类实现功能可以被分为下面8
类:
- 内存操作
- 内存屏障
- 对象操作
- 数据操作
- CAS操作
- 线程调度
- Class操作
- 系统信息
5.1 内存操作
如果你是一个写过C
或者C++
的程序员,一定对内存操作不会陌生,而在Java
中是不允许直接对内存进行操作的,对象内存的分配和回收都是由JVM
自己实现的。但是在Unsafe
中,提供的下列接口可以直接进行内存操作:
// 分配新的本地空间
public native long allocateMemory(long bytes);
// 重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
// 将内存设置为指定值
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);
// 清除内存
public native void freeMemory(long address);
使用下面的代码进行测试:
private void memoryTest() {
int size = 4;
long addr = unsafe.allocateMemory(size);
long addr3 = unsafe.reallocateMemory(addr, size * 2);
System.out.println("addr: " + addr);
System.out.println("addr3: " + addr3);
try {
unsafe.setMemory(null, addr, size, (byte)1);
for (int i = 0; i < 2; i++) {
unsafe.copyMemory(null, addr, null, addr3 + size * i, 4);
}
System.out.println(unsafe.getInt(addr));
System.out.println(unsafe.getLong(addr3));
} finally {
unsafe.freeMemory(addr);
unsafe.freeMemory(addr3);
}
}
先看结果输出:
addr: 2433733895744
addr3: 2433733894944
16843009
72340172838076673
分析一下运行结果,首先使用allocateMemory
方法申请4
字节长度的内存空间,调用setMemory
方法向每个字节写入内容为byte
类型的1
,当使用Unsafe
调用getInt
方法时,因为一个int
型变量占4
个字节,会一次性读取4
个字节,组成一个int
的值,对应的十进制结果为16843009
。你可以通过下图理解这个过程:
在代码中调用reallocateMemory
方法重新分配了一块8
字节长度的内存空间,通过比较addr
和addr3
可以看到和之前申请的内存地址是不同的。在代码中的第二个for
循环里,调用copyMemory
方法进行了两次内存的拷贝,每次拷贝内存地址addr
开始的4
个字节,分别拷贝到以addr3
和addr3+4
开始的内存空间上:
拷贝完成后,使用getLong
方法一次性读取8
个字节,得到long
类型的值为72340172838076673
。
需要注意,通过这种方式分配的内存属于堆外内存,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用freeMemory
方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在try
中执行对内存的操作,最终在finally
块中进行内存的释放。
为什么要使用堆外内存?
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是
JVM
,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC
时减少回收停顿对于应用的影响。 - 提升程序
I/O
操作的性能。通常在I/O
通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
典型应用
DirectByteBuffer
是Java
用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty
、MINA
等NIO
框架中应用广泛。DirectByteBuffer
对于堆外内存的创建、使用、销毁等逻辑均由Unsafe
提供的堆外内存API
来实现。
下图为DirectByteBuffer
构造函数,创建DirectByteBuffer
的时候,通过Unsafe.allocateMemory
分配内存、Unsafe.setMemory
进行内存初始化,而后构建Cleaner
对象用于跟踪DirectByteBuffer
对象的垃圾回收,以实现当DirectByteBuffer
被垃圾回收时,分配的堆外内存一起被释放。
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配内存并返回基地址
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 内存初始化
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 跟踪DirectByteBuffer对象的垃圾回收,以实现堆外内存释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
5.2 内存屏障
在介绍内存屏障前,需要知道编译器和CPU
会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致CPU
的高速缓存和内存中数据的不一致,而内存屏障(Memory Barrier
)就是通过阻止屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。
在硬件层面上,内存屏障是CPU
为了防止代码进行重排序而提供的指令,不同的硬件平台上实现内存屏障的方法可能并不相同。在Java8
中,引入了3
个内存屏障的函数,它屏蔽了操作系统底层的差异,允许在代码中定义、并统一由JVM
来生成内存屏障指令,来实现内存屏障的功能。
Unsafe
中提供了下面三个内存屏障相关方法:
// 内存屏障,禁止load操作重排序。
// 屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
// 内存屏障,禁止store操作重排序。
// 屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
// 内存屏障,禁止load、store操作重排序
public native void fullFence();
内存屏障可以看做对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。以loadFence
方法为例,它会禁止读操作重排序,保证在这个屏障之前的所有读操作都已经完成,并且将缓存数据设为无效,重新从主存中进行加载。
看到这估计很多小伙伴们会想到volatile
关键字了,如果在字段上添加了volatile
关键字,就能够实现字段在多线程下的可见性。基于读内存屏障,我们也能实现相同的功能。下面定义一个线程方法,在线程中去修改flag
标志位,注意这里的flag
是没有被volatile
修饰的:
@Getter
class ChangeThread implements Runnable {
/**volatile**/
boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
}
}
在主线程的while
循环中,加入内存屏障,测试是否能够感知到flag
的修改变化:
public static void main(String[] args) {
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.isFlag();
unsafe.loadFence(); //加入读内存屏障
if (flag) {
System.out.println("detected flag changed");
break;
}
}
System.out.println("main thread end");
}
运行结果:
subThread change flag to:false
detected flag changed
main thread end
而如果删掉上面代码中的loadFence
方法,那么主线程将无法感知到flag
发生的变化,会一直在while
中循环。可以用图来表示上面的过程:
了解Java
内存模型(JMM
)的小伙伴们应该清楚,运行中的线程不是直接读取主内存中的变量的,只能操作自己工作内存中的变量,然后同步到主内存中,并且线程的工作内存是不能共享的。上面的图中的流程就是子线程借助于主内存,将修改后的结果同步给了主线程,进而修改主线程中的工作空间,跳出循环。
典型应用
在Java8
中引入了一种锁的新机制——StampedLock
,它可以看成是读写锁的一个改进版本。StampedLock
提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于StampedLock
提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load
到线程工作内存时,会存在数据不一致问题。
为了解决这个问题,StampedLock
的validate
方法会通过Unsafe
的loadFence
方法加入一个load
内存屏障。
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
5.3 对象操作
对象属性对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的putInt
、getInt
方法外,Unsafe
提供了全部8
种基础数据类型以及Object
的put
和get
方法,并且所有的put
方法都可以越过访问权限,直接修改内存中的数据。阅读openJDK
源码中的注释发现,基础数据类型和Object
的读写稍有不同,基础数据类型是直接操作的属性值(value
),而Object
的操作则是基于引用值(reference value
)。下面是Object
的读写方法:
// 在对象的指定偏移地址获取一个对象引用
public native Object getObject(Object o, long offset);
// 在对象指定偏移地址写入一个对象引用
public native void putObject(Object o, long offset, Object x);
除了对象属性的普通读写外,Unsafe
还提供了volatile
读写和有序写入方法。volatile
读写方法的覆盖范围与普通读写相同,包含了全部基础数据类型和Object
类型,以int
类型为例:
// 在对象的指定偏移地址处读取一个int值,支持volatile load语义
public native int getIntVolatile(Object o, long offset);
// 在对象指定偏移地址处写入一个int,支持volatile store语义
public native void putIntVolatile(Object o, long offset, int x);
相对于普通读写来说,volatile
读写具有更高的成本,因为它需要保证可见性和有序性。在执行get
操作时,会强制从主存中获取属性值,在使用put
方法设置属性值时,会强制将值更新到主存中,从而保证这些变更对其他线程是可见的。有序写入的方法有以下三个:
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
有序写入的成本相对volatile
较低,因为它只保证写入时的有序性,而不保证可见性,也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性,需要对内存屏障的知识点再进一步进行补充,首先需要了解两个指令的概念:
Load
:将主内存中的数据拷贝到处理器的缓存中Store
:将处理器缓存的数据刷新到主内存中
顺序写入与volatile
写入的差别在于,在顺序写时加入的内存屏障类型为StoreStore
类型,而在volatile
写入时加入的内存屏障是StoreLoad
类型,如下图所示:
在有序写入方法中,使用的是StoreStore
屏障,该屏障确保Store1
立刻刷新数据到内存,这一操作先于Store2
以及后续的存储指令操作。而在volatile
写入中,使用的是StoreLoad
屏障,该屏障确保Store1
立刻刷新数据到内存,这一操作先于Load2
及后续的装载指令,并且,StoreLoad
屏障会使该屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成之后,才执行该屏障之后的内存访问指令。
综上所述,在上面的三类写入方法中,在写入效率方面,按照put
、putOrder
、putVolatile
的顺序效率逐渐降低。
对象实例化
使用Unsafe
的allocateInstance
方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作:
@Data
public class A {
private int b;
public A() {
this.b = 1;
}
}
分别基于构造函数、反射以及Unsafe
方法的不同方式创建对象进行比较:
public void objTest() throws Exception {
A a1 = new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3 = (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}
打印结果分别为1、1、0,说明通过allocateInstance
方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了Class
对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将A
类的构造函数改为private
类型,将无法通过构造函数和反射创建对象,但allocateInstance
方法仍然有效。
典型应用
- 常规对象实例化方式:我们通常所用到的创建对象的方式,从本质上来讲,都是通过
new
机制来实现对象的创建。但是,new
机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。 - 非常规的实例化方式:而
Unsafe
中提供allocateInstance
方法,仅通过Class
对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM
安全检查等。它抑制修饰符检测,也就是即使构造器是private
修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance
在java.lang.invoke
、Objenesis
(提供绕过类构造器的对象生成方式)、Gson
(反序列化时用到)中都有相应的应用。
5.4 数组操作
arrayBaseOffset
与arrayIndexScale
这两个方法配合起来使用,即可定位数组中每个元素在内存中的位置。
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
典型应用
这两个与数据操作相关的方法,在java.util.concurrent.atomic
包下的AtomicIntegerArray
(可以实现对Integer
数组中每个元素的原子性操作)中有典型的应用,如下图AtomicIntegerArray
源码所示,通过Unsafe
的arrayBaseOffset
、arrayIndexScale
分别获取数组首元素的偏移地址base
及单个元素大小因子scale
。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的getAndAdd
方法即通过checkedByteOffset
方法获取某数组元素的偏移地址,而后通过CAS
实现原子性操作。
5.5 CAS操作
这部分主要为CAS
相关操作的方法。
/**
* CAS
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected,
Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
什么是CAS
?CAS
即比较并替换(Compare And Swap
),是实现并发算法时常用到的一种技术。CAS
操作包含三个操作数——内存位置、预期原值及新值。执行CAS
操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS
是一条CPU
的原子指令(cmpxchg
指令),不会造成所谓的数据不一致问题,Unsafe
提供的CAS
方法(如compareAndSwapXXX
)底层实现即为CPU
指令cmpxchg
。
典型应用
在JUC
包的并发工具类中大量地使用了CAS
操作,像在前面介绍synchronized
和AQS
的文章中也多次提到了CAS
,其作为乐观锁在并发工具类中广泛发挥了作用。在Unsafe
类中,提供了compareAndSwapObject
、compareAndSwapInt
、compareAndSwapLong
方法来实现的对Object
、int
、long
类型的CAS
操作。以compareAndSwapInt
方法为例:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
参数中o
为需要更新的对象,offset
是对象o
中整形字段的偏移量,如果这个字段的值与expected
相同,则将字段的值设为x
这个新值,并且此更新是不可被中断的,也就是一个原子操作。下面是一个使用compareAndSwapInt
的例子:
private volatile int a;
public static void main(String[] args) {
CasTest casTest = new CasTest();
new Thread(() -> {
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(() -> {
for (int i = 5 ; i < 10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a + " ");
}
}).start();
}
private void increment(int x) {
while (true) {
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
if (unsafe.compareAndSwapInt(this, fieldOffset, x - 1, x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
运行代码会依次输出:
1 2 3 4 5 6 7 8 9
在上面的例子中,使用两个线程去修改int
型属性a
的值,并且只有在a
的值等于传入的参数x
减一时,才会将a
的值变为x
,也就是实现对a
的加一的操作。流程如下所示:
需要注意的是,在调用compareAndSwapInt
方法后,会直接返回true
或false
的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在AtomicInteger
类的设计中,也是采用了将compareAndSwapInt
的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。
5.6 线程调度
Unsafe
类中提供了park
、unpark
、monitorEnter
、monitorExit
、tryMonitorEnter
方法进行线程调度。
// 取消阻塞线程
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);
方法park
、unpark
即可实现线程的挂起与恢复,将一个线程进行挂起是通过park
方法实现的,调用park
方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark
可以终止一个挂起的线程,使其恢复正常。此外,Unsafe
源码中monitor
相关的三个方法已经被标记为deprecated
,不建议被使用。
monitorEnter
方法用于获得对象锁;monitorExit
用于释放对象锁,如果对一个没有被monitorEnter
加锁的对象执行此方法,会抛出IllegalMonitorStateException
异常;tryMonitorEnter
方法尝试获取对象锁,如果成功则返回true
,反之返回false
。
典型应用
Java
锁和同步器框架的核心类AbstractQueuedSynchronizer(AQS)
,就是通过调用LockSupport.park()
和LockSupport.unpark()
实现线程的阻塞和唤醒的,而LockSupport
的park
、unpark
方法实际是调用Unsafe
的park
、unpark
方式实现的。
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
LockSupport
的park
方法调用了Unsafe
的park
方法来阻塞当前线程,此方法将线程阻塞后就不会继续往后执行,直到有其他线程调用unpark
方法唤醒当前线程。下面的例子对Unsafe
的这两个方法进行测试:
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("subThread try to unpark mainThread");
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("park main mainThread");
unsafe.park(false, 0L);
System.out.println("unpark mainThread success");
}
程序输出为:
park main mainThread
subThread try to unpark mainThread
unpark mainThread success
程序运行的流程也比较容易看懂,子线程开始运行后先进行睡眠,确保主线程能够调用park
方法阻塞自己,子线程在睡眠5
秒后,调用unpark
方法唤醒主线程,使主线程能继续向下执行。整个流程如下图所示:
5.7 Class操作
Unsafe
对Class
的相关操作主要包括类加载和静态变量的操作方法。
静态属性读取相关的方法
//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要实例化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);
创建一个包含静态属性的类,进行测试:
@Data
public class User {
public static String name = "Hydra";
int age;
}
private void staticTest() throws Exception {
User user = new User();
System.out.println(unsafe.shouldBeInitialized(User.class));
Field sexField = User.class.getDeclaredField("name");
long fieldOffset = unsafe.staticFieldOffset(sexField);
Object fieldBase = unsafe.staticFieldBase(sexField);
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println(object);
}
运行结果:
falseHydra
在Unsafe
的对象操作中,我们学习了通过objectFieldOffset
方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用staticFieldOffset
方法。在上面的代码中,只有在获取Field
对象的过程中依赖到了Class
,而获取静态变量的属性时不再依赖于Class
。
在上面的代码中首先创建一个User
对象,这是因为如果一个类没有被实例化,那么它的静态属性也不会被初始化,最后获取的字段属性将是null
。所以在获取静态属性前,需要调用shouldBeInitialized
方法,判断在获取前是否需要初始化这个类。如果删除创建User
对象的语句,运行结果会变为:
truenull
使用defineClass
方法允许程序在运行时动态地创建一个类
public native Class<?> defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,ProtectionDomain protectionDomain);
在实际使用过程中,可以只传入字节数组、起始字节的下标以及读取的字节长度,默认情况下,类加载器(ClassLoader
)和保护域(ProtectionDomain
)来源于调用此方法的实例。下面的例子中实现了反编译生成后的class
文件的功能:
private static void defineTest() {
String fileName = "F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
File file = new File(fileName);
try (FileInputStream fis = new FileInputStream(file)) {
byte[] content = new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod("getAge").invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
}
在上面的代码中,首先读取了一个class
文件并通过文件流将它转化为字节数组,之后使用defineClass
方法动态的创建了一个类,并在后续完成了它的实例化工作,流程如下图所示,并且通过这种方式创建的类,会跳过JVM
的所有安全检查。
除了defineClass
方法外,Unsafe
还提供了一个defineAnonymousClass
方法:
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
使用该方法可以用来动态的创建一个匿名类,在Lambda
表达式中就是使用ASM
动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在JDK 15
发布的新特性中,在隐藏类(Hidden classes
)一条中,指出将在未来的版本中弃用Unsafe
的defineAnonymousClass
方法。
典型应用
Lambda
表达式实现需要依赖Unsafe
的defineAnonymousClass
方法定义实现相应的函数式接口的匿名类。
5.8 系统信息
这部分包含两个获取系统相关信息的方法。
// 返回系统指针的大小。返回值为4(32位系统)或8(64位系统)。
public native int addressSize();
// 内存页的大小,此值为2的幂次方。
public native int pageSize();
典型应用
这两个方法的应用场景比较少,在java.nio.Bits
类中,在使用pageCount
计算所需的内存页的数量时,调用了pageSize
方法获取内存页的大小。另外,在使用copySwapMemory
方法拷贝内存时,调用了addressSize
方法,检测32
位系统的情况。
六、总结
总的来说就是这么几类
Info
相关。主要返回某些低级别的内存信息:addressSize()
,pageSize()
。Objects
相关。主要提供Object
和它的域操纵方法:allocateInstance()
,objectFieldOffset()
。Class
相关。主要提供Class
和它的静态域操纵方法:staticFieldOffset()
,defineClass()
,defineAnonymousClass()
,ensureClassInitialized()
。Arrays
相关。数组操纵方法:arrayBaseOffset()
,arrayIndexScale()
。Synchronization
相关。主要提供低级别同步原语(如基于CPU
的CAS
(Compare-And-Swap)原语):monitorEnter()
,tryMonitorEnter()
,monitorExit()
,compareAndSwapInt()
,putOrderedInt()
。Memory
相关。直接内存访问方法(绕过JVM
堆直接操纵本地内存):allocateMemory()
,copyMemory()
,freeMemory()
,getAddress()
,getInt()
,putInt()
。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器