Edited at

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

More than 5 years have 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の方が速いケースも多い

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