スレッドの排他
なんか、スレッドセーフかどうかのテストをしたいなーと思ったら、スレッドってあんまりやったことなかったので、けっこうてこずりました。
ということで、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より速い場合も多いのではないでしょうか。対象クラスによると思いますが。