Unsafe:如何不通过new来创建对象

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

前言

Java中,最常用的就是通过new调用相应构造器来创建对象实例,而当构造器不是public,而是private,new没了用武之地,我们又该怎样创建对象实例? 先创建构造器被private修饰的类,代码如下:

@Setter
@Getter
public class Girlfriend {
    private String name;
    private int age;
    private Girlfriend(String name) {
        this.name = name;
    }
}
复制代码

让我最后再new一遍...... 在这里插入图片描述

反射创建实例

类加载触发时机曾云:除了new,我还有反射。通过反射,可以获取类的字段、方法,同样可以获取类的构造器来创建对象。 对于private修饰的字段、方法、类构造器,我们必须禁用安全检查才能获取到。

未禁止安全检查

默认是开启安全检查机制,对于被private修饰的方法、类、构造器会禁止访问。 在这里插入图片描述 运行结果: 在这里插入图片描述

禁止安全检查

通过setAccessible(true)来关闭安全检查,访问private修饰的构造器。

在这里插入图片描述 运行结果: 在这里插入图片描述 就这,就这?其实这篇文章真的想讲的是Unsafe,一个可以直接操作内存,不用构造器就可以创建对象的类。

sun.mics.Unsafe

jvm的出现,让Java不再有C语言管理内存的困扰,同时也失去了类似指针操作内存的功能。于是Unsafe的出现填补了空缺。但正如其名,直接操作内存被认为是不安全的,会带来很多安全问题。所以,Unsafe没法通过new实例化,唯一一个构造器也是private。我们查看一下源码中和Unsafe实例有关的字段、方法。

源码如下:

public final class Unsafe {
    private static final Unsafe theUnsafe;
    private Unsafe() {
    }
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        // 检查调用类的加载器是不是Bootstrap,也就是null
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
 }
复制代码

不难看出getUnsafe()是一个public方法,但是它会检查调用getUsafe()类的加载器是不是Bootstrap类加载器,但是我们定义类的默认加载器是AppClassLoader,所以会直接抛出异常。

检查类加载器代码如下:

	// bootstrap加载器负责加载rt.jar,不是java编写,所以是null
	public static boolean isSystemDomainLoader(ClassLoader var0) {
        return var0 == null;
    }
复制代码

那么,我们只能通过反射从theUnsafe字段和构造器来创建实例。

代码如下:

public class Boy {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Class<Unsafe> unsafeClass = Unsafe.class;
        // 第一种方式:通过构造器获取Unsafe实例
        Constructor<Unsafe> declaredConstructor = unsafeClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Unsafe unsafe1 = declaredConstructor.newInstance();

        // 第二种方法:通过字段获取Unsafe实例
        Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe2 = (Unsafe)theUnsafe.get(null);
    }
}
复制代码

这样用两种方法获取到了unsafe实例对象,接下来看一下如何构建实例。

allocateInstance():构建实例

利用unsafe实例,使用这个方法可以直接不通过构造器来创建实例对象。

  // 利用Unsafe实例构造GirlFriends对象
  Girlfriend girlfriend1 = (Girlfriend) unsafe1.allocateInstance(Girlfriend.class);
  Girlfriend girlfriend2 = (Girlfriend) unsafe2.allocateInstance(Girlfriend.class);
  girlfriend1.setName("小芳1");
  girlfriend2.setName("小芳2");
  System.out.println(girlfriend1.getName());
  System.out.println(girlfriend2.getName());
复制代码

运行结果: 在这里插入图片描述 从运行结果可以看出来,两种方式获取的unsafe对象都能用来创建实例对象。那么Unsafe还有什么其他的功能。现在Unsafe一共提供了100多个方法,现在就找几个来测试一下。

putObject():修改对象成员变量

 Field nameField = Girlfriend.class.getDeclaredField("name");
 System.out.println(girlfriend1.getName());
 // 获取name成员变量在内存中的地址相对于对象内存地址的偏移量
 long l = unsafe1.objectFieldOffset(nameField);
 // 修改成员变量name的值
 unsafe1.putObject(girlfriend1, l, "小红");
 System.out.println(girlfriend1.getName());
复制代码

运行结果: 在这里插入图片描述

compareAndSwapInt():原子修改int属性值

有兴趣的可以了解一下锁里面的CAS原理

Field ageField = Girlfriend.class.getDeclaredField("age");
long l1 = unsafe1.objectFieldOffset(ageField);
girlfriend1.setAge(20);
System.out.println("初始年龄: " + girlfriend1.getAge());
// cas操作,第三个参数必须是旧值,即20,否则修改失败。
unsafe1.compareAndSwapInt(girlfriend1, l1, 20, 18);
System.out.println("修改后年龄: " +girlfriend1.getAge());
// 通过偏移量获取int值
int i = unsafe1.getInt(girlfriend1, l1);
System.out.println("偏移量获取int值:" + i);

复制代码

运行结果: 在这里插入图片描述

allocateMemory():分配内存

// 分配一个8byte的内存,并返回入口地址
long address = unsafe1.allocateMemory(8L);
// 用0000 0000代表一个字节,从入口地址初始化8个字节
unsafe1.setMemory(address, 8L, (byte) 0);
// aLong为0
long aLong1 = unsafe1.getLong(address);
// 修改以address为入口的long类型数据(8byte)
unsafe1.putLong(address,100L);
// aLong2为100
long aLong2 = unsafe1.getLong(address);
复制代码

Netty应用场景

因为netty框架在内存中定义了自己的内存分配管理体系,所以在PlatformDependent0用Unsafe来分配内存,获取变量。

结语

Unsafe还要其他的方法,有兴趣可以探究一下。其核心方法主要是围绕着内存地址入口、地址偏移量来展开,在开发中还请慎用。

猜你喜欢

转载自juejin.im/post/7032256564681506824