一、概覽
在這篇文章中,我們將介紹一個(gè)由JRE提供的很有趣的類---sun.misc.包下的Unsafe。這個(gè)類為我們提供了底層機(jī)制,這些底層機(jī)制原本是設(shè)計(jì)用來供Java核心類庫使用的,而非普通Java用戶。
二、獲取Unsafe的實(shí)例
首先,要想使用Unsafe類,我們需要獲取一個(gè)實(shí)例-該實(shí)例并沒有直接給出,因?yàn)檫@個(gè)類是設(shè)計(jì)用來為內(nèi)部使用的。獲取該實(shí)例的方式就是通過getUnsafe()方法。默認(rèn)的警告-它會(huì)拋出一個(gè)SecurityException。
幸運(yùn)地是,我們可以使用反射來獲取該實(shí)例:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
三、使用Unsafe實(shí)例化一個(gè)類
現(xiàn)在我們有一個(gè)簡(jiǎn)單類,它的構(gòu)造函數(shù)會(huì)在對(duì)象創(chuàng)建的時(shí)候,設(shè)置一個(gè)變量值:
class InitializationOrdering {
private long a;
public InitializationOrdering() {
this.a = 1;
}
public long getA() {
return this.a;
}
}
當(dāng)我們使用它的構(gòu)造函數(shù)初始化它時(shí),其getA()方法的返回值為1:
InitializationOrdering o1 = new InitializationOrdering();
assertEquals(o1.getA(), 1);
但是,我們也可以使用Unsafe的allocateInstance()方法,它只會(huì)為此類分配內(nèi)存,而不會(huì)調(diào)用其構(gòu)造函數(shù):
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
InitializationOrdering o3
= (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class);
assertEquals(o3.getA(), 0);
可以看到構(gòu)造函數(shù)并沒有被調(diào)用,因?yàn)間etA()方法的返回值是long類型的默認(rèn)值-即值為0。
四、修改私有變量
假如說我們有一個(gè)類,該類持有了一個(gè)私有變量:
class SecretHolder {
private int SECRET_VALUE = 0;
public boolean secretIsDisclosed() {
return SECRET_VALUE == 1;
}
}
使用Unsafe的putInt()方法,我們可以改變私有變量SECRET_VALUE 的值,改變/破壞該實(shí)例的狀態(tài):
SecretHolder secretHolder = new SecretHolder();
Field f = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
unsafe.putInt(secretHolder, unsafe.objectFieldOffset(f), 1);
assertTrue(secretHolder.secretIsDisclosed());
一旦我們通過反射拿到了某個(gè)字段之后,我們就可以使用Unsafe修改它的值。
五、拋出異常
通過Unsafe調(diào)用的代碼不會(huì)像正常的Java代碼一樣被編譯器檢查。我們可以使用throwException()方法拋出任何的異常,而無需限制用戶處理該異常,即使它是檢查異常:
@Test(expected = IOException.class)
public void givenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt() {
unsafe.throwException(new IOException());
}
在拋出IOException之后,我們既不需要捕獲它也不需要在方法聲明上指定。
六、Off-heap 內(nèi)存
如果某個(gè)應(yīng)用正在耗盡JVM可用內(nèi)存的話,我們會(huì)強(qiáng)制GC進(jìn)程頻繁運(yùn)行。理想情況下,我們可以想要一個(gè)特殊的內(nèi)存區(qū)域,off-heap并且不被GC進(jìn)程控制。
Unsafe類的allocateMemory()方法使我們有能力把大量的對(duì)象分配在堆內(nèi)存之外,這意味著該內(nèi)存不會(huì)被GC看到,也不會(huì)被GC管理。
這可能很有用,但是我們需要記住,當(dāng)我們不用的時(shí)候,我們需要手動(dòng)地管理好這片內(nèi)存,使用freeMemory()對(duì)其進(jìn)行回收。
比如說,我們想創(chuàng)建大量堆外字節(jié)數(shù)組。我們可以使用使用allocateMemory()函數(shù)來實(shí)現(xiàn):
public class OffHeapArray {
private final static int BYTE = 1;
private long size ;
private long address;
public OffHeapArray(long size ) throws NoSuchFieldException,IllegalAccessException{
this.size = size;
address = getUnsafe().allocateMemory(size * BYTE);
}
private Unsafe getUnsafe() throws IllegalAccessException,NoSuchFieldException {
Field f= Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}
public void set(long i, byte value) throws NoSuchFieldException,IllegalAccessException{
getUnsafe().putByte(address + i * BYTE,value);
}
public int get(long idx) throws NoSuchFieldException,IllegalAccessException{
return getUnsafe().getByte(address + idx * BYTE);
}
public long size (){
return size;
}
public void freeMemory() throws NoSuchFieldException,IllegalAccessException{
getUnsafe().freeMemory(address);
}
}
在OffHeapArray的構(gòu)造函數(shù)中,我們以給定的大小初始化數(shù)組。我們把數(shù)組的起始地址保存在address字段中。set()方法接收了腳標(biāo),以及要在數(shù)組中存儲(chǔ)的值。get()方法使用腳標(biāo)來獲取值。
下一步,我們可以使用它的構(gòu)造函數(shù)分配一個(gè)off-heap數(shù)組:
long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
OffHeapArray array = new OffHeapArray(SUPER_SIZE);
我們可以把N個(gè)字節(jié)的值放入該數(shù)組中,并且取回這些值,把他們加起來,檢查一下我們的地址是不是可以正常的工作:
int sum = 0;
for (int i = 0; i < 100; i++) {
array.set((long) Integer.MAX_VALUE + i, (byte) 3);
sum += array.get((long) Integer.MAX_VALUE + i);
}
assertEquals(array.size(), SUPER_SIZE);
assertEquals(sum, 300);
最后,我們需要調(diào)用freeMemory()方法手動(dòng)地把內(nèi)存釋放給操作系統(tǒng)。
七、CompareAndSwap 操作
java.concurrent包下許多高效的構(gòu)造函數(shù),像 AtomicInteger,在本質(zhì)上使用的就是Unsafe的CompareAndSwap()方法,以提供最佳的性能。該構(gòu)造在lock-free算法中被廣泛使用,相較于java的悲觀鎖,它可以利用CAS處理器指令提供更快的速度。
我們構(gòu)造一個(gè)基于CAS的counter,使用Unsafe的compareAndSwapLong()方法:
class CASCounter {
private Unsafe unsafe;
private volatile long counter = 0;
private long offset;
private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}
public CASCounter() throws Exception {
unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
}
public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}
public long getCounter() {
return counter;
}
}
在CASCounter的構(gòu)造函數(shù)中,我們得到了counter字段的地址,以便于在后面的increment()方法中使用。我們需要把counter字段聲明為volatile以便對(duì)其他正在讀寫的線程可見。我們使用objectFieldOffset()方法得到了offset字段的內(nèi)存地址。
該類最重要的部分是increment()方法,我們?cè)趙hile循環(huán)中使compareAndSwapLong()把之前獲取的值自增,并檢查該值自我們上次獲取它之后,有沒有發(fā)生改變。
如果它發(fā)生了改變了的話,我們就不斷重試直到成功。這里沒有阻塞操作,這也就是它為什么被稱為lock-free算法的原因。
我們可以在多線程中測(cè)試我們的代碼:
int NUM_OF_THREADS = 1_000;
int NUM_OF_INCREMENTS = 10_000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
CASCounter casCounter = new CASCounter();
IntStream.rangeClosed(0, NUM_OF_THREADS - 1)
.forEach(i -> service.submit(() -> IntStream
.rangeClosed(0, NUM_OF_INCREMENTS - 1)
.forEach(j -> casCounter.increment())));
下一步,我們可以獲取該計(jì)數(shù)器的值,判斷它的狀態(tài)是不是正確:
assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());
八、Park/Unpark
在Unsafe的API中,還有兩個(gè)比較有趣的方法,JVM會(huì)使用它們完成線程的上下文切換。
當(dāng)線程在等待某些動(dòng)作的時(shí)候,JVM可以使用Unsafe類的park()方法把該線程阻塞住。
@Test
public void testPark() throws Exception {
final boolean[] run = new boolean[1];
Thread thread = new Thread() {
@Override
public void run() {
unsafe.park(true, 100000L);
run[0] = true;
}
};
thread.start();
unsafe.unpark(thread);
thread.join(100L);
assertTrue(run[0]);
}
park方法和Object.wait()方法很相似,但是它是在本地OS代碼層面調(diào)用的,因此可以利用
一些架構(gòu)細(xì)節(jié)獲取最佳性能。
當(dāng)線程被阻塞后,如果需要使它重新變成runnable的話,JVM會(huì)使用unpark()方法。我們經(jīng)常
在線程的堆棧中看到這些方法調(diào)用,尤其是使用了線程池的那些應(yīng)用。
九、總結(jié)
在本文中,我們研究了Unsafe類及其最有用的構(gòu)造。
我們了解了如何訪問私有字段,如何分配堆外內(nèi)存,以及如何使用compare-and-swap來實(shí)現(xiàn)無鎖算法。