likes
comments
collection
share

Java并发编程之Unsafe

作者站长头像
站长
· 阅读数 53

简介

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
    }
}

agename的偏移量结果分别是1216,之前在Java并发编程之synchronized(一)一文中提到过如何打印对象信息,这里顺便验证一下两个属性的偏移量是不是正确

System.out.println(ClassLayout.parseInstance(new Student()).toPrintable());

Java并发编程之Unsafe 可以看到对象头的长度是8+4一共12个字节,实例数据部分是4+48个字节,另外还有4个字节填充,而agename的内存偏移量正好就是1216

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=张三)
    }
}

上面代码分别获取了agename属性在内存中的偏移量,然后通过unsafe的方法分别把student对象的两个属性值进行了修改,最终打印的就是修改后的结果,说明操作就成功了。

转载自:https://juejin.cn/post/7227416765881876517
评论
请登录