Java 9 Variable Handles Demystified

原文はこちら。
The original article was written by Eugen Paraschiv.
https://www.baeldung.com/java-variable-handles

1. Introduction

Java 9で開発者にとって有用な、数多くの新機能が登場しました。

その一つに、java.lang.invoke.VarHandle APIがあります。VarHandleとは変数ハンドル(Variable Handle)で、この記事で取り上げようとしているものです。

VarHandle (Java SE 9 & JDK 9 )
https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/VarHandle.html
https://docs.oracle.com/javase/jp/9/docs/api/java/lang/invoke/VarHandle.html

2. What Are Variable Handles?

一般に、変数ハンドルは型付けされた変数への参照に過ぎません。変数は配列要素、インスタンス、クラスの静的フィールドの可能性があります。

VarHandleクラスを使うと、特定状況の下で変数への書き込み、読み出しアクセスが可能になります。

VarHandleはイミュータブルで可視的な状態を持ちません。さらに、VarHandlesをユーザーがサブクラス化することはできません。

各VarHandleは以下で構成されています。 

  • ジェネリックタイプ T:このVarHandleが参照する各変数の型
  • 座標型のリスト CT1, CT2, …, CTn: このVarHandleによって参照される変数の場所を指し示す、座標表現の型。

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

VarHandleの目的は、java.util.concurrent.atomicとsun.misc.Unsafe のフィールドや配列要素に対する演算と等価なものを呼び出すための標準を定義することです。

これらの演算はほとんどは原始的(atomic)もしくは順序付きの演算(ordered operations)です(例:アトミックフィールドのインクリメント)

3. Creating Variable Handles

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. Guidelines and Conventions

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

例えば、以下はJava自身がVarHandlesを内部で使い、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);
    }
}

https://github.com/openjdk/jdk14u/blob/d6d8d4d931b06d919e7688c6106f489a173d8608/src/java.base/share/classes/java/util/concurrent/atomic/AtomicReference.java#L51

ほとんどの場合、VarHandles の利用パターンはほぼ同じです。これがわかったところで、実際にどのように使うことができるかを見てみましょう。

3.2. Variable Handles for Public Variables

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

このVarHandleのcoordinateTypesプロパティが空ではなく、1個の要素を持ち、それがVariableHandlesUnitTestクラスであることがわかります。

3.3. Variable Handles for Private Variables

privateメンバーの変数ハンドルが必要な場合、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クラスと、Reflection APIのsetAccessible()メソッドでしたが、このアプローチには欠点があります。例えば、変数の特定のインスタンスにのみ機能する、という点です。

VarHandleはこのようなケースにおいてよりよい、高速なソリューションです。

3.4. Variable Handles for Arrays

配列フィールドを取得するために先ほどの構文を利用できましたが、特定の型の配列のVarHandleを取得することもできます。

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

このようなVarHandleには2個の座標型である intと[]があって。プリミティブ型intの配列を表現していることを確認できます。

4. Invoking VarHandle Methods

VarHandleメソッドのほとんどはObject型の可変長引数を期待しています。引数としてUsing Object… を使用すると、静的な引数チェックはなされません。

全ての引数チェックは実行時に行われます。また、異なるメソッドは異なる型の異なる個数の引数を持つと想定します。

適切な型を持つ適切な個数の引数を与えることができなければ、メソッドの呼び出しでWrongMethodTypeExceptionをスローします。

例えば、get()が少なくとも(変数の場所を決めるのに役立つ)1個の引数を期待しているのに対し、set()はもう一つの引数(変数に割り当てる値)を期待しています。

5. Variable Handles Access Modes

一般に、VarHandleクラスのメソッドの全ては5個の異なるアクセスモードに該当します。以下でその各々を見ていきましょう。

5.1. Read Access

読み込みアクセスレベルを持つメソッドは、指定されたメモリ順序効果の下で変数の値を取得することができます。このアクセスモードを持つメソッドは、get()、getAcquire()、getVolatile()、getOpaque() などがあります。

VarHandleに対してget()メソッドを利用するのは簡単です。

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

このget()メソッドはパラメータとしてCoordinateTypesのみを取ります。そのため、今回はこのメソッドを利用できます。

5.2. Write Access

書き込みアクセスレベルを持つメソッドは、指定されたメモリ順序効果の下で変数の値を設定できます。読み取りアクセスを持つメソッドと同様、この書き込みアクセスを持つメソッドには、set()、setOpaque()、setVolatile()、setRelease() などがあります。

今回のVarHandleでset()メソッドを使うことができます。

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

このset()メソッドは少なくとも2個の引数を期待しています。1個は変数の位置を決める引数、もう一つは変数に設定する値です。

5.3. Atomic Update Access

このアクセスレベルを持つメソッドを使ってアトミックに変数の値を更新できます。compareAndSet()メソッドを使って効果を確認しましょう。

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

CoordinateTypesとは別に、compareAndSet()メソッドはoldValueとnewValueの2つの値を取ります。このメソッドは、oldVariableと等しい場合は変数の値を設定し、そうでない場合は変数の値を変更せずに残します。

5.4. Numeric Atomic Update Access

これらのメソッドにより、特定のメモリ順序効果の下、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. Bitwise Atomic Update Access

このアクセスレベルを持つメソッドにより、特定のメモリ順序効果の下でアトミックにビット単位の演算を実行できます。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. Memory Ordering Effects

先ほどから、VarHandleメソッドは特定のメモリ順序効果の下で変数にアクセスできる、と述べてきました。ほとんどのメソッドでは、4つのメモリ順序効果があります。

  • プレーンな読み書きは、32ビット以下の参照とプリミティブに対してビット単位でのアトミック性を保証します。また、他の特徴に関しては順序制約を課すことはありません。
  • Opaque操作は、同じ変数へのアクセスに関して、ビット単位でアトミックであり、一貫して順序付けられています。
  • Acquire操作とRelease操作は、Opaqueの特性に従います。また、Acquireの読み込みは、Releaseモードの書き込みと一致した後にのみ順序付けられます。
  • Volatile操作は、互いに完全に順序付けられています。

アクセスモードが以前のメモリ順序付け効果を上書きすることを覚えておくことは非常に重要です。これは、例えば get() を使用した場合、変数をvolatileと宣言していたとしても、それはプレーンな読み込み操作になることを意味します。

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

7. Conclusion

このチュートリアルではvarhandleとその使い方をご紹介しました。

varhandleは低レベルの操作を可能にするという目的を持つため、このトピックは非常に複雑です。必要でなければ使わないでおくべきです。

いつも通り、コードサンプルはGitHubにあります。

Core Java 9
https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-9-new-features

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中