LoginSignup
3
1

More than 1 year has passed since last update.

JEP 193: Variable Handles について

Last updated at Posted at 2021-10-11

Java 9 で Variable Handles と呼ばれる新しい API が追加されました。これは Java 7 の MethodHandle を拡張し、クラス java.lang.invoke.VarHandle でフィールドの変数や配列に対して強い型付けを持って参照ができます。ただし変数の参照だけでなく、Atomic 操作やリフレクションなどの操作に利用可能です。

例えば、ある一つの変数に対してアトミック操作を行うことによって、並列プログラミング時に変数に対するアクセスを保護を行うことができます。
これまでは、アトミック関連クラスでこれらの操作を行ってきましたが、これからは、Variable Handles を使用することができます。

また、リフレクションの代わりに使用することもできます。リフレクション API と比較した場合、優れたパフォーマンスを発揮し、型安全性を持たせることができます。

つまり、Variable Handles はパフォーマンスが求められる場面における、リフレクションの代替として、もしくはアトミック操作を利用する場面において利用を検討することができます。

アトミック操作(不可分操作)とは
途中に割り込みが入らない操作の事で、例えば、銀行口座の入金や出金の処理では、金額の不一致を防ぐために排他制御が必要です。例えば、排他制御を行いアトミック性を確保します。

NOTE: 
Java で、変数に volatile キーワードをつけても Atomic にはならず排他制御には利用できません。
Variable Handles は、C/C++11 Atomic と互換性を持つように設計されています。今回の VarHandle を実装せずに、従来のAtomic API に対して修正を加えた場合、C/C++ 11 で追加されたメモリモデルを利用するために、追加のアクセス整合性ポリシーが必要となり、実装が複雑化する可能性がありました。それを避けるために Variable Handles を導入しています

C/C++11: 2011年に見直されたC言語の仕様の規格:  
C/C++11のメモリの取得と解放の詳細については、
https://cpprefjp.github.io/reference/atomic.html
https://cpprefjp.github.io/reference/atomic/memory_order.html

Variable Handle の目的

Variable Handle は下記の4点を考慮して提供される API です。

  • 安全性: JVM を壊さないように メモリを扱う
  • 整合性: フィールドへのアクセスは、getfield および putfield バイト・コードと同じアクセス規則に従う (final は操作不可)
  • パフォーマンス: sun.misc.Unsafe と同等のパフォーマンスを提供
  • ユーザビリティ: sun.misc.Unsafe よりも優れた API を提供

並列処理プログラミングの需要が増える一方で、今まで、クラスの変数(フィールド)に対する操作を、アトミック操作や順序付けされた操作として、うまく実装できない課題がありました。

これまで、これらを実現する為に、synchronized を使用するか、 java.util.concurrent.atomic を使用してきました。しかし、パフォーマンスが悪かったり、処理に対するオーバヘッドが大きいため、よりパフォーマンスを求めるために、sun.misc.Unsafe という、非推奨で非サポートの JVM の組み込み関数が使用されてきました(Java 1.5 以降では、java.util.concurrent.atomic 配下のクラスで内部的に Unsafe を利用)。 特に、Unsafe の組み込み関数は高速であるため、非推奨であるにもかかわらず、フレームワークやライブラリ等で幅広く利用され、結果として移植性や安全性が損なわれていました。

現在の実装の問題点

JEP 193: Variable Handles のオーナーの Paul Sandoz によると現在の問題点を、下記のように説明しています。

* Atomic* classes have overhead
    • Not used in j.u.concurrent classes
* sun.misc.Unsafe is... 
    unsafe, not “portable”, and going away
    • CAS is too important to relegate
* No unified/safe model for accessing on and off-heap

訳すと下記の通りです。

  • Atomic 関連のクラスは、オーバヘッドが高い
    • パフォーマンスが悪いため Atomic 関連クラスを java.util.concurrent クラスの中で使用していない
  • sun.misc.Unsafe は
    • 安全でなく、ポータビリティもない、そして今後取り除かれる。
    • CAS(Copy and Set)は移管するのにとても重要
  • Heap に対してアクセスするための統合的で安全なモデルがない

sun.misc.Unsafe を利用した実装例(非推奨)

sun.misc.Unsafe を使用した変数を Atomic で書き換える例を下記に記載します。

MyCounter のコンストラクターで counter のメモリ・アドレス の offset 値を取得しています。incrementAndGet() メソッドで、この offset 値を使用して変数(フィールド)の値を更新します。
書き換える対象の変数は、値を読み書きするすべてのスレッドから見えるように、volatile として宣言する必要があります。(JLS 8.3.1.4. volatile Fields)

incrementAndGet()メソッドでは while ループを使用し、成功するまで操作を繰り返し行います。 実際には compareAndSwapLong() を呼び出し、変数が記載されているメモリのオフセットを指定し、現在の値と比較したのち、以前の値を1増加しています。そして更新したのち、以前の値が変更されたかどうかを確認しています。ここではブロッキング処理は実装していないため、高速にクラス内のインスタンス変数を書き換えることができています。

しかし、型安全性がないため Java 言語の観点では推奨された実装方法ではありません。

import java.util.stream.IntStream;
import java.lang.reflect.Field;
import sun.misc.Unsafe;

class MyCounter {
    private Unsafe unsafe;
    private volatile long counter = 0;
    private long valueOffset;

    public MyCounter() throws IllegalAccessException, NoSuchFieldException {
        unsafe = getUnsafe();
        valueOffset = unsafe.objectFieldOffset(MyCounter.class.getDeclaredField("counter"));
    }

    public static void main(String... argv){
        try{
            MyCounter main = new MyCounter();

            IntStream.range(0,100)
                .forEach(num -> {
                    //実際にはここが並列処理として実行される
                    System.out.println(main.incrementAndGet());
                });
        }catch (IllegalAccessException | NoSuchFieldException e){
            e.printStackTrace();
        }
    }

    private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        return (Unsafe) f.get(null);
    }

    public long incrementAndGet() {
        long current = counter;
        long next = current + 1;

        while (!unsafe.compareAndSwapLong(this, valueOffset, current, next)) {
            counter = next;
        }
        return counter;
    }
}

Java 11 移行の sun.misc.Unsafe の扱い

sun.misc.Unsafe は Java11 から非サポートになります。

java.util.concurrent.atomic パッケージ配下の Atomic* 関連クラスも、将来的に VarHandles で書き換えられるようです。
実際に、Java 17 の java.util.concurrent.atomic.AtomicInteger のソースコードを確認した所、下記のようにコメントが記載されていました。これによると、現時点では、まだ起動時の依存関係で未解決の問題があるため Unsafe を利用しているようですが、将来的には書き換えられると思われます。

import jdk.internal.misc.Unsafe;

    /*
     * This class intended to be implemented using VarHandles, but there
     * are unresolved cyclic startup dependencies.
     */
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final long VALUE
        = U.objectFieldOffset(AtomicInteger.class, "value");

Java 11 で危険を承知で利用したい場合は、module-info.java に require を記載する等の方法で回避できますが
基本的には非推奨なため、ここではその詳細については割愛します。

java.lang.invoke.VarHandle の利用方法

VarHandle を利用するためには、下記のクラスを利用します。

クラス java.lang.invoke.VarHandle

サンプルコード

複数のスレッドから変更されるインスタンス変数(フィールド)を持つクラスを定義します。

import java.nio.ByteBuffer;

public class Data {
    public int counter = 1;
    private int privateField = 10;
    public String name = "Yoshio Terada";
    public byte[] data = new byte[]{1, 0, 0, 0, 1, 0, 0, 0};
    public char[] charArray = new char[]{'A','B','C','D','E','F'};
    public ByteBuffer dataBuffer = ByteBuffer.wrap(this.data);
}

VarHandle を利用して読み書きをするコード例を下記に示します。ここでは最初に ATOMIC 操作ではない API を利用しています。

    public void nonAtomicGetAndSetEvaluation(Data data){
        try {
            VarHandle varHandle = MethodHandles.lookup().findVarHandle(Data.class,"counter", int.class);
            Data data = new Data();

            //Read Access
            System.out.println(varHandle.get(data)); //アクセスモード: GET
            System.out.println(varHandle.getVolatile(data)); //アクセスモード: GET_VOLATILE
            System.out.println(varHandle.getOpaque(data)); //アクセスモード: GET_OPAQUE
            System.out.println(varHandle.getAcquire(data)); //アクセスモード: GET_ACQUIRE

            //Write Access
            int newValue = 2;
            varHandle.set(data, newValue); //アクセスモード: SET
            System.out.println(varHandle.get(data)); //アクセスモード: GET
            varHandle.setVolatile(data, newValue + 1); //アクセスモード: SET_VOLATILE
            System.out.println(varHandle.get(data));
            varHandle.setOpaque(data, newValue + 2); //アクセスモード: SET_OPAQUE
            System.out.println(varHandle.get(data));
            varHandle.setRelease(data, newValue + 3); //アクセスモード: SET_RELEASE
            System.out.println(varHandle.get(data));

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void arrayCheck(Data data) {
        VarHandle byteArrayViewVarHandle = MethodHandles.arrayElementVarHandle(char[].class);
        // char 配列 {'A','B','C','D','E','F'} より char を一つづつ取得
        for (int i = 0; i < data.charArray.length; i++) {
            var out = byteArrayViewVarHandle.getAcquire(data.charArray, i);
            System.out.println(out);
            // char が 'C' , 'D' ならば 'Z' に置き換え
            if (out.equals('C')|| out.equals('D')) {
                byteArrayViewVarHandle.setRelease(data.charArray, i, 'Z');
            }
        }
        // char 配列は {'A','B','Z','Z','E','F'} に置き換わる
    }

ソースコードの説明

VarHandle オブジェクトは、MethodHandles.Lookup クラスのファクトリ・メソッドを使用して作成できます。

Lookup のファクトリから VarHandle を生成するために3種類のメソッドが用意されています。インスタンス変数、クラス変数用、そしてリフレクションの代わりに利用可能な VarHandle を生成することができます。

VarHandle staticVarHandle = MethodHandles.lookup()
  .findStaticVarHandle(Hello.class, "staticConter", int.class);
VarHandle varHandle = MethodHandles.lookup()
  .findVarHandle(Hello.class, "count", int.class);
VarHandle unreflectVarHandle = MethodHandles
  .lookup()
  .unreflectVarHandle(Hello.class.getDeclaredField("count"));

アクセスモード

VarHandle は異なるアクセスモードで変数への読み取り、書き込みのアクセスをサポートしています。

列挙型 VarHandle.AccessMode に31 個のアクセスモードが定義されており、AccessMode ごとに、VarHandle 内に対応メソッドがあります。たとえば、アクセス モード GET_AND_ADD の場合、VarHandle.getAndAdd() メソッドを使用します。
上記のサンプルコードでは、ATOMIC ではないアクセス・モードを利用しています。

ATOMIC ではない読み書き用のアクセス・タイプと対応するメソッド

アクセスモード 対応するメソッド  ATOMIC か否か
GET get() NON ATOMIC
GET_ACQUIRE getAcquire() NON ATOMIC
GET_OPAQUE getOpaque() NON ATOMIC
GET_VOLATILE getVolatile() NON ATOMIC
SET set() NON ATOMIC
SET_OPAQUE setOpaque() NON ATOMIC
SET_RELEASE setRelease() NON ATOMIC
SET_VOLATILE setVolatile() NON ATOMIC

ATOMIC で読み書きをするアクセスタイプと対応するメソッドを下記に示します。

  • アトミックで比較・設定する更新アクセスモード
アクセスモード 対応するメソッド  ATOMIC か否か
COMPARE_AND_EXCHANGE compareAndExchange() ATOMIC
COMPARE_AND_EXCHANGE_ACQUIRE compareAndExchangeAcquire() ATOMIC
COMPARE_AND_EXCHANGE_RELEASE compareAndExchangeRelease() ATOMIC
COMPARE_AND_SET compareAndSet() ATOMIC
GET_AND_SET getAndSet() ATOMIC
GET_AND_SET_ACQUIRE getAndSetAcquire() ATOMIC
GET_AND_SET_RELEASE getAndSetRelease() ATOMIC
WEAK_COMPARE_AND_SET weakCompareAndSet() できる限り(Possibly) ATOMIC
WEAK_COMPARE_AND_SET_ACQUIRE weakCompareAndSetAcquire() できる限り(Possibly) ATOMIC
WEAK_COMPARE_AND_SET_PLAIN weakCompareAndSetPlain() できる限り(Possibly) ATOMIC
WEAK_COMPARE_AND_SET_RELEASE weakCompareAndSetRelease() できる限り(Possibly) ATOMIC
  • 数値用のアトミック更新アクセスモード
アクセスモード 対応するメソッド  ATOMIC か否か
GET_AND_ADD getAndAdd() ATOMIC
GET_AND_ADD_ACQUIRE getAndAddAcquire() ATOMIC
GET_AND_ADD_RELEASE getAndAddRelease() ATOMIC
  • ビット単位でのアトミック更新アクセスモード
アクセスモード 対応するメソッド  ATOMIC か否か
GET_AND_BITWISE_AND getAndBitwiseAnd() ATOMIC
GET_AND_BITWISE_AND_ACQUIRE getAndBitwiseAndAcquire() ATOMIC
GET_AND_BITWISE_AND_RELEASE getAndBitwiseAndRelease() ATOMIC
GET_AND_BITWISE_OR getAndBitwiseOr() ATOMIC
GET_AND_BITWISE_OR_ACQUIRE getAndBitwiseOrAcquire() ATOMIC
GET_AND_BITWISE_OR_RELEASE getAndBitwiseOrRelease() ATOMIC
GET_AND_BITWISE_XOR getAndBitwiseXor() ATOMIC
GET_AND_BITWISE_XOR_ACQUIRE getAndBitwiseXorAcquire() ATOMIC
GET_AND_BITWISE_XOR_RELEASE getAndBitwiseXorRelease ATOMIC

Atomic での更新サンプル

上記のアクセスモードの内 ATOMIC 操作が可能なメソッドを利用して実装を行います。数値用、ビット単位、比較をして更新するメソッドが用意されているため、必要に応じたメソッドを呼び出します。

    public void atomicUpdate(Data data){
        try {
            VarHandle varHandle = MethodHandles.lookup().findVarHandle(Data.class, "name", String.class);
            String expectedValue = "Yoshio Terada";
            String newValue = "I Love Duke!!";

            // compareAndSet() は比較・更新に成功したか否かを真偽値で返す (ここでは true が表示、値は I Love Duke に変更)
            System.out.println(varHandle.compareAndSet(data, expectedValue, newValue)); 
            // compareAndExchangeAcquire は読み込み時にメモリをバリアし比較・更新したい場合に利用、返り値は変更前の値(ここでは I Love Duke が表示、値は Yoshio Teradaに変更)
            System.out.println(varHandle.compareAndExchangeAcquire(data, newValue, expectedValue));
            // compareAndExchangeRelease は書き込み時にメモリをバリアし比較・更新したい場合に利用、返り値は変更前の値(ここでは Yoshio Terada が表示、値は I Love Dukeに変更)
            System.out.println(varHandle.compareAndExchangeRelease(data, expectedValue, newValue));
            // getAndSetAcquire は読み込み時にメモリをバリアし更新したい場合に利用、返り値は変更前の値(ここでは I Love Duke が表示、値は Yoshio Teradaに変更)
            System.out.println(varHandle.getAndSetAcquire(data, expectedValue));       
            //最後に Yoshio Terada が表示される
            System.out.println(data.name);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void numericAtomicUpdate (Data data) {
        try {
            VarHandle varHandle = MethodHandles.lookup().findVarHandle(Data.class, "counter", int.class);
            final int adder = 1;

            //読み込み時にメモリをバリアしたい場合 Acquire を利用、返り値は変更前の値(ここでは1が表示)
            System.out.println(varHandle.getAndAddAcquire(data,adder));
            //書き込み時にメモリをバリアしたい場合 Release を利用 返り値は変更前の値 (ここでは2が表示)
            System.out.println(varHandle.getAndAddRelease(data,adder));
            System.out.println(data.counter); // (ここで3が表示)
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

リフレクションの代わりに Variable Handles を利用する

最後に、リフレクション API を利用する代わりに、VarHandle を利用する方法を紹介します。

ここでは、private フィールドにアクセスし、その値を確認したり、更新するサンプルを下記に紹介します。MethodHandles のルックアップで unreflectVarHandle() を呼び出して VarHandle のインスタンスを生成します。
そして、get(), set() などのメソッドを呼び出して値を更新しています。

public class Data {
    private int privateField = 10;
}

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;

public class DataUpdater {
    public static void main(String... argv) {
        DataUpdater main = new DataUpdater();
        Data data = new Data();
        main.insteadOfReflectionTest(data);
    }

    public void insteadOfReflectionTest() {

    public void insteadOfReflectionTest(Data data) {
        try {
            Class<? extends Data> class1 = data.getClass();
            Field privateField = class1.getDeclaredField("privateField");

            // private フィールドにアクセスするための VarHandle を取得
            VarHandle unreflectVarHandle = MethodHandles.privateLookupIn(class1, MethodHandles.lookup())
                    .unreflectVarHandle(privateField);

            // private フィールドの現在値を取得 10 を出力
            System.out.println(unreflectVarHandle.get(data));
            // private フィールドを新しい値で更新
            unreflectVarHandle.set(data, 20);
            // private フィールドの現在値を取得 20 を出力
            System.out.println(unreflectVarHandle.get(data));

        } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

メモリ・フェンス

今回、詳しく取り上げていませんが、VarHandle は C++11 の atomic_thread_fence に対応したメモリの順番を制御するためのメモリ・フェンスの機能も持っています。

フェンス操作は、メモリ順序をきめ細かく制御するための最小限の機能を提要しています。VarHandle では、異なるフェンスを作成するために 5 つの static メソッドを提供しています。

フェンスの強弱

loadLoadFence fence(弱) < acquire fence(中) < full fence(強)

storeStoreFence fence(弱) < release fence(中) < full fence(強)

Method フェンス前の操作 フェンス後の操作 C++ 11 atomic_thread_fence との対応 意味
fullFence() loads and stores loads and stores memory_order_seq_cst フェンス前のロードとストアが、フェンス後、ロードとストアで並べ替えられないようにする
acquireFence() loads loads and stores memory_order_acquire フェンス前のロードがフェンス後にロードおよびストアで並べ替えられないようにする
releaseFence() loads and stores stores memory_order_release フェンス前のロードとストアがフェンス後にストアで並び替えられないようにする
loadLoadFence() loads loads フェンス前のロードがフェンス後のロードと並び替えられないようにする
storeStoreFence() stores stores フェンス前のストアがフェンス後のストアと並べ替えられないようにする

まとめ

上記のサンプル・コードで示したように、VarHandle はアトミック操作やリフレクションの代わりに、利用できることがわかりました。アトミック関連クラスは、利用する際にオーバヘッドが高く、sun.misc.Unsafe はハイ・パフォーマンスではあるものの、非推奨のクラスでした。こうした課題を解決するために提供された VarHandle はパフォーマンスが求められる場面において利用が可能です。

利用する上では、いくつかの注意点があります。そこで実際に使用する際には API ドキュメントを注意深く読んで、理解してお使いください。
注意点を理解した上でご利用いただく事で、推奨された方法で並列プログラミング時のパフォーマンスを向上させることも可能ですので、この記事を手始めにお試しください。

備考

VarHandle は既に Fork/Join 関連クラスで実際に sun.misc.Unsafe から書き換えられています。下記の ForkJoinPool の実装中では Fence の実際の使用例も記載されていますので、参照してください。

(参照:ForkJoinPool, ForkJoinTask)

また現在、Incubator のプロジェクトとして位置づけされている JEP 412: Foreign Function & Memory API (Incubator) は実装で Variable Handle を利用して実装しています。

Foreign Function & Memory API のソースコードはこちら

参考


TODO: 上記のサンプルをマルチ・スレッドのコードに書き直し、もう少し検証をしたい。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1