并发笔记-JUC之Unsafe类

image.png

相关背景

一下代码基于JDK1.8进行分析,在高版本中部分方法已经被删除获取迁移到其他地方。

Java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作。Unsafe类使用private修饰构造方法,只能使用他自己提供的一个final类来进行获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static native void registerNatives();
static {
registerNatives();
sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
}

private Unsafe() {}

private static final Unsafe theUnsafe = new Unsafe();
//获取unsafe实例的单例方法
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
//只有由主类加载器(BootStrap classLoader)加载的类才能调用这个类中的方法
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}

Unsafe类提供102个native方法,主要包含类、对象、变量的管理(获取和修改),内存管理,线程管理,内存屏障等。

1
2
3
4
5
6
7
public static Unsafe getUnsafe() throws Exception{
//1.最简单的使用方式是基于反射获取Unsafe实例
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
}

类、对象、变量管理

getObject

1
public native Object getObject(Object o, long offset);

该方法用于获取Java对象o中内存地址偏移量为offerset的对象。类似的方法还有getInt()、getDouble等。

Object o:引用对象
long offset:内存偏移量(偏移量是可以通过objectFieldOffset方法获取获取的)

1
2
3
public native long objectFieldOffset(Field f);
public native long staticFieldOffset(Field f);
public native int arrayBaseOffset(Class<?> arrayClass);

操作案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
public class UnsafeTest {

//类占8位
private int id;//占4位
private String sex;//对象占4位
private String name;//offset=20

public static Unsafe getUnsafe() throws Exception{
//1.最简单的使用方式是基于反射获取Unsafe实例
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
}

public static void main(String[] args) throws Exception {
Unsafe unsafe = getUnsafe();
UnsafeTest hello = new UnsafeTest();
hello.name = "mikey";
Field field = UnsafeTest.class.getDeclaredField("name");
long offset = unsafe.objectFieldOffset(field);
System.out.println("offset="+offset);
Object name = unsafe.getObject(hello, offset);
System.out.println(name);
}
}

putObject

该方法用于修改Java对象o中内存地址偏移量为offerset的对象。类似的方法还有putInt()、putDouble等。

1
public native void putObject(Object o, long offset, Object x);

Object o:待更新的对象
long offset:对象熟悉的偏移地址
Object x:更新后的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Data
public class UnsafeTest {

//类占8位
private int id;//占4位
private String sex;//对象占4位
private String name;//offset=20

public static Unsafe getUnsafe() throws Exception{
//1.最简单的使用方式是基于反射获取Unsafe实例
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
}

public static void main(String[] args) throws Exception {
Unsafe unsafe = getUnsafe();
UnsafeTest hello = new UnsafeTest();
hello.name = "mikey";
Field field = UnsafeTest.class.getDeclaredField("name");
long offset = unsafe.objectFieldOffset(field);
System.out.println("offset="+offset);
Object name = unsafe.getObject(hello, offset);
System.out.println(name);
unsafe.putObject(hello,offset,"leo");
System.out.println(hello.name);
}
}

getObjectVolatile

getObjectVolatile方法用于获取对象o带有volatile关键字修饰的属性,和getObject方法类似。

1
Object getObjectVolatile(Object o, long offset);

putObjectVolatile

putObjectVolatile方法用于设置对象o带有volatile关键字修饰的属性,和putObject方法类似。

1
2
3
4
5
/**
* Stores a reference value into a given Java variable, with
* volatile store semantics. Otherwise identical to putObject(Object, long, Object)
*/
public native void putObjectVolatile(Object o, long offset, Object x);

putOrderedObject


putObjectVolatile(Object,long,Object)的版本,不保证存储对其他线程的即时可见性。通常只有当底层字段是 volatile(或者如果是数组单元,则只能使用volatile访问)时,此方法才有用。

1
public native void    putOrderedObject(Object o, long offset, Object x);

defineClass

告诉VM定义一个类,而不进行安全检查。默认情况下类加载器和保护域来自调用方的类。

1
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);

defineAnonymousClass

定义一个类,但不告知类加载器和系统字典。类似于我们代码中的lambda表达式。

1
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

staticFieldBase

结合staticFieldOffset返回静态字段的位置。
获取基“Object”(如果有),通过它可以通过getInt(Object,long)等方法访问给定类的静态字段。此值可能为空。此值可能引用一个“cookie”对象,不能保证它是真实对象,并且除了用作此类中get和put例程的参数外,不应以任何方式使用它。

1
public native Object staticFieldBase(Field f);

shouldBeInitialized

检测给定类是否需要初始化。这通常是需要与获取类别。仅当对ensureClassifilized的调用无效时返回false。

1
public native boolean shouldBeInitialized(Class<?> c);

ensureClassInitialized

该方法用于确保给定类已初始化。这通常需要与获取类的静态字段基一起使用。

1
public native void ensureClassInitialized(Class<?> c);

内存管理

获取偏移地址

1
2
3
4
public native long objectFieldOffset(Field f);//获取对象的字段(属性)偏移地址
public native long staticFieldOffset(Field f);//获取对象的静态字段(属性)偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);//获取数组的字段(属性)偏移地址
public native int arrayIndexScale(Class<?> arrayClass);//可以获取数组中元素间的偏移地址增量

addressSize

获取本机指针字节大小。该值将为4或8。

1
public native int addressSize();

pageSize

获取本地内存的页数,该值为2的幂次方

1
public native int pageSize();

getAddress/putAddress

从给定内存地址获取本机指针。如果地址为零,或未指向从allocateMemory获得的块,则结果未定义。
如果本机指针的宽度小于64位,则会将其作为无符号数扩展到Java long。指针可以通过任何给定的字节偏移量进行索引,只需将该偏移量(作为简单整数)添加到表示指针的long中即可。从目标地址实际读取的字节数可能由地址大小决定。

1
2
public native long getAddress(long address);
public native void putAddress(long address, long x);

通过Unsafe类可以分配内存,可以释放内存;

类中提供的3个本地方法allocateMemoryreallocateMemoryfreeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。

allocateMemory

分配一个新的给定大小本地内存。内存内容未初始化;它们通常是垃圾。生成的本机指针永远不会为零,并且将针对所有值类型进行对齐。通过调用 freeMemory 来释放此内存,或使用 reallocatemory 调整其大小。

1
public native long allocateMemory(long bytes);

reallocateMemory

将新的本机内存块大小调整为给定的字节大小。这个超过旧块大小的新块的内容为未初始化;它们通常是垃圾。生成的本机当且仅当请求的大小为零时,指针将为零。这个生成的本机指针将针对所有值类型进行对齐。处置通过调用 freeMemory 或使用 reallocateMemory 调整其大小。其中传递给此方法的address可能为null在这种情况下,将执行分配。

1
public native long reallocateMemory(long address, long bytes);

setMemory

将给定内存块中的所有字节设置为固定值(通常为零)。这提供了单寄存器寻址模式。

1
public void setMemory(long address, long bytes, byte value)

copyMemory

将给定内存块中的所有字节设置为另一个块的副本。此方法通过两个参数确定每个块的基址,因此它(实际上)提供双寄存器寻址模式,如{getInt(Object,long)}中所述。当对象引用为null时,偏移量提供一个绝对基址。传输以确定大小的相干(原子)单位进行通过地址和长度参数。如果有效地址和长度均为偶数模8,传输以“长”单位进行。如果有效地址和长度分别为偶模4或2,传输以“int”或“short”为单位进行。

1
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);

freeMemory

释放address地址的内存空间,如果address为空则不做任何处理

1
public native void freeMemory(long address);

线程管理

线程管理主要提供了对象锁,cas轻量级锁等功能

monitorEnter

锁住当前对象o,它必须通过{monitorExit方法}解锁。可重入锁。

1
public native void monitorEnter(Object o);

monitorExit

解锁当前对象o

1
public native void monitorExit(Object o);

tryMonitorEnter

尝试锁定对象。返回true或false以指示锁定是否成功。如果是,则对象必须是通过{monitorExit}解锁。

1
public native boolean tryMonitorEnter(Object o);

park/unpark

锁住/解锁当前线程。LockSupport类就是基于这两个方法实现的。

park:除非许可可用,否则出于线程调度目的禁用当前线程。
如果许可证用,则会使用该许可并立即返回调用;否则,出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下三种情况之一:

  • 其他一些线程以当前线程为目标调用unpark;
  • 其他线程中断当前线程;
  • 该调用错误地(即无原因地)返回。

此方法不报告导致该方法返回的原因。调用者应该首先重新检查导致线程停止的条件。例如,调用者还可以在返回时确定线程的中断状态。

1
2
3
4
//第一个参数是是否是绝对时间,第二个参数是等待时间值。如果isAbsolute是true则会实现ms定时。如果isAbsolute是false则会实现ns定时。
public native void park(boolean isAbsolute, long time);
//如果线程在park上被阻塞,那么它将解除阻塞。否则,它对park的下一个调用将保证不会阻塞。如果给定的线程尚未启动,则不能保证此操作有任何效果。
public native void unpark(Object thread);

compareAndSwapObject

CAS(compareAndSwapObject比较并交换)

1
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

针对Object对象进行CAS操作。即是对应Java变量引用o,原子性地更新o中偏移地址为offset的属性的值为x,当且仅的偏移地址为offset的属性的当前值为expected才会更新成功返回true,否则返回false。

  • o:目标Java变量引用。
  • offset:目标Java变量中的目标属性的偏移地址。
  • expected:目标Java变量中的目标属性的期望的当前值。
  • x:目标Java变量中的目标属性的目标更新值。

类似的方法有compareAndSwapIntcompareAndSwapLong,在Jdk8中基于CAS扩展出来的方法有getAndAddIntgetAndAddLonggetAndSetIntgetAndSetLonggetAndSetObject,它们的作用都是:通过CAS设置新的值,返回旧的值。

CAS底层实现

1
2
3
4
5
6
7
8
9
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
//获取对象的变量的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
//调用Atomic操作
//进入atomic.hpp,大意就是先去获取一次结果,如果结果和现在不同,就直接返回,因为有其他人修改了;否则会一直尝试去修改。直到成功。
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

内存屏障

在Unsafe类中提供了三个内存屏障的方法,storeFence、fullFence、loadFence

storeFence

表示该方法之前的所有store(写)操作在内存屏障之前完成。

1
public native void storeFence();

loadFence

表示该方法之前的所有load(读)操作在内存屏障之前完成。

1
public native void loadFence();

fullFence

表示该方法之前的所有load、store操作在内存屏障之前完成

1
public native void fullFence();

资料

http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
jdk1.8 sun.misc.Unsafe


并发笔记-JUC之Unsafe类
https://mikeygithub.github.io/2022/05/25/yuque/并发笔记-JUC之Unsafe类/
作者
Mikey
发布于
2022年5月25日
许可协议