注意:この記事は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()); } }