Java スレッドの同期
2 つ以上のスレッドがリソースを共有する必要がある場合、特定の時点でリソースが 1 つのスレッドのみによって占有されていることを確認する何らかの方法が必要です。これを実現するプロセスは同期と呼ばれます。ご覧のとおり、Java はこれに対して独自の言語レベルのサポートを提供します。
同期の鍵となるのは、モニター (セマフォとも呼ばれます) の概念です。モニターは、相互に排他的にロックされたオブジェクト、つまりミューテックスです。一度にモニターを取得できるスレッドは 1 つだけです。スレッドがロックを必要とする場合、スレッドはモニターに入らなければなりません。ロックされたチューブに入ろうとする他のすべてのスレッドは、最初のスレッドがチューブから出るまでハングする必要があります。これらの他のスレッドは待機モニターと呼ばれます。モニターを所有するスレッドは、必要に応じて同じモニターに再度入ることができます。
C や C++ などの他の言語で同期を使用したことがある場合は、その使用方法が少し奇妙であることがわかるでしょう。これは、多くの言語自体が同期をサポートしていないためです。対照的に、同期スレッドの場合、プログラムはオペレーティング システムのソース言語を使用する必要があります。幸いなことに、Java は言語要素を通じて同期を実装しているため、同期に関連する複雑さのほとんどは解消されています。
コードは 2 つの方法で同期できます。どちらの方法にも synchronized キーワードの使用が含まれます。以下で 2 つの方法について説明します。
同期メソッドの使用
Java での同期は、すべてのオブジェクトに対応する暗黙的なモニターがあるため簡単です。オブジェクトのモニターに入るには、synchronized キーワードで変更されたメソッドを呼び出します。スレッドが同期メソッド内にある場合、そのメソッド (または他の同期メソッド) を呼び出そうとする同じインスタンスの他のすべてのスレッドは待機する必要があります。モニターを終了し、待機中の他のスレッドにオブジェクトの制御を放棄するには、モニターを所有するスレッドが同期メソッドから戻るだけです。
同期の必要性を理解するために、同期を使用すべきであるにもかかわらず使用されていない簡単な例から始めましょう。次のプログラムには 3 つの単純なクラスがあります。 1 つ目は Callme で、単純なメソッド call() があります。 call() メソッドには msg という名前の文字列パラメータがあります。このメソッドは、角かっこ内のメッセージ文字列を出力しようとします。興味深いのは、左括弧とメッセージ文字列を出力するために call() を呼び出した後、Thread.sleep(1000) が呼び出され、現在のスレッドが 1 秒間一時停止されることです。
次のクラス Caller のコンストラクターは、それぞれ target と msg に格納されている Callme と String のインスタンスを参照します。コンストラクターは、オブジェクトの run() メソッドを呼び出す新しいスレッドも作成します。スレッドはすぐに開始されます。 Caller クラスの run() メソッドは、パラメータ msg 文字列を介して Callme インスタンス ターゲットの call() メソッドを呼び出します。最後に、Synch クラスは、Callme の単純なインスタンスと、異なるメッセージ文字列を持つ Caller の 3 つのインスタンスを作成することから始めます。
Callme の同じインスタンスが各 Caller インスタンスに渡されます。
// This program is not synchronized. class Callme { void call(String msg) { System.out.print("[" + msg); try { Thread.sleep(1000); } catch(InterruptedException e) { System.out.println("Interrupted"); } System.out.println("]"); } } class Caller implements Runnable { String msg; Callme target; Thread t; public Caller(Callme targ, String s) { target = targ; msg = s; t = new Thread(this); t.start(); } public void run() { target.call(msg); } } class Synch { public static void main(String args[]) { Callme target = new Callme(); Caller ob1 = new Caller(target, "Hello"); Caller ob2 = new Caller(target, "Synchronized"); Caller ob3 = new Caller(target, "World"); // wait for threads to end try { ob1.t.join(); ob2.t.join(); ob3.t.join(); } catch(InterruptedException e) { System.out.println("Interrupted"); } } }
このプログラムの出力は次のとおりです:
Hello[Synchronized[World] ] ]
この場合、call() メソッドは sleep() を呼び出すことで実行を別のスレッドに移行できます。結果は、3 つのメッセージ文字列が混合された出力になります。このプログラムには、3 つのスレッドが同じオブジェクトの同じメソッドを同時に呼び出すことを防ぐメソッドはありません。 3 つのスレッドがメソッドを完了するために競合するため、これは競合状態です。この例では、sleep() を使用して、この効果を再現可能かつ明確にしています。ほとんどの場合、コンテキストの切り替えがいつ発生するかわからないため、競合はより複雑で予測不可能です。これにより、プログラムが正常に実行される場合もあれば、失敗する場合もあります。
上記の例の目的を達成するには、call() を継続的に使用する権限が必要です。つまり、ある時点で、それを支配できるスレッドを 1 つだけに制限する必要があります。これを行うには、次のように、call() 定義の前にキーワード synchronized を追加するだけです:
class Callme { synchronized void call(String msg) { ...
これにより、1 つのスレッドが call() を使用するときに、他のスレッドが call() に入ることを防ぎます。 call() の前に synchronized を追加すると、プログラム出力は次のようになります:
[Hello] [Synchronized] [World]
マルチスレッド状況では常に、オブジェクトの内部状態を操作するための 1 つまたは複数のメソッドがあります。同期キーワード「状態競合の防止」を使用する必要があります。スレッドがインスタンスの同期メソッドに入ると、他のスレッドは同じインスタンスの同期メソッドに入ることができないことに注意してください。ただし、インスタンスの他の非同期メソッドは引き続き呼び出すことができます。
同期されたステートメント
作成されたクラス内に同期されたメソッドを作成することは、同期を取得するための簡単で効果的な方法ですが、常に機能するとは限りません。その理由を考えてみてください。マルチスレッド アクセス用に設計されていないクラス オブジェクトへの同期アクセスを取得するとします。つまり、クラスは同期メソッドを使用しません。また、クラスは自分ではなく第三者によって作成されたものであり、そのソースコードを入手することはできません。この方法では、関連するメソッドの前に synchronized モディファイアを追加することはできません。このクラスのオブジェクトはどのように同期できるのでしょうか?幸いなことに、解決策は簡単です。このクラスによって定義されたメソッドの呼び出しを同期ブロック内に置くだけです。
以下は同期ステートメントの一般的な形式です:
synchronized(object) { // statements to be synchronized }
其中,object是被同步对象的引用。如果你想要同步的只是一个语句,那么不需要花括号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。
下面是前面程序的修改版本,在run( )方法内用了同步块:
// This program uses a synchronized block. class Callme { void call(String msg) { System.out.print("[" + msg); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Interrupted"); } System.out.println("]"); } } class Caller implements Runnable { String msg; Callme target; Thread t; public Caller(Callme targ, String s) { target = targ; msg = s; t = new Thread(this); t.start(); } // synchronize calls to call() public void run() { synchronized(target) { // synchronized block target.call(msg); } } } class Synch1 { public static void main(String args[]) { Callme target = new Callme(); Caller ob1 = new Caller(target, "Hello"); Caller ob2 = new Caller(target, "Synchronized"); Caller ob3 = new Caller(target, "World"); // wait for threads to end try { ob1.t.join(); ob2.t.join(); ob3.t.join(); } catch(InterruptedException e) { System.out.println("Interrupted"); } } }
这里,call( )方法没有被synchronized修饰。而synchronized是在Caller类的run( )方法中声明的。这可以得到上例中同样正确的结果,因为每个线程运行前都等待先前的一个线程结束。
Java线程间通信
多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序。线程还有第二优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条件成立,就要采取适当的行动。这浪费了CPU时间。举例来说,考虑经典的序列问题,当一个线程正在产生数据而另一个程序正在消费它。为使问题变得更有趣,假设数据产生器必须等待消费者完成工作才能产生新的数据。在轮询系统,消费者在等待生产者产生数据时会浪费很多CPU周期。一旦生产者完成工作,它将启动轮询,浪费更多的CPU时间等待消费者的工作结束,如此下去。很明显,这种情形不受欢迎。
为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:
wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。
notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。
这些方法在Object中被声明,如下所示:
final void wait( ) throws InterruptedException final void notify( ) final void notifyAll( )
wait( )存在的另外的形式允许你定义等待时间。
下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,设法获得同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;以及PC,创建单个Q,Producer,和Consumer的小类。
// An incorrect implementation of a producer and consumer. class Q { int n; synchronized int get() { System.out.println("Got: " + n); return n; } synchronized void put(int n) { this.n = n; System.out.println("Put: " + n); } } class Producer implements Runnable { Q q; Producer(Q q) { this.q = q; new Thread(this, "Producer").start(); } public void run() { int i = 0; while(true) { q.put(i++); } } } class Consumer implements Runnable { Q q; Consumer(Q q) { this.q = q; new Thread(this, "Consumer").start(); } public void run() { while(true) { q.get(); } } } class PC { public static void main(String args[]) { Q q = new Q(); new Producer(q); new Consumer(q); System.out.println("Press Control-C to stop."); } }
尽管Q类中的put( )和get( )方法是同步的,没有东西阻止生产者超越消费者,也没有东西阻止消费者消费同样的序列两次。这样,你就得到下面的错误输出(输出将随处理器速度和装载的任务而改变):
Put: 1 Got: 1 Got: 1 Got: 1 Got: 1 Got: 1 Put: 2 Put: 3 Put: 4 Put: 5 Put: 6 Put: 7 Got: 7
生产者生成1后,消费者依次获得同样的1五次。生产者在继续生成2到7,消费者没有机会获得它们。
用Java正确的编写该程序是用wait( )和notify( )来对两个方向进行标志,如下所示:
// A correct implementation of a producer and consumer. class Q { int n; boolean valueSet = false; synchronized int get() { if(!valueSet) try { wait(); } catch(InterruptedException e) { System.out.println("InterruptedException caught"); } System.out.println("Got: " + n); valueSet = false; notify(); return n; } synchronized void put(int n) { if(valueSet) try { wait(); } catch(InterruptedException e) { System.out.println("InterruptedException caught"); } this.n = n; valueSet = true; System.out.println("Put: " + n); notify(); } } class Producer implements Runnable { Q q; Producer(Q q) { this.q = q; new Thread(this, "Producer").start(); } public void run() { int i = 0; while(true) { q.put(i++); } } } class Consumer implements Runnable { Q q; Consumer(Q q) { this.q = q; new Thread(this, "Consumer").start(); } public void run() { while(true) { q.get(); } } } class PCFixed { public static void main(String args[]) { Q q = new Q(); new Producer(q); new Consumer(q); System.out.println("Press Control-C to stop."); } }
内部get( ), wait( )被调用。这使执行挂起直到Producer 告知数据已经预备好。这时,内部get( ) 被恢复执行。获取数据后,get( )调用notify( )。这告诉Producer可以向序列中输入更多数据。在put( )内,wait( )挂起执行直到Consumer取走了序列中的项目。当执行再继续,下一个数据项目被放入序列,notify( )被调用,这通知Consumer它应该移走该数据。
下面是该程序的输出,它清楚的显示了同步行为:
Put: 1 Got: 1 Put: 2 Got: 2 Put: 3 Got: 3 Put: 4 Got: 4 Put: 5 Got: 5
更多深入解析Java的线程同步以及线程间通信相关文章请关注PHP中文网!