スレッドの排他


なんか、スレッドセーフかどうかのテストをしたいなーと思ったら、スレッドってあんまりやったことなかったので、けっこうてこずりました。

ということで、synchronizedの基本的なことまとめ。


以下のクラスを実行してみます。

public class Test001 {
  
  public static void main(String[] args) {
    new Test001().exe();
  }
  
  private void exe() {
    MyThread1 thread1 = new MyThread1();
    MyThread2 thread2 = new MyThread2();
    thread1.start();
    thread2.start();
  }
  
  // スレッド1のクラス
  class MyThread1 extends Thread {
    public void run() {
      Target.test1("thread1 : ");
    }
  }
  
  // スレッド2のクラス
  class MyThread2 extends Thread {
    public void run() {
      Target.test2("thread2 : ");
    }
  }
  
  // 複数スレッドから呼び出されるクラス
  static class Target {

    static int cnt;

    public static void test1(String name) {
      for (int i = 0; i < 500; i++) {
        System.out.println(name + cnt++);
      }
    }
    
    public static void test2(String name) {
      for (int i = 0; i < 500; i++) {
        System.out.println(name + cnt++);
      }
    }
  }
}

Targetクラスのtest1メソッド、test2メソッドが2つのスレッドに呼び出されあって以下のように出力されました。Targetのcntフィールドのカウントアップがぐっちゃりです。

thread2 : 0
thread1 : 0
thread1 : 2
thread2 : 1
thread2 : 3
thread2 : 5
thread2 : 6
thread2 : 7
・
・
・
thread2 : 997
thread2 : 998
thread2 : 999


そこで、Targetクラスのtest1メソッド、test2メソッドは1度呼び出されたらメソッドの処理が終わるまで、他のスレッドは実行させないようにしましょう。

例えばこんな感じ。(メソッドにsynchronized)

  // 複数スレッドから呼び出されるクラス
  static class Target {
    
    static int cnt;
    
    public static synchronized void test1(String name) {
      for (int i = 0; i < 500; i++) {
        System.out.println(name + cnt++);
      }
    }
    
    public static synchronized void test2(String name) {
      for (int i = 0; i < 500; i++) {
        System.out.println(name + cnt++);
      }
    }
  }


こんなんでもいい。(処理をsynchronizedブロックにする)
※lockの代わりにthis.classでもいいんだけど、次の話のネタふりとして。

  // 複数スレッドから呼び出されるクラス
  static class Target {
    
    static int cnt;
    static Object lock = new Object();
    
    public static void test1(String name) {
      synchronized (lock) {
        for (int i = 0; i < 500; i++) {
          System.out.println(name + cnt++);
        }
      }
    }
    
    public static void test2(String name) {
      synchronized (lock) {
        for (int i = 0; i < 500; i++) {
          System.out.println(name + cnt++);
        }
      }
    }

実行してみると。

thread2 : 0
thread2 : 1
thread2 : 2
thread2 : 3
thread2 : 4
・
・
・
thread2 : 498
thread2 : 499
thread1 : 500
thread1 : 501
・
・
・
thread1 : 995
thread1 : 996
thread1 : 997
thread1 : 998
thread1 : 999

きっちりtest2が500回の処理を終えた後にtest1が動作してますね。


でも、こんなんじゃダメ。

  // 複数スレッドから呼び出されるクラス
  static class Target {
    
    static int cnt;
    static Object lock1 = new Object();
    static Object lock2 = new Object();
    
    public static  void test1(String name) {
      synchronized (lock1) {
        for (int i = 0; i < 500; i++) {
          System.out.println(name + cnt++);
        }
      }
    }
    
    public static  void test2(String name) {
      synchronized (lock2) {
        for (int i = 0; i < 500; i++) {
          System.out.println(name + cnt++);
        }
      }
    }
  }

1つのロックオブジェクトで制御しないと、結果は最初と同じようになりました。

thread1 : 0
thread1 : 2
thread1 : 3
thread2 : 1
thread2 : 5
thread2 : 6

ということで、ロックオブジェクトによって、複数スレッドで呼び出されたくないメソッド組み合わせをコントロールできるわけですね。


スレッドセーフじゃないクラスを使うときはメソッド内で毎回newしましょうとよく言いますが、1回だけnewしてsynchronizedで排他をかけながら処理したほうが2倍速かったことがありました。
処理速度を重んじる場合は、かなり検討の余地ありです。ネットで調べるとsynchronizedは遅いで有名だけど、newより速い場合も多いのではないでしょうか。対象クラスによると思いますが。