【JDK源码】魔法类Unsafe

简介

Unsafe为我们提供了访问底层的机制,这种机制仅供java核心类库使用,而不应该被普通用户使用。但是,为了更好地了解java的生态体系,我们应该去学习它,去了解它,不求深入到底层的C/C++代码,但求能了解它的基本功能。

获取Unsafe的实例

可以看到Unsafe仅供java内部类使用

private Unsafe() {} // 私有构造

private static final Unsafe theUnsafe = new Unsafe(); // 私有的Unsafe对象属性

查看Unsafe的源码我们会发现它提供了一个getUnsafe()的静态方法

@CallerSensitive
public static Unsafe getUnsafe() {
    Class<?> caller = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(caller.getClassLoader()))
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

如果直接调用这个方法会抛出一个SecurityException异常,这是因为Unsafe仅供java内部类使用,外部类不应该使用它

在这里插入图片描述

我们可以用反射,上面我们知道它有一个属性叫theUnsafe,我们直接通过反射拿到它即可。反射不理解可以参考Java反射.pptx

public static void main(String[] args) throws Exception {
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe)f.get(null);
    
    System.out.println(unsafe.getClass());
    // 输出class sun.misc.Unsafe
}

使用Unsafe实例化一个类

class User {
    int age;
    public User(){
        this.age = 10;
    }
}
/**
 * 如果我们通过构造方法实例化这个类,age属性将会返回10。
 */
User user1 = new User();
// 输出10
System.out.println(user1.age);
================================================
/**
 * 如果我们调用Unsafe来实例化呢?
 */
User user2 = (User) unsafe.allocateInstance(User.class);
// 打印0
System.out.println(user2.age);

age将返回0,因为 Unsafe.allocateInstance()只会给对象分配内存,并不会调用构造方法,所以这里只会返回int类型的默认值0。

修改私有字段的值

使用Unsafe的putXXX()方法,我们可以修改任意私有字段的值。

/**
 * User类
 */
class User {
    private int age;
    public User(){
    }
    public int getAge(){
        return age;
    }
}
/**
 * 原来的反射修改私有属性的方法
 */
public static void main(String[] args) throws Exception {
    Field f1 = User.class.getDeclaredField("age");
    User user = User.class.getConstructor().newInstance();
    f1.setAccessible(true);
    f1.set(user,10);
}
// 输出10
System.out.println(f1.getInt(user));
/**
 * 使用Unsafe
 */
public static void main(String[] args) throws Exception {
    User user = User.class.getConstructor().newInstance();
    Field age = user.getClass().getDeclaredField("age");
    unsafe.putInt(user,unsafe.objectFieldOffset(age),20);
    // 输出20
    System.out.println(user.getAge());
}

一旦我们通过反射调用得到字段age,我们就可以使用Unsafe将其值更改为任何其他int值

抛出checked异常

我们知道如果代码抛出了checked异常,要不就使用try…catch捕获它,要不就在方法签名上定义这个异常,但是,通过Unsafe我们可以抛出一个checked异常,同时却不用捕获或在方法签名上定义它。

// 使用正常方式抛出IOException需要定义在方法签名上往外抛
public static void readFile() throws IOException {    
	throw new IOException();
}
// 使用Unsafe抛出异常不需要定义在方法签名上往外抛
public static void readFileUnsafe() {
    unsafe.throwException(new IOException());
}

使用堆外内存

如果进程在运行过程中JVM上的内存不足了,会导致频繁的进行GC。理想情况下,我们可以考虑使用堆外内存,这是一块不受JVM管理的内存。

使用Unsafe的allocateMemory()我们可以直接在堆外分配内存,这可能非常有用,但我们要记住,这个内存不受JVM管理,因此我们要调用freeMemory()方法手动释放它。

假设我们要在堆外创建一个巨大的int数组,我们可以使用allocateMemory()方法来实现

class OffHeapArray {    // 一个int等于4个字节    
    private static final int INT = 4;    
    private long size;    
    private long address;    
    private static Unsafe unsafe;    
    static {        
        try {            
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);        
        } catch (NoSuchFieldException e) {
                e.printStackTrace();        
        } catch (IllegalAccessException e) {
                e.printStackTrace();        
        }    
    }   
    // 构造方法,分配内存
    public OffHeapArray(long size){
        this.size = size;
        // 参数在堆外存的字节数
        address = unsafe.allocateMemory(size*INT);
    }
    // 设置指定索引处的元素
    public void set(long i , int value){
        unsafe.putInt(address+i*INT,value);
    }
    // 获取指定索引处的元素
    public int get(long i){
        return unsafe.getInt(address+i*INT);
    }
	// 释放堆外内存
    public void freeMemory(){
        unsafe.freeMemory(address);
    }
}

在构造方法中调用allocateMemory()分配内存,在使用完成后调用freeMemory()释放内存

public static void main(String[] args) throws Exception {
    OffHeapArray off = new OffHeapArray(4);
    off.set(1,2);
    off.set(2,3);
    off.set(3,4);
    off.set(2,5);
    off.set(0,6);
    int sum = 0;
    for (long i = 0; i < off.getSize(); i++) {
        sum += off.get(i);
    }
    // 输出17
    System.out.println(sum);
    off.freeMemory();
}

最后,一定要记得调用freeMemory()将内存释放回操作系统

CompareAndSwap操作

也就是CAS

JUC下面大量使用了CAS操作,它们的底层是调用的Unsafe的CompareAndSwapXXX()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用CAS处理器指令提供极大的加速。

比如,我们可以基于Unsafe的compareAndSwapInt()方法构建线程安全的计数器

class Counter{
    private volatile int count = 0;

    // 记录count字段的偏移量 用Unsafe获取
    private static long offset;
    // 获取Unsafe对象
    private static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
            // 设置count偏移量,为了能让Unsafe对其操作
            offset =unsafe.objectFieldOffset(Counter.class.getDeclaredField("count"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void increment(){
        int before = count;
        // 失败了就重试直到成功为止
        while (!unsafe.compareAndSwapInt(this,offset,before,before+1)) {
            before = count;
        }
    }
    public int getCount(){
        return count;
    }
}

在increment()方法中,我们通过调用Unsafe的compareAndSwapInt()方法来尝试更新之前获取到的count的值,如果它没有被其它线程更新过(这里就是偏移量的作用如果偏移量发生改变就说明此线程在改count的时候被其他线程修改过,所以会失败,就会重新获取count的值,重新判断),则更新成功,否则不断重试直到成功为止。

public static void main(String[] args) throws Exception{
    Counter counter = new Counter();
    for (int i = 0; i < 100; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i1 = 0; i1 < 1000; i1++) {
                    counter.increment();
                }
            }
        }).start();
    }

    // 为了防止main线程提前结束
    Thread.sleep(2000);
    // 输出100000
    System.out.println(counter.getCount());
}

park/unpark

JVM在上下文切换的时候使用了Unsafe中的两个方法park()和unpark()

  • 当一个线程正在等待某个操作时,JVM调用Unsafe的park()方法来阻塞此线程。

  • 当阻塞中的线程需要再次运行时,JVM调用Unsafe的unpark()方法来唤醒此线程。

总结

使用Unsafe几乎可以操作一切:

(1)实例化一个类;

(2)修改私有字段的值;

(3)抛出checked异常;

(4)使用堆外内存;

(5)CAS操作;

(6)阻塞/唤醒线程;

扩展

论实例化一个类的方式?

(1)通过构造方法实例化一个类;

(2)通过Class实例化一个类;

(3)通过反射实例化一个类;

(4)通过克隆实例化一个类;

(5)通过反序列化实例化一个类;

(6)通过Unsafe实例化一个类;

public class Demo8 {
    private static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws Exception{
        /**
         * 通过构造方法
         */
        Student s1 = new Student();
        /**
         * 通过Class实例化一个类
         */
        Student s2 = Student.class.newInstance();
        /**
         * 通过反射实例化一个类
         */
        Student s3 = Student.class.getConstructor().newInstance();
        /**
         * 通过克隆实例化一个类
         */
        Student s4 = (Student) s3.clone();
        /**
         * 通过反序列化实例化一个类
         */
        Student s5 = unserialize(s4);
        /**
         * 通过Unsafe实例化一个类
         */
        Student s6 = (Student) unsafe.allocateInstance(Student.class);

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
        System.out.println(s4);
        System.out.println(s5);
        System.out.println(s6);
		/*
        Student@6d21714c
		Student@108c4c35
		Student@4ccabbaa
		Student@5f4da5c3
		Student@4bf558aa
		Student@2d38eb89
		*/
    }
    public static Student unserialize(Student student) throws Exception{

        ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("D://a.txt"));
        output.writeObject(student);
        output.close();

        ObjectInputStream input = new ObjectInputStream(new FileInputStream("D://a.txt"));
        Student student1 = (Student)input.readObject();
        input.close();
        return student1;
    }
}
class Student implements Cloneable,Serializable {
    private int age;
    public Student(){
        this.age = 10;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>