問題提起
Java逆引きレシピを読んでたら
Class Count {
private volatile int count;
public void increment() {
count++;
}
@Override
public String toString() {
return "count: " + count;
}
}
というようなコード例が見られたが、synchronizedされていない状態でvolatile変数をインクリメント、もしくはデクリメントするのはやめよう。
原因
インクリメントもしくはデクリメントはatomicな操作ではないため。もう少し具体的に書くと、インクリメントの動作の流れは以下の通り。
- 現在の値を取得
- 1.で取得した値に+1した値を代入
ここで、1と2の操作の間に別のスレッドによって何らかの値が代入されると、想定外の動作となってしまう。
修正方法
AtomicIntegerやAtomicLongという便利なクラスが標準で用意されているので、複数スレッドからsynchronizedなしに参照される値には、もれなくこちらを使いましょう。
import java.util.concurrent.atomic.AtomicInteger;
Class Count {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
@Override
public String toString() {
return "count: " + count.get();
}
}
Java8以降では、LongAdderというクラスも用意されています。JavaDocによると、以下のような特徴があるようです。
This class is usually preferable to AtomicLong when multiple threads update a common sum that is used for purposes such as collecting statistics, not for fine-grained synchronization control.
This class extends Number, but does not define methods such as equals, hashCode and compareTo because instances are expected to be mutated, and so are not useful as collection keys.
後半は注意ですね。HashMapのKeyに入れたりするのはやめましょう。
歯止め
先ほどのコードで静的解析ツールのFindbugsを使うと警告が出るので、導入するのもよいと思います。この件以外にも様々なバグが見つかるので非常に便利です。
おわりに
こんな誤植を見つけてしまいましたが、良い本だと思います。AtomicInteger, AtomicLongの説明も載っていますし、LongAdderはこの本で初めて知りました。そもそも、逆引きはすごい便利です。Kindle版もあるよ。