文章目录
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的大量操作(increment
、compareAndSet
等等)底层是通过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存在,有的位置被重复写入。