Unsafe及CAS介紹

一、概覽

在這篇文章中,我們將介紹一個(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)無鎖算法。

十、參考文獻(xiàn)

參考文獻(xiàn)1
參考文獻(xiàn)2
參考文獻(xiàn)3

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容