マルチスレッドでintを使う
そもそもJavaやAndroidでintを使う場合,複数のスレッドから演算をするときはロックを取らないといけません.これは1行の演算やインクリメント・デクリメントでも同じです.
例えばi = i + 1
を演算する場合,まずi
を取得して,1を足してからまたi
に代入しますが,取得してから代入するまでに他のスレッドがi
の値を変更している可能性があります.
i++
と書いた場合も同様でインクリメントしている間に値が変わっている可能性があります.
アセンブラとかに詳しい人は「インクリメントなんて機械語でアトミックな処理があるじゃん」と思うかもしれませんが,i+=2
の場合はロックを取らないといけないことを考えると,言語仕様として一貫性を保つためにもi++
はアトミックな処理になっていないのではないか,と想像します.
そもそも本当に値は変わってるのか
「とはいってもそんなの滅多に起きないでしょ?」と思う気持ちはもっともなので試してみましょう.
public class AITest {
public int i;
public AITest(){
i = 0;
}
public static void main(String[] args) throws Throwable {
test1();
}
public static void test1() throws Throwable {
int threadNum = 20;
AITest test = new AITest();
ExecutorService service = Executors.newFixedThreadPool(threadNum);
long start = System.currentTimeMillis();
for (int n = 0; n < threadNum; n++) {
service.submit(()->{
for (int m = 0; m < 100000; m++) {
test.i++;
}
});
}
service.shutdown();
service.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(test.i+" : "+(System.currentTimeMillis() - start));
}
}
一応解説しておくと
1.Executors.newFixedThreadPool(threadNum)
で固定数のスレッドプールを作る
2.実行時間を計るために現在時刻の取得
3.スレッド数分のループを作って,各スレッドにタスクを投入する
4.各スレッドは10万回インクリメントする
5.service.shutdown()
とservice.awaitTermination()
を使って終了まで待機
6.値の出力(成功していれば20*100000=2000000になるはず)
さて,実行結果は
1264434 : 77
ということで盛大にズレます.ちなみに複数回行うと毎回違う値が出力されます.あとvolatile
を付けても関係ありません.volatile
は取得するときに最新の値が取れる,ぐらいに思っておけばいいです.
ついでに実行時間は77msecです.もっと早い環境で試験するならSystem.nanoTime()
を使ってください.
synchronizedを使ってロックする
もしかしたら一番一般的(?)なsynchronizedによる実装は下記の通り.testメソッドだけ載せます.
public static void test2() throws Throwable {
int threadNum = 20;
AITest test = new AITest();
ExecutorService service = Executors.newFixedThreadPool(threadNum);
long start = System.currentTimeMillis();
for (int n = 0; n < threadNum; n++) {
service.submit(()->{
for (int m = 0; m < 100000; m++) {
synchronized (test){
test.i++;
}
}
});
}
service.shutdown();
service.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(test.i+" : "+(System.currentTimeMillis() - start));
}
AITestクラスにsynchronized public void increment(){}
みたいなメソッドを追加しても同じです.で,結果は
2000000 : 171
ということで正確な値が出ました.かかった時間は171msecということでやはりロック取得に時間がかかってます.
AtomicInteger#incrementAndGet()を使う
ようやく本番です.
public class AITest {
public final AtomicInteger ai;
public AITest(){
ai = new AtomicInteger(0);
}
public static void test3() throws Throwable {
int threadNum = 20;
AITest test = new AITest();
ExecutorService service = Executors.newFixedThreadPool(threadNum);
long start = System.currentTimeMillis();
for (int n = 0; n < threadNum; n++) {
service.submit(()->{
for (int m = 0; m < 100000; m++) {
test.ai.incrementAndGet();
}
});
}
service.shutdown();
service.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(test.ai.get()+" : "+(System.currentTimeMillis() - start));
}
で,実行結果が下記の通り.
2000000 : 111
正確な値かつ,111msecということで結構早くなります.ということで,たかだか普通のintを足し算するときはちゃんとAtomicIntegerを使いましょう.
おまけ
AtomicLongっていうのもありますが,AtomicFloatとかAtomicDoubleってのはありません.
これは自分で作れってことらしいです.まぁintとlongがあれば作れるけどさ・・・.
追記
こっちの方法でもっとちゃんと計ってみました.結果はそんなに変わってませんが.