死磕-java魔法类之Unsafe解析

31次阅读

共计 6334 个字符,预计需要花费 16 分钟才能阅读完成。

(手机横屏看源码更方便)


问题

(1)Unsafe 是什么?

(2)Unsafe 只有 CAS 的功能吗?

(3)Unsafe 为什么是不安全的?

(4)怎么使用 Unsafe?

简介

本章是 java 并发包专题的第一章,但是第一篇写的却不是 java 并发包中类,而是 java 中的魔法类 sun.misc.Unsafe。

Unsafe 为我们提供了访问底层的机制,这种机制仅供 java 核心类库使用,而不应该被普通用户使用。

但是,为了更好地了解 java 的生态体系,我们应该去学习它,去了解它,不求深入到底层的 C /C++ 代码,但求能了解它的基本功能。

获取 Unsafe 的实例

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

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

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

那么,我们就没有方法了吗?

当然不是,我们有反射啊!查看源码,我们发现它有一个属性叫 theUnsafe,我们直接通过反射拿到它即可。

public class UnsafeTest {public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
    }
}

使用 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()方法,我们可以修改任意私有字段的值。

public class UnsafeTest {public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);

        User user = new User();
        Field age = user.getClass().getDeclaredField("age");
        unsafe.putInt(user, unsafe.objectFieldOffset(age), 20);

        // 打印 20
        System.out.println(user.getAge());
    }
}

class User {
    private int age;

    public User() {this.age = 10;}

    public int getAge() {return age;}
}

一旦我们通过反射调用得到字段 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 int get(long i) {return unsafe.getInt(address + i * INT);
    }
    // 设置指定索引处的元素
    public void set(long i, int value) {unsafe.putInt(address + i * INT, value);
    }
    // 元素个数
    public long size() {return size;}
    // 释放堆外内存
    public void freeMemory() {unsafe.freeMemory(address);
    }
}

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

使用方式如下:

OffHeapArray offHeapArray = new OffHeapArray(4);
offHeapArray.set(0, 1);
offHeapArray.set(1, 2);
offHeapArray.set(2, 3);
offHeapArray.set(3, 4);
offHeapArray.set(2, 5); // 在索引 2 的位置重复放入元素

int sum = 0;
for (int i = 0; i < offHeapArray.size(); i++) {sum += offHeapArray.get(i);
}
// 打印 12
System.out.println(sum);

offHeapArray.freeMemory();

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

CompareAndSwap 操作

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

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

class Counter {
    private volatile int count = 0;

    private static long offset;
    private static Unsafe unsafe;
    static {
        try {Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
            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;}
}

我们定义了一个 volatile 的字段 count,以便对它的修改所有线程都可见,并在类加载的时候获取 count 在类中的偏移地址。

在 increment()方法中,我们通过调用 Unsafe 的 compareAndSwapInt()方法来尝试更新之前获取到的 count 的值,如果它没有被其它线程更新过,则更新成功,否则不断重试直到成功为止。

我们可以通过使用多个线程来测试我们的代码:

Counter counter = new Counter();
ExecutorService threadPool = Executors.newFixedThreadPool(100);

// 起 100 个线程,每个线程自增 10000 次
IntStream.range(0, 100)
    .forEach(i->threadPool.submit(()->IntStream.range(0, 10000)
        .forEach(j->counter.increment())));

threadPool.shutdown();

Thread.sleep(2000);

// 打印 1000000
System.out.println(counter.getCount());

park/unpark

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

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

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

我们之前在分析 java 中的集合时看到了大量的 LockSupport.park()/unpark(),它们底层都是调用的 Unsafe 的这两个方法。

总结

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

(1)实例化一个类;

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

(3)抛出 checked 异常;

(4)使用堆外内存;

(5)CAS 操作;

(6)阻塞 / 唤醒线程;

彩蛋

论实例化一个类的方式?

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

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

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

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

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

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

public class InstantialTest {

    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 {
        // 1. 构造方法
        User user1 = new User();
        // 2. Class,里面实际也是反射
        User user2 = User.class.newInstance();
        // 3. 反射
        User user3 = User.class.getConstructor().newInstance();
        // 4. 克隆
        User user4 = (User) user1.clone();
        // 5. 反序列化
        User user5 = unserialize(user1);
        // 6. Unsafe
        User user6 = (User) unsafe.allocateInstance(User.class);

        System.out.println(user1.age);
        System.out.println(user2.age);
        System.out.println(user3.age);
        System.out.println(user4.age);
        System.out.println(user5.age);
        System.out.println(user6.age);
    }

    private static User unserialize(User user1) throws Exception {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://object.txt"));
        oos.writeObject(user1);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://object.txt"));
        // 反序列化
        User user5 = (User) ois.readObject();
        ois.close();
        return user5;
    }

    static class User implements Cloneable, Serializable {
        private int age;

        public User() {this.age = 10;}

        @Override
        protected Object clone() throws CloneNotSupportedException {return super.clone();
        }
    }
}

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

正文完
 0