2013/11/15

[Java]マルチスレッドでの排他処理を行うsynchronizedを理解する

今までマルチスレッドプログラミングというものをしてこなかったので
排他処理とか synchronized とかなにそれオイシイの状態だったので
理解するために簡単なサンプルを Android で作ってみました。

参考
Javaスレッドメモ(Hishidama's Java thread Memo)

サンプルコードは以下にあります。
SynchronizedActivity.java | wada811/AndroidLibrary@wada811
AndroidLibrary ってなってるけどコレは Android 関係ないです。

メソッド修飾の synchronized

クラスメソッド

/**
 * クラスメソッドを synchronized にした場合のテスト実行メソッド
 */
public void syncClassMethodExecute(){
    PreferenceUtils.putString(this, SyncClassMethodExecuter.KEY, null);
    new Thread(new SyncClassMethodExecutorRunnable("A")).start();
    new Thread(new SyncClassMethodExecutorRunnable("B")).start();
    new Thread(new SyncClassMethodExecutorRunnable("C")).start();
}

public class SyncClassMethodExecutorRunnable implements Runnable {
    String threadName;

    public SyncClassMethodExecutorRunnable(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run(){
        SyncClassMethodExecuter.execute(self, threadName);
    }
}

/**
 * クラスメソッドを synchronized にした場合のテストクラス
 */
public static class SyncClassMethodExecuter {

    public static final String KEY = SyncClassMethodExecuter.class.getSimpleName();

    public static final synchronized void execute(Context context, String threadName){
        String lastThreadName = PreferenceUtils.getString(context, KEY, null);
        if(lastThreadName != null){
            LogUtils.d(threadName + ": do nothing. " + lastThreadName + " is executed.");
        }else{
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            LogUtils.d(threadName + ": execute");
            PreferenceUtils.putString(context, KEY, threadName);
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
synchronized にしてるから1秒待って A が実行されたら1秒後に B, C と実行されていて
ブロックされているのが実行ログからわかります。
11-14 22:57:39.801: [SynchronizedActivity#syncClassMethodExecute:115]
11-14 22:57:40.872: [SynchronizedActivity$SyncClassMethodExecutor#execute:152]A: execute
11-14 22:57:41.933: [SynchronizedActivity$SyncClassMethodExecutor#execute:145]B: do nothing. A is executed.
11-14 22:57:41.943: [SynchronizedActivity$SyncClassMethodExecutor#execute:145]C: do nothing. A is executed.
synchronized をとれば A, B, C 全部が execute を通ることも確認できます。

インスタンスメソッド

/**
 * インスタンスメソッドを synchronized にした場合のテスト実行メソッド
 */
public void syncThisInstanceMethodExecute(){
    LogUtils.d();
    SyncThisInstanceMethodExecutor executor = new SyncThisInstanceMethodExecutor();
    new Thread(new SyncThisInstanceMethodExecutorRunnable(executor, "A")).start();
    new Thread(new SyncThisInstanceMethodExecutorRunnable(executor, "B")).start();
    new Thread(new SyncThisInstanceMethodExecutorRunnable(executor, "C")).start();
}

public class SyncThisInstanceMethodExecutorRunnable implements Runnable {
    private SyncThisInstanceMethodExecutor mExecutor;
    private String                         mThreadNmae;

    public SyncThisInstanceMethodExecutorRunnable(SyncThisInstanceMethodExecutor executor, String threadNmae) {
        mExecutor = executor;
        mThreadNmae = threadNmae;
    }

    @Override
    public void run(){
        mExecutor.execute(mThreadNmae);
    }

}

/**
 * インスタンスメソッドを synchronized にした場合のテストクラス
 */
public class SyncThisInstanceMethodExecutor {
    private int mCount = 0;

    public synchronized void execute(String threadName){
        if(mCount == N){
            LogUtils.d(threadName + ": do nothing");
        }else{
            for(int i = 0; i < N; i++){
                LogUtils.d(threadName + ": " + ++mCount);
            }
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
インスタンスメソッドの synchronized はメソッドボディを synchronized(this){ 〜 } で囲ったのと
同じ動きをするらしいので SyncThisInstanceMethodExecutor という名前にしました。
参考: Javaスレッドメモ(Hishidama's Java thread Memo)
コレもバッチリ A の実行が終わるのを待たされているのが実行ログから分かります。
[SynchronizedActivity#syncThisInstanceMethodExecute:219]
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 1
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 2
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 3
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 4
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 5
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 6
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 7
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 8
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 9
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 10
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:250]B: do nothing
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:250]C: do nothing

ロックオブジェクトを synchronized

/**
 * インスタンスメソッドでロックオブジェクトを synchronized した場合のテスト実行メソッド
 */
public void syncLockInstanceMethodExecute(){
    LogUtils.d();
    SyncLockInstanceMethodExecutor executor = new SyncLockInstanceMethodExecutor();
    new Thread(new SyncLockInstanceMethodExecutorRunnable(executor, "A")).start();
    new Thread(new SyncLockInstanceMethodExecutorRunnable(executor, "B")).start();
    new Thread(new SyncLockInstanceMethodExecutorRunnable(executor, "C")).start();
}

public class SyncLockInstanceMethodExecutorRunnable implements Runnable {
    private SyncLockInstanceMethodExecutor mExecutor;
    private String                         mThreadNmae;

    public SyncLockInstanceMethodExecutorRunnable(SyncLockInstanceMethodExecutor executor, String threadNmae) {
        mExecutor = executor;
        mThreadNmae = threadNmae;
    }

    @Override
    public void run(){
        mExecutor.execute(mThreadNmae);
    }

}

/**
 * インスタンスメソッドでロックオブジェクトを synchronized した場合のテストクラス
 */
public class SyncLockInstanceMethodExecutor {
    private final Object lock   = new Object();
    private int          mCount = 0;

    public void execute(String threadName){
        synchronized(lock){
            if(mCount == N){
                LogUtils.d(threadName + ": do nothing");
            }else{
                for(int i = 0; i < N; i++){
                    LogUtils.d(threadName + ": " + ++mCount);
                }
                try{
                    TimeUnit.SECONDS.sleep(1);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}
SyncLockInstanceMethodExecutor は SyncThisInstanceMethodExecutor とほとんど同じだけど
メソッド修飾の synchronized じゃなくて synchronized 文になってオブジェクトをロックしています。
SyncThis と同じなので実行ログは省略します。
StackOverflow でこっちの方法を推奨している回答がありました。
synchronization - How to synchronize or lock upon variables in Java? - Stack Overflow
blog.pasberth.com: Javaのsynchronizedってなにをブロックしているの?のコメントでもあるように
synchronized(this) は同一インスタンスでしか排他処理ができないので
SyncLockInstanceMethodExecutor を複数インスタンス生成されたら排他制御できなくなります。
なので以下のようにグローバルなオブジェクトを synchronized すると別インスタンスでも排他制御できます。
public static final Object LOCK = new Object();

public void syncGrobalLockExecute(){
    LogUtils.d();
    new Thread(new SyncGrobalLockExecutorRunnable(new SyncGrobalLockExecutor(), "A")).start();
    new Thread(new SyncGrobalLockExecutorRunnable(new SyncGrobalLockExecutor(), "B")).start();
    new Thread(new SyncGrobalLockExecutorRunnable(new SyncGrobalLockExecutor(), "C")).start();
}

public class SyncGrobalLockExecutorRunnable implements Runnable {
    private SyncGrobalLockExecutor mExecutor;
    private String                 mThreadNmae;

    public SyncGrobalLockExecutorRunnable(SyncGrobalLockExecutor executor, String threadNmae) {
        mExecutor = executor;
        mThreadNmae = threadNmae;
    }

    @Override
    public void run(){
        mExecutor.execute1(mThreadNmae);
        mExecutor.execute2(mThreadNmae);
        mExecutor.execute3(mThreadNmae);
    }

}

public class SyncGrobalLockExecutor {
    private int mCount = 0;

    public void execute1(String threadName){
        synchronized(LOCK){
            for(int i = 0; i < N; i++){
                LogUtils.d(threadName + "[1]: " + ++mCount);
            }
        }
    }

    public void execute2(String threadName){
        synchronized(LOCK){
            for(int i = 0; i < N; i++){
                LogUtils.d(threadName + "[2]: " + ++mCount);
            }
        }
    }

    public void execute3(String threadName){
        synchronized(LOCK){
            for(int i = 0; i < N; i++){
                LogUtils.d(threadName + "[3]: " + ++mCount);
            }
        }
    }
}
同じ LOCK に対する排他制御なので execute1, execute2, execute3 同士も排他制御されます。
インスタンスごとに呼ぶメソッドを変えてあげればわかりやすいです。
SyncGrobalLockExecutor はサンプルに入ってないので適当に書き換えて試してみて下さい。

synchronized を使いこなせればプログラミングの幅が広がりそうですね。
長くなったのでこのへんで。

タグ(RSS)