9.2 ロックとアンロック
9.2.1 干渉を防ぐ
ある共通の処理に対し並列にプログラムが動作すると、メソッドが複数のスレッドから同時に呼び出されたり、変数が同時に参照される危険性があります。これは、プログラムの整合性に大きな影響を与えてしまい、大きな問題となります。
それでも、もし、複数のスレッドから呼び出されると問題が発生するようなプログラムを構築しなければならない場合、常に 1 つのスレッドが実行している間は他のスレッドを待機させるような仕組みが必要になります。それは、メソッドの宣言時に synchronized 修飾子を指定することによって、複数のスレッドからメソッドを起動できないようにすることで解決できます。
スレッドの同期の仕組みを、Java ではロックとアンロックという形で実現しています。synchronized 修飾子が指定されているメソッドを実行する場合、スレッドはロックを保有する必要があります。ロックとは、オブジェクトごとに内部で数値的にカウントされている情報です。すなわち、ロックとはブロックの所有権のようなものだと考えてください。synchronized が指定されているコードを実行するには、ロックが必要なのです。
ロックを要求できるスレッドは 1 つだけです。スレッドがロックを取得すると、ロックと同じ回数だけアンロックされなければ他のスレッドがロックを取得することはできません。synchronized 修飾子を指定したメソッドを実行するときは、スレッドがロック(メソッドの所有権)を取得し、メソッドを終了すると同時にアンロックします。スレッドがロックを発生させられない場合、他のスレッドがメソッドを実行しています。この場合は、そのスレッドがアンロックするまで実行を待機することになります。
class Test extends Thread { public static void main(String args[]) { new Test("Sub Thread1").start(); new Test("Sub Thread2").start(); currentThread().setName("Main Thread"); syncMethod(); } public Test(String name) { super(name); } public void run() { System.out.println("Call SyncMethod : " + getName()); syncMethod(); } synchronized private static void syncMethod() { System.out.println("SyncMethod : " + currentThread().getName()); try { Thread.sleep(1000); } catch(InterruptedException err) { System.out.println(err); } } }
>java Test SyncMethod : Main Thread Call SyncMethod : Sub Thread1 Call SyncMethod : Sub Thread2 SyncMethod : Sub Thread1 SyncMethod : Sub Thread2
コード1の SyncMethod は synchronized 修飾子が指定されているクラス・メソッドです。このメソッドを実行するスレッドは、ロックを発生させなければなりません。つまり、このメソッドを複数のスレッドから同時に実行することはできません。
プログラムは、main() メソッドで新しいスレッドを 2 つ生成しています。それぞれのメソッドには Sub Thread1 と Sub Thread2 という名前を与え、さらに main() メソッドを実行しているメイン・スレッドには Main Thread という名前を指定しています。そして、それぞれのスレッドからほぼ同時に SyncMethod() を呼び出すように仕組んでいます。
SyncMethod() では、すぐにメソッドが終了しないように Thread.sleep() メソッドを用いてスレッドを待機させます。これによって、このメソッドを呼び出したスレッドは長い時間、ロックを占有することになります。後続してメソッドを起動したスレッドはロックを持たないため、先のメソッドが処理を終了する、すなわちアンロックするのを強制的に待たされることになるでしょう。
実行結果を見れば、最初にメイン・スレッドが SyncMethod() を呼び出しています。これに続いて即座にサブ・スレッドも run() が実行され、SyncMethod() を呼び出していますが、すぐに実行することはできず待たされてしまいます。これは、メイン・スレッドが SyncMethod() のロックを開放していないためです。
ロックはオブジェクトごとに存在するため、インスタンスが異なれば個別に判断されます。コード1でアンロックを待ったのは SyncMethod() メソッドがクラス・メソッドだったからです。インスタンスが異なるメソッド間では、スレッドの同期を行う必要はありません。
9.2.2 synchronized文
メソッドの宣言で synchronized 修飾子を指定することでメソッド全体を同期対象にすることができましたが、synchronized 文というものも存在します。synchronized 文は特定のブロックを実行するときにロックを取得し、ブロックから抜け出す時にアンロックする仕組みを提供します。
自分以外の誰かが作った便利なクラスが、必ずしもスレッドセーフに作られているとは限りません。もし、マルチスレッドの環境でメソッドを同時に呼び出せば問題が発生するとしても、自分以外が作ったクラス・ライブラリのソースを操作することはできません。このような場合は、オブジェクトに対してロックをかけることができる synchronized 文を使うことができるのです。こうすれば、何らかのインスタンスのメソッドの呼び出しを特定のブロック内で同期させることができます。synchronized 文は次のように記述します。
synchronized(式文) ブロック
式文の結果は必ず参照型でなければなりません。参照型が null の場合は例外が発生します。synchronized 文のブロックを実行するには、スレッドが指定された参照型からロックを取得しなければなりません。ブロックを実行している間、指定した参照型はロックされているため、他のスレッドは同じオブジェクトで synchronized 文を実行することはできません。ブロックを抜け出した時点で、自動的にアンロックされます。
class SyncTest { public void show() { System.out.println("Show : " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch(InterruptedException err) { System.out.println(err); } } } class Test extends Thread { private static SyncTest obj = new SyncTest(); public static void main(String args[]) { new Test("Sub Thread1").start(); new Test("Sub Thread2").start(); new Test("Sub Thread3").start(); } public Test(String name) { super(name); } public void run() { System.out.println("Call : " + getName()); synchronized(obj) { obj.show(); } } }
>java Test Call : Sub Thread1 Show : Sub Thread1 Call : Sub Thread2 Call : Sub Thread3 Show : Sub Thread2 Show : Sub Thread3
コード2の run() メソッドでは、SyncTest クラスの show() メソッドを呼び出すために同期制御を行っています。synchronized 文を用いてすでに確保しているクラス変数 obj をロックしています。show() メソッドを呼び出している間、このオブジェクトをスレッドによってロックされているため、他のスレッドが同じオブジェクトを用いて synchronized を実行することはできません。そのため、Sub Thread2 と Sub Thread3 は直前のスレッドがブロックを抜け出すまで待たされています。