volatileで排他制御出来ると、いつから思ってた? atomicやsynchronized使うべし

More than 1 year has passed since last update.

久々に volatileが使われているソース
しかも使い方間違ってて、ちゃんと同期出来てないものに
出会ったので、確認コード

複数のスレッドから、同じカウンターをインクリメントするサンプル

test.java
package test;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class Sync {
    private final int THREAD_NUM = 50;
    private final int COUNT_NUM = 1000;

    private static Sync instance = new Sync();


    private int mCounter=0;
    private volatile int mVolatileCounter=0;
    private Object mSyncObj = new Object();
    private AtomicInteger mAtomCounter = new AtomicInteger(0);

    public static void main(String[] args) {
        instance._run( "dummy", instance._no_sync );
        instance.mCounter = 0;
        instance._run( "_no_sync", instance._no_sync );
        instance.mCounter = 0;
        instance._run( "_synchronized", instance._synchronized );
        instance._run( "_volatile", instance._volatile );
        instance._run( "_atom", instance._atom );
        instance.mAtomCounter.set(-1);
        instance._run( "_lock_free", instance._lock_free );
    }


    private Runnable _no_sync   =           
        new Runnable(){
            public void run() {
                for (int i = 0; i < COUNT_NUM; i++) {
                    System.out.print( mCounter++ + ",");
                }
            }   
        };

    private Runnable _synchronized =
        new Runnable(){
            public void run() {
                for (int i = 0; i < COUNT_NUM; i++) {
                    synchronized(mSyncObj){
                        System.out.print( mCounter++ + ",");
                    }
                }
            }
        };

    private Runnable _volatile = 
        new Runnable(){
            public void run() {
                for (int i = 0; i < COUNT_NUM; i++) {
                    System.out.print( mVolatileCounter++ + ",");
                }
            }                       
        };

    private Runnable _atom = 
            new Runnable(){
        public void run() {
            for (int i = 0; i < COUNT_NUM; i++) {
                System.out.print( mAtomCounter.getAndIncrement() + ",");    
            }
        }                       
    };

    private Runnable _lock_free = 
            new Runnable(){
        public void run() {
            for (int i = 0; i < COUNT_NUM; i++) {
                int n;
                do{
                    n = mAtomCounter.get();
                }
                while(!mAtomCounter.compareAndSet(n, n+1));
                        // print前に他スレッドからcompareAndSetが呼ばれ、出力がずれるので修正
                // System.out.print( mAtomCounter.get() + ",");                 
                System.out.print( n+1 + ",");                   

            }
        }                       
    };


    public void _run( String sMsg, Runnable runnable){

        System.out.println( "[" + sMsg + "]  Start!!");     

        long start = System.currentTimeMillis();

        ArrayList<Thread>   thread = new ArrayList<Thread>();
        for( int i=0; i<THREAD_NUM; i++ ){
            Thread tt = new Thread( runnable );
            thread.add(tt);
            tt.start();
        }

        for( Thread t : thread ){
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        System.out.println("\n" + sMsg + ": " + (System.currentTimeMillis() - start) + "ms");       
    }
}

C++ゲンガーなので変な書き方だったらすみません。
まず、今回は コンソール出力が重いので 時間は無視して

結果の見方

synchronized以外は、出力までatomic性を求めてないので
数値の順番はどうでもよい。
重複(抜け)があると、同期に問題がある

データを スレッド起動より500ms待機させ
よりスレッドの衝突がしやすいものにかえた
(asahina_devさんありがとうございます)
ので、スレッド数10、ループ3回で差が出たので
そのデータにさしかえました

LockFreeがConsole出力前に他スレッドがカウンター書き換える可能性があるので
少し修正しました

同期しない:
2,4,6,4,3,3,11,2,14,2,16,15,13,12,10,9,7,8,6,5,23,22,21,20,19,18,17,26,25,24,
重複、抜けがあります
抜け:0,1,27,28,29
重複:2,2,3,4,6,

synchronized:
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29
パーフェクト!
もちろん出力までsynchronizedしてるので出力までAtomic

volatile:
0,2,0,4,5,1,0,6,3,8,7,9,12,14,13,10,11,19,18,17,16,15,24,23,22,21,20,25,26,27,
重複、抜けあり
抜け:28,29,30
重複:0,0,0,

atomic:
1,8,4,6,5,2,3,7,16,18,15,14,13,12,11,10,0,9,25,24,23,22,21,20,19,17,28,27,26,29,
重複データなし。Atomic性保たれてます

LockFree:
0,3,4,5,2,8,6,7,1,16,15,14,13,12,11,10,9,24,23,22,21,20,19,18,17,25,26,27,28,29,
atomicと同じく重複データなし。Atomic性保持。

速度(参考)

データ量を100倍、スレッド数2倍にして
Console出力命令を消し
各機能の10回平均

同期なし: 7ms
そりゃー速い!!

volatile: 180ms
速いけど、カウンターとして正しく動作しない

synchronized: 403ms
意外に速かった!!
カウンター以外の処理もAtomic性を保てるので
基本的に synchronized使えばよくね?

atomic: 1089ms
予想外に遅かった・・・
でも、条件によってはsynchronizedより速い事もある・・はず

lock_free: 1094ms
多分 getAndIncrement等は、8086系では少なくとも compareAndSet
で実装してるのでしょう
上記とだいたい同じ

結論

volatileは同期されると思うな。volatileは基本 コンパイル最適か抑制。
同期したければ素直に synchronized 使う
Atomic系の処理は、よく考えて使おう。synchronizedの方が速いケースも多い

間違い指摘あったらおねがいします。