1概要

この記事では、JDKで提供されている魅力的なクラス

sun.misc

パッケージの

Unsafe

を見ていきます。このクラスは、標準的なユーザではなくコアJavaライブラリによってのみ使用されるように設計された低レベルのメカニズムを提供します。

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


2

Unsafe


のインスタンスを取得する

まず、

Unsafe

クラスを使用できるようにするには、インスタンスを取得する必要があります。クラスが内部使用専用に設計されている場合、これは簡単ではありません。

  • インスタンスを取得する方法は、静的メソッド

    getUnsafe()を介して行われます。


    デフォルトでは、警告として

    SecurityExceptionがスローされます。

幸いなことに、

リフレクションを使ってインスタンスを取得することができます。

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

typeのデフォルト値 – 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()

を使用して正しく再利用する必要があることを覚えておく必要があります。

大規模なオフヒープメモリのバイト配列を作成したいとしましょう。これを実現するために

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の配列を初期化しています。

set()メソッドは、インデックスと、配列に格納される指定された

value

を取ります。

get()__メソッドは、配列の開始アドレスからのオフセットであるインデックスを使用してbyte値を取得しています。

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

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

メソッドで使用できるように、counterフィールドのアドレスを取得しています。

このフィールドは、この値を読み書きしているすべてのスレッドから見えるように、volatileとして宣言する必要があります。

offset

フィールドのメモリアドレスを取得するのに

objectFieldOffset()

メソッドを使用しています。

このクラスの最も重要な部分は

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

クラスとその最も有用な構成体を調べました。

プライベートフィールドへのアクセス方法、オフヒープメモリの割り当て方法、およびロックフリーアルゴリズムを実装するためのcompare-and-swapコンストラクトの使用方法について説明しました。

これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/core-java[GitHubへの移行]で見つけることができます – これはMavenプロジェクトなので、インポートは簡単なはずです。そのまま実行します。