プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

Java9のVarHandleを使ってみた

 注意:この記事はJava9が公開される前、アーリーアクセス版時代の話です。今?試してないけど、変わって無さそうでしたよ。
 AtomicIntegerを使う必要が出てきた場面で、ふとVarHandleのことを思い出し、ちょっと試してみました。
 試したコードは最後に載せておきますが、やってみたことはこんな感じ。

  • staticなint型変数のVarHandleの取得
  • setとsetVolatileの実行
  • int,volatile int,int(synchronized),AtomicIntegerで並列カウントし、結果の出力と簡易的な時間の計測



VarHandleの取得

 まずこの時点でつまった。JavaDocに書いておいてよ、取得方法。※Java9が公開された現在、JavaDocに詳しく書いてあります。
 たぶん、こんな感じ。まず、Hogeにstaticなaと、フィールドメンバのbがあるとする。

public class Hoge{
  static int a;
  int b;
  int[] arr;
}

 これの取得はたぶん、こんな感じ。(後述するテストコードではlookup().in(Test.class)というのをキャッシュしてから実行してる)

java.lang.invoke.MethodHandles.lookup()
                .findStaticVarHandle(Hoge.class,"a",int.class);
java.lang.invoke.MethodHandles.lookup()
                .findVarHandle(Hoge.class,"b",int.class);
//配列の「要素」を操作したい場合は以下のメソッドでVarHandleを取得する
java.lang.invoke.MethodHandles.arrayElementVarHandle(int[].class);




代入

 VarHandleのJavaDocを眺めていると、値を代入する単純なメソッドはsetとsetOpaque,setReleaseとsetVolatileがあるっぽい。

  • setは普通の代入の代わりっぽい。
  • setOpaqueはプログラムオーダーは保証されるけど、スレッド間のメモリの整合性を考慮しない普通の代入?
  • setReleaseはリオーダーがされないことが保証されるっぽい。setReleaseを使えばJavaでダブルチェックロッキングパターンが実装可能な感じかな?
  • setVolatileがvolatileが付与されているかのごとく代入するっぽい。setReleaseとの差はよく分からん。

 まぁ、よくわからんけど、volatile修飾子がついていてもsetで代入できるし、volatile修飾子がついていなくてもsetVolatileが実行できるっぽい。

        //------ set COUNTER ----------------------
        System.out.println("---------Method set------");
        COUNTER_HANDLE.set(10); // volatileではない変数のハンドル
        V_COUNTER_HANDLE.set(10);//volatileな変数のハンドル。setVolatileじゃなくても代入可能

        System.out.println("---------Method setVolatile------");
        COUNTER_HANDLE.setVolatile(0);//volatileではない変数のハンドル。setじゃなくても代入可能
        V_COUNTER_HANDLE.setVolatile(0);////volatileな変数のハンドル。

 なお、staticな変数でない場合は、以下のようにする

handle.set(obj,0)

 配列の要素を操作したい場合は、以下のようにする

handle.set(arr,index,value)




カウンターの測定

 以下の4つの条件でカウンターを動かし、実行時間の測定と、最終結果が正しいかどうかを確認する。

  • volatileな変数をVarHandleでインクリメント
  • volatileな変数をVarHandleでインクリメント
  • 非volatileな変数をsynchronizedした関数でインクリメント
  • AtomicIntegerでインクリメント

 ちなみに、2016/11/28日現在のJDK9のソースコードではAtomicIntegerはまだUnsafeを使っているようです。

 まずは、シングルスレッドで20億回インクリメントして測定したもの。(※後述のソースコードには存在していない

内容 最終結果 計測時間
volatileな変数をVarHandleでインクリメント 2000000000 約10秒
volatileな変数をVarHandleでインクリメント 2000000000 約10秒
非volatileな変数をsynchronizedした関数でインクリメント 2000000000 約40秒
AtomicIntegerでインクリメント 2000000000 約10秒

 
結果からシングルスレッドではVarHandleとAtomicIntegerの速度は同等であることが言える。


 次に200万回インクリメントする関数を、1000スレッド起動して実行し、最終結果と実行時間を計測した。実行時間の計測には本当ならJVMが安定するまでの時間等を考慮するべきだけど、結構時間かかる処理だから誤差でしょってことでその辺は考慮していない。

内容 最終結果 計測時間
volatileな変数をVarHandleでインクリメント 2000000000 約40秒
volatileな変数をVarHandleでインクリメント 2000000000 約40秒
非volatileな変数をsynchronizedした関数でインクリメント 2000000000 約38秒
AtomicIntegerでインクリメント 2000000000 約40秒

 おんや?volatileでなくても結果が正しく、また、何度実行してもsynchronizedが他のよりも2,3秒早いという結果になりました。
 前者についてはJavaDocを読んでみると、VarHandle.getAndAdd()が内部でVarHandle.setVolatile()を使っているようで、volatileがついていなくてもvolatileがついているのと同等に扱われたためかと思います。
 後者については、AtomicIntegerとかVarHandleはCPUが常に100%になるのにたいして、synchronizedするとスレッドが待機状態になるから、あまりCPUリソースを使わないのが原因かと思います。
 
 これではちょっと結果としてどうかという気がしたので、条件を変更して250000000回インクリメントする関数を8スレッド動かしてみました。この位のスレッド数なら、通常でもあり得るでしょう。

内容 最終結果 計測時間
volatileな変数をVarHandleでインクリメント 2000000000 約38秒
volatileな変数をVarHandleでインクリメント 2000000000 約38秒
非volatileな変数をsynchronizedした関数でインクリメント 2000000000 約64秒
AtomicIntegerでインクリメント 2000000000 約38秒

 おんやぁ?私の予想に反して、synchronizedがえっらい遅くなっただけですね。(VarHandleとかがもうちょっと早くなるかと思ってた)
 ま、まぁ、synchronizedが遅いという結果が得られたので良しとしましょう。

 上記の結果をまとめると以下のようになる。

  • AtomicInteger(Unsafeを使用)とVarHandleは速度が同等
  • volatileであってもなくても、VarHandle.getAndAddが中でsetVolatileを使っているためか結果は同じ
  • volatileであってもなくても、VarHandleを使った場合実行時間は同じ



使ってみた所感


 ぶっちゃけ使いにくい。使うの面倒くさいわー。AtomicIntegerで良いと思うわー。


 かつて、以下↓のような構文が使えるようになるとか何とか聞いた気がしたんだけど、早くプリーズ。

 field.volatile.incrementAndGet();



テストコード

 

import java.lang.invoke.*;
import java.util.concurrent.atomic.*;

public class Test{

    private int a = 0;
    private int[] arr = new int[10];
    private final VarHandle aHandle,arrHandle;

    public Test(){
        VarHandle ahandle=null,arrhandle=null;
        try{
            ahandle=MethodHandles.lookup().findVarHandle(Test.class,"a",int.class);
            arrhandle=MethodHandles.arrayElementVarHandle(int[].class);
        }catch(Throwable t){
            t.printStackTrace();
        }
        this.arrHandle =arrhandle;
        this.aHandle =ahandle;
        aHandle.set(this,100);
        System.out.println("a = "+a);
        arrHandle.set(arr,5,100);
        System.out.println("arr[5] = "+arr[5]);
    }



    private static int COUNTER=1234;
    private static final VarHandle COUNTER_HANDLE;//COUNTERのハンドル

    private static volatile int V_COUNTER=5678;//※ volatileを付加
    private static final VarHandle V_COUNTER_HANDLE;//V_COUNTERのハンドル

    private static int S_COUNTER = 0; // synchronizedな関数でカウントする為の変数

    private static final AtomicInteger ACOUNTER=new AtomicInteger(0);



    //----  VarHandleの初期化。たぶんこんな感じ---------
    static{
        VarHandle handle = null,vhandle=null;
        try{
            MethodHandles.Lookup lookup = MethodHandles.lookup().in(Test.class);
            handle = lookup.findStaticVarHandle(Test.class,"COUNTER",int.class);
            vhandle = lookup.findStaticVarHandle(Test.class,"V_COUNTER",int.class);
        }catch(Throwable t){
            t.printStackTrace();
        }
        COUNTER_HANDLE = handle;
        V_COUNTER_HANDLE = vhandle;
    }
    //-----------------------------------

            


    public static void count(){
        COUNTER_HANDLE.getAndAdd(1);
    }

    public static void vcount(){
        V_COUNTER_HANDLE.getAndAdd(1);
    }

    public static synchronized void scount(){
        S_COUNTER++;
    }



    public static final int LOOP = 2000000000/8;
    public static final int THREADS = 8;
    public static void run1(){
        for(int i=0;i<LOOP;i++){
            count();
        }
    }
    public static void run2(){
        for(int i=0;i<LOOP;i++){
            vcount();
        }
    }
    public static void run3(){
        for(int i=0;i<LOOP;i++){
            scount();
        }
    }
    public static void run4(){
        for(int i=0;i<LOOP;i++){
            ACOUNTER.incrementAndGet();
        }
    }


    public static void main(String[] args){
        new Test();


        showCounter();
        showVCounter();
        
        //------ set COUNTER ----------------------
        System.out.println("---------Method set------");
        COUNTER_HANDLE.set(10);
        V_COUNTER_HANDLE.set(10);//setVolatileじゃなくても代入可能
        showCounter();
        showVCounter();
        System.out.println("---------Method setVolatile------");
        COUNTER_HANDLE.setVolatile(0);//setじゃなくても代入可能
        V_COUNTER_HANDLE.setVolatile(0);
        showCounter();
        showVCounter();


        System.out.println("---------Run1 start------");
        long t = System.currentTimeMillis();
        Thread[] ths  = new Thread[THREADS];
        for(int i=0;i<THREADS;i++){
            ths[i] = new Thread(Test::run1);
            ths[i].start();
        }
        for(int i=0;i<THREADS;i++){
            try{
                ths[i].join();
            }catch(InterruptedException e){}
        }
        t = System.currentTimeMillis() - t;
        showCounter();
        System.out.println("Time = " + t/1000d);


        System.out.println("---------Run2 start------");
        t = System.currentTimeMillis();
        for(int i=0;i<THREADS;i++){
            ths[i] = new Thread(Test::run2);
            ths[i].start();
        }
        for(int i=0;i<THREADS;i++){
            try{
                ths[i].join();
            }catch(InterruptedException e){}
        }
        t = System.currentTimeMillis() - t;
        showVCounter();
        System.out.println("Time = " + t/1000d);



        System.out.println("---------Run3 start------");
        t = System.currentTimeMillis();
        for(int i=0;i<THREADS;i++){
            ths[i] = new Thread(Test::run3);
            ths[i].start();
        }
        for(int i=0;i<THREADS;i++){
            try{
                ths[i].join();
            }catch(InterruptedException e){}
        }
        t = System.currentTimeMillis() - t;
        System.out.println("S_COUNTER="+S_COUNTER);
        System.out.println("Time = " + t/1000d);

        System.out.println("---------Run4 start------");
        t = System.currentTimeMillis();
        for(int i=0;i<THREADS;i++){
            ths[i] = new Thread(Test::run4);
            ths[i].start();
        }
        for(int i=0;i<THREADS;i++){
            try{
                ths[i].join();
            }catch(InterruptedException e){}
        }
        t = System.currentTimeMillis() - t;
        System.out.println("ACOUNTER="+ACOUNTER.get());
        System.out.println("Time = " + t/1000d);
        
    }


    public static void showCounter(){
        System.out.print("COUNTER =");
        System.out.println(COUNTER_HANDLE.get());
    }
    public static void showVCounter(){
        System.out.print("V_COUNTER =");
        System.out.println(V_COUNTER_HANDLE.get());
    }

}