西海岸より

つらつらざつざつと

Atomicな変数の利用

昨日の続き。シンプルに複数スレッドでvalueというカウンターをインクリメントしていくコード

  • スレッドセーフでは無いコード
public final class Counter {
    private long value = 0;
    static final int threadNum = 1000;
    static final int loopNum = 1000;
    
    public long getValue(){
        return value;
    }
    public void increment(){
        if(value == Long.MAX_VALUE)
            throw new IllegalStateException("counter overflow");
        ++value;
    }
    
    public static void main(String[] args){
        CounterThread[] threads = new CounterThread[threadNum];
        
        for(int i=0; i<threads.length; i++){
            threads[i] = new CounterThread();
            threads[i].start();
        }
        
        for(Thread thread : threads){
            try{
                thread.join(100);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        System.out.println("Ideal: " + (threadNum * loopNum) );
        System.out.println("Real: " + CounterThread.getCounterValue() );
    }
}

class CounterThread extends Thread{
    
    private final static Counter counter = new Counter();

    public void run(){
        for(int k=0; k<Counter.loopNum; k++)
            counter.increment();
    }
    
    public static long getCounterValue(){
        return counter.getValue();
    }
}
  • 実行結果
Ideal: 1000000
Real: 985201

d:id:mmasashi:20090630 のようにlong valueにvolatileをつけても結果は同じ。
[++value]には読み込み->書き込みという処理プロセスが隠れているためで、確かに読み書きのタイミングでは同期されるけど、あるスレッドで読み出した直後に別スレッドも読み出しを行った場合に、両方とも同じ値に対してインクリメントして書き込むためカウントがロストしてしまう。

  • 解決方法

方法は二つ。

    • valueにアクセスする関数を同期化

valueにvolatileを付加する変わりに、getValueとincrementメソッドにsynchronizedをつけることで、インクリメントのプロセス全体として同期化が行われ、上記のような問題も起きない。getValueメソッドの同期化をする理由は昨日の記事の通り。

    • java.util.concurrent.atomic.AtomicLongを使う

Java5からは、このようなケースを考慮したAtomicなデータ型をサポートしており、これを利用することでプログラム自体が同期化を意識する必要がなくなる。またシンプルになるだけでなく、同期の幅も狭くなりJava5以上ならこちらを利用した方が良い。

  • AtomicLongを使う場合の修正箇所
    private AtomicLong value = new AtomicLong(0);
    ....

    public long getValue(){
        return value.longValue();
    }
    public void increment(){
        if(value.longValue() == Long.MAX_VALUE)
            throw new IllegalStateException("counter overflow");
		value.incrementAndGet();
    }

concurrentパッケージ以下はかなり洗練されているので、一度目を通しておくと良いと思う。