Java并发编程之Unsafe
简介
Unsafe
是一个比较底层的类,位于sun.misc
包下。取名是Unsafe
并不意味着线程不安全,而是因为它可以直接操作内存和线程,如果使用不当会产生意想不到的后果,所以必须慎用,名字可以理解为一种警示作用。
前面提到的一些线程安全类的方法进行CAS
操作,就是通过调用Unsafe
的方法实现的。
获取Unsafe
public final class Unsafe {
private static final Unsafe theUnsafe;
}
从上面可以看到Unsafe
类中提供了一个Unsafe
类型的属性theUnsafe
,虽然是静态的但是权限是private
,所以不能直接获取。
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
getUnsafe
方法是Unsafe
类中的一个静态方法,返回的就是theUnsafe
属性,看到这个是不是想着直接通过Unsafe.getUnsafe()
的方式就能获取Unsafe
对象了。想法没错但是不要忽略这里的if
条件VM.isSystemDomainLoader()
,这个方法判断的是当前的类加载器是否是启动类加载器BootstrapClassLoader
,而我们自己的类使用的加载器类型是AppClassLoader
,所以直接调用的话就会抛出异常。
既然不能直接调用,那我们就想其他办法,上面也说了我们要获取的其实就是Unsafe
类的一个属性theUnsafe
,获取属性值自然就想到反射了,那我们就直接通过反射获取这个属性就好了,代码如下:
public class Test2 {
public static void main(String[] args) throws Exception {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); // 获取Unsafe类的属性字段
theUnsafe.setAccessible(true); // 因为是私有属性,所以需要开启权限
Unsafe unsafe = (Unsafe) theUnsafe.get(null); // 获取属性的值,因为是static属性,所以传参是null
System.out.println(unsafe); // 打印结果:sun.misc.Unsafe@4f023edb
}
}
常用方法
获取属性偏移量
Unsafe
类中提供了两个获取属性偏移量的方法,分别是:
objectFieldOffset
:获取对象属性在内存中的偏移量staticFieldOffset
:获取静态属性在内存中的偏移量
两个方法的用法类似,这里以objectFieldOffset
举例:
class Student {
volatile int age;
volatile String name;
// 省略setter、getter
}
public class Test2 {
public static void main(String[] args) throws Exception {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
// 获取age、name在内存中的偏移量
long ageOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("age"));
long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
System.out.println(ageOffset); // 12
System.out.println(nameOffset); // 16
}
}
age
和name
的偏移量结果分别是12
和16
,之前在Java并发编程之synchronized(一)一文中提到过如何打印对象信息,这里顺便验证一下两个属性的偏移量是不是正确
System.out.println(ClassLayout.parseInstance(new Student()).toPrintable());
可以看到对象头的长度是
8+4
一共12
个字节,实例数据部分是4+4
共8
个字节,另外还有4
个字节填充,而age
和name
的内存偏移量正好就是12
和16
。
CAS操作
进行CAS
操作的方法主要有三个:
compareAndSwapInt
:对int
类型进行CAS
操作compareAndSwapLong
:对long
类型进行CAS
操作compareAndSwapObject
:对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);
四个参数的含义分别是:要操作的对象、要操作的对象的属性的内存偏移量、属性旧值、属性新值。可以理解为把某个对象中的某个内存数据从旧值修改成新的值。
// 省略Student类声明
public class Test {
public static void main(String[] args) throws Exception {
// 省略获取unsafe对象
long ageOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("age"));
long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
Student student = new Student();
unsafe.compareAndSwapInt(student, ageOffset, 0, 18);
unsafe.compareAndSwapObject(student, nameOffset, null, "张三");
System.out.println(student); // Student(age=18, name=张三)
}
}
上面代码分别获取了age
和name
属性在内存中的偏移量,然后通过unsafe
的方法分别把student
对象的两个属性值进行了修改,最终打印的就是修改后的结果,说明操作就成功了。
转载自:https://juejin.cn/post/7227416765881876517