1. 序章

Java 9は、開発者に多くの新しい便利な機能をもたらしました。

それらの1つは、 java .lang.invoke.VarHandle API(変数ハンドルを表す)です。これについては、この記事で説明します。

2. 可変ハンドルとは何ですか?

一般に、変数ハンドルは、変数への型付き参照にすぎません。 変数は、クラスの配列要素、インスタンス、または静的フィールドにすることができます。

VarHandle クラスは、特定の条件下で変数への書き込みおよび読み取りアクセスを提供します。

VarHandles は不変であり、表示状態はありません。 さらに、それらをサブクラス化することはできません。

VarHandleには次のものがあります:

  • 汎用タイプT。これは、このVarHandleで表されるすべての変数のタイプです。
  • 座標式のタイプである座標タイプCTのリスト。これにより、このVarHandleによって参照される変数を見つけることができます。

座標タイプのリストは空の場合があります。

VarHandle の目標は、 java.util.concurrent.atomicおよびsun.misc.Unsafeに相当するを呼び出すための標準を定義することです。フィールドと配列要素の操作。

これらの操作は、ほとんどがアトミック操作または順序付けされた操作です。たとえば、アトミックフィールドのインクリメントです。

3. 可変ハンドルの作成

VarHandle を使用するには、最初に変数が必要です。

例で使用するint型のさまざまな変数を使用して単純なクラスを宣言しましょう。

public class VariableHandlesUnitTest {
    public int publicTestVariable = 1;
    private int privateTestVariable = 1;
    public int variableToSet = 1;
    public int variableToCompareAndSet = 1;
    public int variableToGetAndAdd = 0;
    public byte variableToBitwiseOr = 0;
}

3.1. ガイドラインと規則

慣例として、VarHandlestaticfinal フィールドとして宣言し、静的ブロックで明示的に初期化する必要があります。 また、通常、対応するフィールド名の大文字バージョンを名前として使用します。 

たとえば、Java自体がVarHandleを内部的に使用してAtomicReferenceを実装する方法は次のとおりです。

private volatile V value;
private static final VarHandle VALUE;
static {
    try {
        MethodHandles.Lookup l = MethodHandles.lookup();
        VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
    } catch (ReflectiveOperationException e) {
        throw new ExceptionInInitializerError(e);
    }
}

ほとんどの場合、VarHandleを使用するときに同じパターンを使用できます。

これがわかったので、次に進んで、実際にそれらをどのように使用できるかを見てみましょう。

3.2. パブリック変数の変数ハンドル

これで、findVarHandle()メソッドを使用してpublicTestVariableのVarHandleを取得できます

VarHandle PUBLIC_TEST_VARIABLE = MethodHandles
  .lookup()
  .in(VariableHandlesUnitTest.class)
  .findVarHandle(VariableHandlesUnitTest.class, "publicTestVariable", int.class);

assertEquals(1, PUBLIC_TEST_VARIABLE.coordinateTypes().size());
assertEquals(VariableHandlesUnitTest.class, PUBLIC_TEST_VARIABLE.coordinateTypes().get(0));

このVarHandlecoordinateTypesプロパティは空ではなく、VariableHandlesUnitTestクラスという1つの要素を持っていることがわかります。

3.3. プライベート変数の変数ハンドル

プライベートメンバーがあり、そのような変数の変数ハンドルが必要な場合、 privateLookupIn()メソッドを使用してこれを取得できます。

VarHandle PRIVATE_TEST_VARIABLE = MethodHandles
  .privateLookupIn(VariableHandlesUnitTest.class, MethodHandles.lookup())
  .findVarHandle(VariableHandlesUnitTest.class, "privateTestVariable", int.class);

assertEquals(1, PRIVATE_TEST_VARIABLE.coordinateTypes().size());
assertEquals(VariableHandlesUnitTest.class, PRIVATE_TEST_VARIABLE.coordinateTypes().get(0));

ここでは、通常の lookup()よりもアクセスが広い privateLookupIn()メソッドを選択しました。 これにより、 private public 、またはprotected変数にアクセスできます。

Java 9以前は、この操作に相当するAPIは、UnsafeクラスとReflectionAPIのsetAccessible()メソッドでした。

ただし、このアプローチには欠点があります。 たとえば、変数の特定のインスタンスに対してのみ機能します。

VarHandle は、このような場合のより優れた、より高速なソリューションです。

3.4. 配列の可変ハンドル

前の構文を使用して、配列フィールドを取得できます。

ただし、特定のタイプの配列のVarHandleを取得することもできます。

VarHandle arrayVarHandle = MethodHandles.arrayElementVarHandle(int[].class);

assertEquals(2, arrayVarHandle.coordinateTypes().size());
assertEquals(int[].class, arrayVarHandle.coordinateTypes().get(0));

このようなVarHandleには2つの座標タイプint[]があり、intプリミティブの配列を表していることがわかります。

4. VarHandleメソッドの呼び出し

ほとんどのVarHandleメソッドは、可変数の型の引数を想定しています。 物体。 使用する物体… 引数として静的引数チェックを無効にします。

すべての引数チェックは実行時に行われます。 また、メソッドが異なれば、タイプの異なる引数の数も異なると予想されます。

適切な型で適切な数の引数を指定できない場合、メソッド呼び出しはWrongMethodTypeExceptionをスローします。

たとえば、 get()は、変数を見つけるのに役立つ少なくとも1つの引数を期待しますが、 set()は、変数に割り当てられる値であるもう1つの引数を期待します。 。

5. 可変ハンドルアクセスモード

一般に、 VarHandle クラスのすべてのメソッドは、5つの異なるアクセスモードに分類されます。

次のサブセクションでそれらのそれぞれを見ていきましょう。

5.1. アクセスを読む

アクセスレベルを読み取るメソッドを使用すると、指定されたメモリオーダリング効果の下で変数の値を取得できます。 このアクセスモードには、 get() getAcquire() getVolatile() getOpaque()などのメソッドがいくつかあります。

VarHandleget()メソッドを簡単に使用できます。

assertEquals(1, (int) PUBLIC_TEST_VARIABLE.get(this));

get()メソッドは、パラメーターとして CoordinateTypes のみを使用するため、この場合はthisを使用できます。

5.2. 書き込みアクセス

アクセスレベルを書き込むメソッドを使用すると、特定のメモリオーダリング効果の下で変数の値を設定できます。

読み取りアクセスのあるメソッドと同様に、書き込みアクセスのあるメソッドがいくつかあります。 set() setOpaque() setVolatile()、および setRelease ()

VarHandleset()メソッドを使用できます。

VARIABLE_TO_SET.set(this, 15);
assertEquals(15, (int) VARIABLE_TO_SET.get(this));

set()メソッドは、少なくとも2つの引数を必要とします。 1つ目は変数の検索に役立ち、2つ目は変数に設定する値です。

5.3. アトミックアップデートアクセス

このアクセスレベルのメソッドを使用して、変数の値をアトミックに更新できます。

compareAndSet()メソッドを使用して、効果を確認してみましょう。

VARIABLE_TO_COMPARE_AND_SET.compareAndSet(this, 1, 100);
assertEquals(100, (int) VARIABLE_TO_COMPARE_AND_SET.get(this));

CoordinateTypes とは別に、 compareAndSet()メソッドは、oldValuenewValueの2つの追加値を取ります。 このメソッドは、変数が oldVariable と等しい場合は変数の値を設定し、それ以外の場合は変更しないままにします。

5.4. 数値アトミックアップデートアクセス

これらのメソッドを使用すると、特定のメモリオーダリング効果の下で getAndAdd ()などの数値操作を実行できます。

VarHandleを使用してアトミック操作を実行する方法を見てみましょう。

int before = (int) VARIABLE_TO_GET_AND_ADD.getAndAdd(this, 200);

assertEquals(0, before);
assertEquals(200, (int) VARIABLE_TO_GET_AND_ADD.get(this));

ここで、 getAndAdd()メソッドは、最初に変数の値を返し、次に指定された値を追加します。

5.5. ビット単位のアトミック更新アクセス

このアクセス権を持つメソッドを使用すると、特定のメモリオーダリング効果の下でビット単位の演算をアトミックに実行できます。

getAndBitwiseOr()メソッドの使用例を見てみましょう。

byte before = (byte) VARIABLE_TO_BITWISE_OR.getAndBitwiseOr(this, (byte) 127);

assertEquals(0, before);
assertEquals(127, (byte) VARIABLE_TO_BITWISE_OR.get(this));

このメソッドは、変数の値を取得し、ビット単位のOR演算を実行します。

メソッド呼び出しは、メソッドに必要なアクセスモードを変数に許可されているアクセスモードと一致させない場合、IllegalAccessExceptionをスローします。

たとえば、これは、 final変数でset()メソッドを使用しようとした場合に発生します。

6. メモリオーダリング効果

VarHandleメソッドを使用すると、特定のメモリオーダリング効果の下で変数にアクセスできることを前述しました。

ほとんどの方法には、4つのメモリオーダリング効果があります。

  • Plain の読み取りと書き込みにより、32ビット未満の参照とプリミティブのビット単位のアトミック性が保証されます。 また、他の特性に関して順序の制約を課しません。
  • Opaque 演算はビット単位のアトミックであり、同じ変数へのアクセスに関してコヒーレントに順序付けられます。
  • AcquisitionおよびRelease操作は、Opaqueプロパティに従います。 また、 Acquisition 読み取りは、Releaseモードの書き込みと一致した後にのみ順序付けられます。
  • Volatile 操作は、相互に完全に順序付けられています。

アクセスモードは以前のメモリオーダリング効果を上書きすることを覚えておくことが非常に重要です。 これは、たとえば、 get()を使用すると、変数を volatile として宣言した場合でも、単純な読み取り操作になることを意味します。

そのため、開発者はVarHandle操作を使用する際に細心の注意を払う必要があります。

7. 結論

このチュートリアルでは、変数ハンドルとその使用方法を紹介しました。

変数ハンドルは低レベルの操作を可能にすることを目的としているため、このトピックは非常に複雑であり、必要な場合を除いて使用しないでください。

いつものように、コードサンプルはGitHubから入手できます。