Atomic〜とか中身でsynchronizedしてるだけじゃね〜(´・∀・`)
なんて思ってた。で、試す機会が来た。
import java.util.concurrent.atomic.AtomicInteger; public class Test{ //走らせるスレッドの数 static final int thread = 1000; //1つのスレッドの中でカウントする回数 static final int loop = 100000; //AtomicIntegerを使ってカウント static AtomicInteger acounter=new AtomicInteger(); static class Runnable1 implements Runnable{ public void run(){ for(int i=0;i<loop;i++)acounter.incrementAndGet(); } } //synchronizedでカウント static int counter = 0; static synchronized int count(){return ++counter;} static class Runnable2 implements Runnable{ public void run(){ for(int i=0;i<loop;i++)count(); } } public static void main (String[] args) throws InterruptedException{ Thread[] th = new Thread[thread]; //以下省略………。 //(Runnable1とRunnable2をthread個走らせて //終わるまでに何秒かかるか測定しているだけ) } }
Runnable1のAtomicIntegerを使ってカウントすると約3秒、Runnable2の自前でsynchronizedでカウントすると約25秒。ものすごい顕著な差が出た。
もっというと、Runnable2の方では走らせるスレッド数を10000にするとエラーになる。※余談
あるぇえ?なんぞこれ。
これはAtomicIntegerを追ってみるしかないか。AtomicIntegerソースコード
AtomicIntegerのソースコードからincrementAndGet()関数を抜き出して分かりやすく書き換えてみた
//AtomicIntegerの持っている値 private volatile int value=0; //sun.misc.Unsafeのインスタンス private static final Unsafe unsafe = Unsafe.getUnsafe(); //変数valueのポインタ(の様な物)を取得 static final long valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); public final int incrementAndGet() { while(true) { int current = value; int next = current + 1; if ( unsafe.compareAndSwapInt(this,valueoffset,current, next) ) return next; } }
なんだか、すごいことしとるでぇえええ。
Unsafe*1というのは、コンストラクターを呼んでいないメモリ確保しただけのインスタンスを作成したり、アドレス指定して指定先を書き換えるなどできるJVMと密着したチートクラス。私の環境ではセキュリティーマネージャーのアクセス制限に引っかかって使えない。なんだかおもしろそうなのに………orz
上のソースでvalueoffsetは変数valueのポインタだと思えばいい。わぉ、Javaなのにポインタと来たか。
compareAndSwapIntはvalueoffsetの指す値がcurrentと同じだった場合、その値をnextに書き換える、という一連の動作をアトミックに行う。
アトミックというのは、これ以上分割できない処理の単位の事。
今回は値読み込み→比較→値の書き換えという一連の動作をこれ以上分割できない一単位として行う。
分割できない1単位なので、読み込みして比較するまでの間に読み込んだ変数の値が書き換わったりすることはない。もちろん、値を書き換える間もだ。ソースコードをこれ以上追っていないのでただの憶測だが、名前や引数からしてCPUのCAS命令(wikipedia)系統の命令を呼び出す物と思われる。
書き換えに成功したらnextを返す。失敗したら成功するまで何度でも同じ事を繰り返す。
これによりvalueは誰かが書き換えてる途中、同じ値で誰かがまた書き込むということが起こらずに安全に値を増やすことが出来る、という事らしい。
いや〜、atomicパッケージすげぇべ!
余談ですがcounterをvolatileにするだけじゃ駄目です。
public class Test{ static final int thread = 1000; static final int loop = 100000; static volatile int counter = 0; static class Runnable3 implements Runnable{ public void run(){ for(int i=0;i<loop;i++)counter++; } } public static void main (String[] args) throws InterruptedException{ Thread[] th = new Thread[thread]; for(int i=0;i<thread;i++){ th[i] = new Thread(new Runnable3()); th[i].start(); } for(Thread t:th)t.join(); System.out.println((double)counter*100/thread/loop+"%"); } }
出力結果は51.490842%でした。インクリメントはアトミックな操作じゃないので読み込みで同じ値を2回に1回(!)読み込んでしまってるんですね。