1. 概要

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

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

2. 安全でないのインスタンスを取得する

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

インスタンスを取得する方法は、静的メソッドgetUnsafe()を使用することです。 注意点は、デフォルトでは–これは SecurityException。

幸い、リフレクションを使用してインスタンスを取得できます:

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

3. 安全でないを使用したクラスのインスタンス化

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

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()メソッドを使用すると、ヒープから巨大なオブジェクトを割り当てることができます。つまり、このメモリは、 GCとJVM

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

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

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、 与えられた配列を初期化していますサイズ。 配列の開始アドレスをに格納しています住所分野。 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の標準的な悲観的同期メカニズムと比較して大幅な高速化を実現できるロックフリーアルゴリズムで広く使用されています。

UnsafecompareAndSwapLong()メソッドを使用して、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()メソッドで使用できるようにします。 このフィールドは、この値を読み書きしているすべてのスレッドに表示されるように、揮発性として宣言する必要があります。 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. パーク/パーク解除

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

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

スレッドがブロックされ、再度実行可能にする必要がある場合、JVMはunpark()メソッドを使用します。特にスレッドプールを使用するアプリケーションでは、スレッドダンプでこれらのメソッド呼び出しがよく見られます。

9. 結論

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

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

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