sun.misc.Unsafe的使用

1. 概述

sun.misc包下提供的Unsafe类提供了供Java核心类库使用的一些底层操作。

2. 应用
1. 获取Unsafe

Unsafe提供了一个静态方法Unsafe.getUnsafe()来获取Unsafe实例,默认调用Unsafe.getUnsafe()会抛异常,我们一般通过反射获取:

public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    return (Unsafe) f.get(null);
}
2. 分配内存

假设我们有如下的Person类定义:

public static class Person {
    private String name = "shit";
    public Person(String s) {
        this.name = s;
    }
    public String getName() {
        return name;
    }
}

一般情况下,我们都是通过调用构造函数来创建这个类的实例的,比如:

Person p1 = new Person("lisi");
System.out.println("P1.name:" + p1.getName());    // 输出 P1.name:lisi

通过Unsafe我们能为Person类分配内存而不调用构造函数(甚至实例字段都不会被初始化,我们的name默认值是shit,下面这个示例打印的是null):

Unsafe unsafe = getUnsafe();
Person p2 = (Person) unsafe.allocateInstance(Person.class);
System.out.println("P2.name:" + p2.getName());    // P2.name:null
3. 修改私有字段

接上面的例子,Unsafe能用类似反射的操作,设置name字段的值:

Unsafe unsafe = getUnsafe();
Person p2 = (Person) unsafe.allocateInstance(Person.class);
System.out.println("P2.name:" + p2.getName());                     // 输出 P2.name:null

Field fname = p2.getClass().getDeclaredField("name");
unsafe.putObject(p2, unsafe.objectFieldOffset(fname), "o_lala");
System.out.println("P2.name:" + p2.getName());                     // 输出 P2.name:o_lala
4. 抛出检查型异常

正常情况下我们的程序如果抛出检查型异常,必须在方法内部try catch或者在方法签名中声明,通过Unsafe我们能够抛出检查型异常而不处理:

private static void throwCheckedException() {
    Unsafe unsafe = getUnsafe();
    unsafe.throwException(new IOException());
}

这里的IOException是检查型异常,可以看到我们即没有try catch也没有声明异常。

5. 堆外内存

正常情况下,我们创建的对象都是放在JVM堆中的,受GC的管制。 特殊场景下,我们可能希望创建的对象不参与GC,突破JVM堆大小限制,Unsafe.allocateMemory()正是为了这个目的而提供的,分配的内存在堆外,对GC来说根本不可见。这也意味着我们要自己管理内存,在不需要的时候释放内存。

比如我们要分配一个堆外大数组,它看起来可能是这样的:

private static class OffHeapArray {
    private final static int BYTE = 1;
    private long size;
    private long address;

    public OffHeapArray(long size) {
        Unsafe unsafe = getUnsafe();
        this.size = size;
        this.address = unsafe.allocateMemory(size * BYTE);
    }

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe) f.get(null);
        } catch (Exception e) {
            return null;
        }
    }

    public void set(long i, byte v) {
        getUnsafe().putByte(address + i, v);
    }

    public byte get(long i) {
        return getUnsafe().getByte(address + i);
    }

}

不过这种方式有一个问题,我能够访问分配给我的内存以外的内存,比如我只分配了一个16字节的数字,我们访问16字节以外的内容:

OffHeapArray arr = new OffHeapArray(16);
for (int i = 16; i < 100; i++) {
    System.out.println(arr.get(i));
}

而且能读到数据:
在这里插入图片描述

通过Unsafe.allocateMemory()分配的内存,最后需要我们调用unsafe.freeMemory(address)来释放才会返回给操作系统。

6. CAS

类似于AtomicInteger的大量操作(incrementcompareAndSet等等)底层是通过Unsafe.compareAndSwap*实现的。基于Unsafe.compareAndSwap*实现原子的increment代码应该是这样的:

private static class Counter {
    private Unsafe unsafe;
    private long offset;

    private volatile long index = 0;

    public Counter() throws NoSuchFieldException, IllegalAccessException {
        unsafe = getUnsafe();
        offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("index"));
    }

    public long increment() {
        long before = index;
        while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
            before = index;
        }
        return before + 1;
    }

    public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        return (Unsafe) f.get(null);
    }
}

使用方式:

Counter counter = new Counter();
System.out.println(counter.increment());
System.out.println(counter.increment());
System.out.println(counter.increment());

Counter类定义的两个关键点,一是index必须定义为volatile保证写入后对其他线程可见,compareAndSwapLong失败后重新读取index赋值给before

7. Park/Unpark

Unsafe提供Unsafe.park(类似于Object.wait())以及Unsafe.unpark()(类似于Object.notify())用于线程阻塞和唤醒,相比wait/notify,park/unpark调用OS原生代码,能过利用特定架构的特性,提供更好的性能。

3. 结论

从上面的例子来看,对我来说Unsafe提供的最有用的功能是堆外内存管理,绕开堆内存限制,避免不的GC扫描。 还有就是CAS,利用CAS实现更高效的免锁原子功能。

4. 番外篇
1. List并发问题

在CAS的章节,为了测试Counter的原子性,我创建了个线程池来并发执行increment,并收集结果。测试代码如下:

Counter counter = new Counter();
List<Long> all = new ArrayList<>();

ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
    es.submit(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 10; j++) {
                all.add(counter.increment());
            }
        }
    });
}
es.shutdown();
es.awaitTermination(1, TimeUnit.HOURS);
System.out.println(all);
System.out.println("count, 数字个数:" + all.size() + ", 去重后个数:" + new HashSet<>(all).size());

输出是这样的:
在这里插入图片描述

究其原因是ArrayList是非线程安全的,add方法内会将写入的位置size++,并发执行会导致有个写入直接被跳过了,导致大量的null存在,有的位置被重复写入。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值