sun.misc.Unsafeへのガイド

sun.misc.Unsafeのガイド

1. 概要

この記事では、JDKが提供する魅力的なクラス(sun.miscパッケージのUnsafe)について説明します。 このクラスは、標準のユーザーではなく、コアJavaライブラリーのみが使用するように設計された低レベルのメカニズムを提供します。

これにより、主にコアライブラリ内での内部使用向けに設計された低レベルのメカニズムが提供されます。

2. Unsafeのインスタンスの取得

まず、Unsafeクラスを使用できるようにするには、インスタンスを取得する必要があります。これは、クラスが内部使用のみを目的として設計されているため、簡単ではありません。

The way to obtain the instance is via the static method getUnsafe().警告は、デフォルトで–これはSecurityException.をスローすることです。

幸い、we can obtain the instance using reflection:

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);

3. Unsafeを使用したクラスのインスタンス化

オブジェクトの作成時に変数値を設定するコンストラクターを持つ単純なクラスがあるとしましょう。

class InitializationOrdering {
    private long a;

    public InitializationOrdering() {
        this.a = 1;
    }

    public long getA() {
        return this.a;
    }
}

コンストラクターを使用してそのオブジェクトを初期化すると、getA()メソッドは1の値を返します。

InitializationOrdering o1 = new InitializationOrdering();
assertEquals(o1.getA(), 1);

ただし、Unsafe.を使用してallocateInstance()メソッドを使用できます。これはクラスにメモリを割り当てるだけで、コンストラクターを呼び出しません。

InitializationOrdering o3
  = (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class);

assertEquals(o3.getA(), 0);

コンストラクターが呼び出されなかったことに注意してください。そのため、getA()メソッドはlongタイプのデフォルト値(0)を返しました。

4. プライベートフィールドの変更

secretのプライベート値を保持するクラスがあるとしましょう。

class SecretHolder {
    private int SECRET_VALUE = 0;

    public boolean secretIsDisclosed() {
        return SECRET_VALUE == 1;
    }
}

Unsafe,からputInt()メソッドを使用して、プライベートSECRET_VALUEフィールドの値を変更し、そのインスタンスの状態を変更/破損することができます。

SecretHolder secretHolder = new SecretHolder();

Field f = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
unsafe.putInt(secretHolder, unsafe.objectFieldOffset(f), 1);

assertTrue(secretHolder.secretIsDisclosed());

リフレクション呼び出しによってフィールドを取得したら、Unsafeを使用してその値を他のint値に変更できます。

5. 例外を投げる

Unsafeを介して呼び出されるコードは、通常のJavaコードと同じようにコンパイラーによって検査されません。 throwException()メソッドを使用すると、チェックされた例外であっても、呼び出し元がその例外を処理するように制限することなく、例外をスローできます。

@Test(expected = IOException.class)
public void givenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt() {
    unsafe.throwException(new IOException());
}

チェックされたIOException,をスローした後、それをキャッチしたり、メソッド宣言で指定したりする必要はありません。

6. オフヒープメモリ

アプリケーションがJVMで利用可能なメモリを使い果たしている場合、GCプロセスを頻繁に実行しなければならなくなる可能性があります。 理想的には、GCプロセスによって制御されない、ヒープ外の特別なメモリ領域が必要です。

UnsafeクラスのallocateMemory()メソッドを使用すると、ヒープから巨大なオブジェクトを割り当てることができます。つまり、this memory will not be seen and taken into account by the GC and the JVMです。

これは非常に便利ですが、このメモリは手動で管理し、不要になったときにfreeMemory()で適切に再利用する必要があることを覚えておく必要があります。

バイトの大きなオフヒープメモリ配列を作成するとします。 これを実現するには、allocateMemory()メソッドを使用できます。

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,のコンストラクターで、指定されたsize.の配列を初期化します。配列の開始アドレスをaddressフィールドに格納します。 set()メソッドは、配列に格納されるインデックスと指定されたvalueを取得します。 get()メソッドは、配列の開始アドレスからのオフセットであるインデックスを使用してバイト値を取得しています。

次に、コンストラクタを使用してそのオフヒープ配列を割り当てることができます。

long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
OffHeapArray array = new OffHeapArray(SUPER_SIZE);

N個のバイト値をこの配列に入れてから、それらの値を取得し、それらを合計してアドレス指定が正しく機能するかどうかをテストできます。

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);

最後に、freeMemory().を呼び出して、メモリをOSに解放する必要があります。

7. CompareAndSwap操作

AtomicInteger,のようなjava.concurrentパッケージからの非常に効率的な構成は、可能な限り最高のパフォーマンスを提供するために、下のUnsafeからcompareAndSwap()メソッドを使用しています。 このコンストラクトは、CASプロセッサ命令を活用してJavaの標準的な悲観的同期メカニズムと比較して大幅な高速化を実現できるロックフリーアルゴリズムで広く使用されています。

UnsafeからcompareAndSwapLong()メソッドを使用してCASベースのカウンターを構築できます。

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コンストラクターでは、カウンターフィールドのアドレスを取得して、後でincrement()メソッドで使用できるようにします。 このフィールドは、この値を読み書きするすべてのスレッドから見えるように、volatileとして宣言する必要があります。 objectFieldOffset()メソッドを使用して、offsetフィールドのメモリアドレスを取得しています。

このクラスの最も重要な部分は、increment()メソッドです。 whileループのcompareAndSwapLong()を使用して、以前にフェッチした値をインクリメントし、フェッチしてから以前の値が変更されたかどうかを確認しています。

成功した場合は、成功するまでその操作を再試行しています。 ここにはブロッキングがありません。これがロックフリーアルゴリズムと呼ばれる理由です。

複数のスレッドから共有カウンターをインクリメントして、コードをテストできます。

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())));

次に、カウンターの状態が適切であることをアサートするために、カウンター値を取得できます。

assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());

8. Park/Unpark

Unsafe APIには、スレッドをコンテキストスイッチするためにJVMによって使用される2つの魅力的なメソッドがあります。 スレッドが何らかのアクションを待機している場合、JVMは、Unsafeクラスのpark()メソッドを使用して、このスレッドをブロックできます。

これはObject.wait()メソッドと非常に似ていますが、ネイティブOSコードを呼び出しているため、アーキテクチャの詳細を利用して最高のパフォーマンスを実現します。

When the thread is blocked and needs to be made runnable again, the JVM uses the unpark() method.特にスレッドプールを使用するアプリケーションでは、スレッドダンプでこれらのメソッド呼び出しがよく見られます。

9. 結論

この記事では、Unsafeクラスとその最も有用な構成について説明しました。

プライベートフィールドにアクセスする方法、オフヒープメモリを割り当てる方法、および比較とスワップ構造を使用してロックフリーアルゴリズムを実装する方法を見ました。

これらすべての例とコードスニペットの実装はover on GitHubにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。