スレッドセーフでないコードを書くには
昨日書いたソースをいじっていたら偶然にもスレッドセーフでない(簡単な)ソースコードが生まれたので記録しておく。ネットでスレッドセーフって調べると上級者向けのソースコードばかりヒットするからこういう簡単なのがあってもいいよね。。。
あるコードがスレッドセーフであるという場合、そのコードを複数のスレッドが同時並行的に実行しても問題が発生しないことを意味する。 特に、ある共有データへの複数のスレッドによるアクセスがあるとき、一度に1つのスレッドのみがその共有データにアクセスするようにして安全性を確保しなければならない。
スレッドセーフ - Wikipedia
複数のスレッドを立てる処理を行う場合、意図しない順序でスレッドが動くことがある。生成された各スレッドは独立して動くので順序関係とか守ってくれないのだ。そのため値の加算や代入が処理に絡む場合、値や出力がおかしくなることがある。これがスレッドセーフでないプログラムである。実際にスレッドセーフでないソースコードを書いてみた。
public class Main { public static void main(String[] args){ Thread[] th = new Thread[100]; for(int i=0;i<100;i++){ th[i] = new Thread1(); } for(Thread t:th){ t.start(); } } } class Thread1 extends Thread{ static int cnt=1; public void run(){ System.out.println("Thread "+cnt+" moved."); cnt++; } }
ポイントはThread1クラスの変数であるcnt。staticな変数であるため、Thread1クラスのインスタンスがstart()する度にcnt++が実行された値が増えていくというわけ。通常通りに動けば出力される値も1ずつ変わっていく。
このソースの処理は通常なら以下のように進むはず。
(1) Thread型配列thを定義。要素数=100。
(2) for文で 100回Thread1クラスのインスタンスをnew。
(3) Thread1クラスのインスタンスをth[i] に格納。
(4) 拡張for文で配列thに含まれる100個のインスタンスでstart()メソッドを実行。
(5) "Thread 1 moved." "Thread 2 moved." ~"Thread 100 moved." がコンソールに出力される(はず)。
実行結果(7行目まで抜粋) Thread 1 moved. Thread 1 moved. Thread 1 moved. Thread 2 moved. Thread 5 moved. Thread 5 moved. Thread 5 moved. (以下省略)
異常が発生する。"Thread 1 moved." の次の出力は "Thread 2 moved." になるべきなのに"Thread 1 moved." が続いている。おまけに"Thread 2 moved." の次は "Thread 5 moved." になっている。スレッドセーフでないコードによって出力に異常が起きたことが確認できる。
ソースコードの何が異常の原因なんだろう?100回インスタンス化されたThread1クラスを見ると原因が分かる。
class Thread1 extends Thread{ static int cnt=1; public void run(){ System.out.println("Thread "+cnt+" moved."); cnt++; } }
このクラスのインスタンスを実行するとrun()の中の処理が(1)⇒(2)の順で実行される。
(1) System.out.println("Thread "+cnt+" moved.");
(2) cnt++
複数スレッドがある場合、スレッドが順次動けば処理は次のようになる。
Thread1(1) (1) System.out.println("Thread 1 moved."); (2) cnt++ Thread1(2) (1) System.out.println("Thread 2 moved."); (2) cnt++ Thread1(3) (1) System.out.println("Thread 3 moved."); (2) cnt++ Thread1(4) (1) System.out.println("Thread 4 moved."); (2) cnt++ ......(以下略)
しかし実際には複数のスレッドは独立に動くので処理は次のようになった。
出力⇒cnt++の順序が崩れてしまっている。
Thread1(1) (1) System.out.println("Thread 1 moved."); Thread1(2) (1) System.out.println("Thread 1 moved."); Thread1(3) (1) System.out.println("Thread 1 moved."); Thread1(4) (1) System.out.println("Thread 2 moved."); (2) cnt++ (2) cnt++ (2) cnt++ Thread1(5) (1) System.out.println("Thread 5 moved."); ......(以下略)
このようにスレッド内に複数の処理がある場合、その順序が守られないことがある。だから、スレッド内に複数の処理を書いて、かつ値の加減・変更・代入などの処理が含まれる場合、スレッドセーフでないコードを書くことが出来る。気を付けよう!
余談。
ちなみにもう一回同じソースを実行すると、出力値が変わる。複数のスレッドの動きは毎回変わるんだね~。これ異常が発見できない動き方をすることもあるんだから怖いね。
実行結果(7行目まで抜粋) Thread 1 moved. Thread 1 moved. Thread 1 moved. Thread 1 moved. Thread 1 moved. Thread 4 moved. Thread 6 moved. (以下省略)