JavaRush /Java Blog /Random-JA /Java のデッドロックとそれに対処する方法
articles
レベル 15

Java のデッドロックとそれに対処する方法

Random-JA グループに公開済み
マルチスレッド アプリケーションを開発する場合、アプリケーションの信頼性とパフォーマンスの方が重要であるというジレンマがよく発生します。たとえば、スレッドの安全性を確保するために同期を使用しますが、同期の順序が正しくない場合、デッドロックが発生する可能性があります。また、リソース消費を制限するためにスレッド プールとセマフォも使用しますが、この設計でエラーが発生すると、リソース不足によるデッドロックが発生する可能性があります。この記事では、デッドロックやアプリケーションのパフォーマンスに関するその他の問題を回避する方法について説明します。また、デッドロックが発生した場合に回復できるようにアプリケーションを作成する方法についても見ていきます。 Java のデッドロックとそれに対処する方法 - 1デッドロックとは、あるリソースを占有している 2 つ以上のプロセスが、他のプロセスが占有している他のリソースを取得しようとしているが、どのプロセスも必要なリソースを占有することができず、それに応じて占有されているリソースを解放する状況です。この定義は一般的すぎるため、理解するのが困難です。よりよく理解するために、例を使用してデッドロックの種類を見ていきます。

同期順序の相互ロック

次のタスクを考えてみましょう。ある口座から別の口座に一定の金額を送金するトランザクションを実行するメソッドを作成する必要があります。解決策は次のようになります。
public void transferMoney(Account fromAccount, Account toAccount, Amount amount) throws InsufficientFundsException {
	synchronized (fromAccount) {
		synchronized (toAccount) {
			if (fromAccount.getBalance().compareTo(amount) < 0)
				throw new InsufficientFundsException();
			else {
				fromAccount.debit(amount);
				toAccount.credit(amount);
			}
		}
	}
}
一見すると、このコードはごく普通に同期されており、ソース アカウントの状態を確認して変更し、宛先アカウントを変更するというアトミックな操作が行われています。ただし、この同期戦略では、デッドロック状況が発生する可能性があります。これがどのように起こるか例を見てみましょう。アカウント A からアカウント B に x お金を移動する、およびアカウント B からアカウント A に y お金を移動する 2 つの取引を行う必要があります。多くの場合、この状況によってデッドロックが発生することはありませんが、不運な一連の状況では、トランザクション 1 がアカウント モニター A を占有し、トランザクション 2 がアカウント モニター B を占有することになります。その結果、デッドロックが発生します。トランザクション 1 は、トランザクション 2 がアカウント モニターを解放するのを待ちます。 B ですが、トランザクション 2 は、トランザクション 1 が占有しているモニター A にアクセスする必要があります。デッドロックに関する大きな問題の 1 つは、テストでデッドロックを見つけるのが簡単ではないことです。この例で説明されている状況でも、スレッドがブロックしない可能性があります。つまり、この状況が常に再現されるわけではないため、診断が大幅に複雑になります。一般に、ここで説明した非決定性の問題はマルチスレッドに典型的なものです (ただし、これが簡単になるわけではありません)。したがって、コード レビューは、テスト中に再現するのが難しいエラーを特定できるため、マルチスレッド アプリケーションの品質を向上させる上で重要な役割を果たします。もちろん、これはアプリケーションをテストする必要がないという意味ではなく、コード レビューを忘れてはいけないというだけです。このコードがデッドロックを引き起こさないようにするにはどうすればよいですか? このブロックは、アカウントの同期が異なる順序で発生する可能性があるという事実によって発生します。したがって、アカウントに何らかの順序を導入すると (これは、アカウント A がアカウント B より小さいと言えるようにする何らかのルールです)、問題は解決されます。どうやってするの?まず、口座が数値、小文字、または自然な順序の概念を持つ何らかの固有の識別子 (口座番号など) を持っている場合 (文字列は辞書順で比較できる)、幸運であると考えることができます。常に最初に小さいアカウントのモニターを占有し、次に大きいアカウントのモニターを占有することができます (またはその逆)。
private void doTransfer(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	if (fromAcct.getBalance().compareTo(amount) < 0)
		throw new InsufficientFundsException();
	else {
		fromAcct.debit(amount);
		toAcct.credit(amount);
	}
}
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	int fromId= fromAcct.getId();
	int toId = fromAcct.getId();
	if (fromId < toId) {
		synchronized (fromAcct) {
			synchronized (toAcct) {
				doTransfer(fromAcct, toAcct, amount)}
			}
		}
	} else  {
		synchronized (toAcct) {
			synchronized (fromAcct) {
				doTransfer(fromAcct, toAcct, amount)}
			}
		}
	}
}
2 番目のオプションでは、そのような識別子がない場合は、自分でそれを考え出す必要があります。第一近似的には、ハッシュ コードによってオブジェクトを比較できます。おそらくそれらは異なるでしょう。しかし、それらが同じであることが判明したらどうなるでしょうか? 次に、同期用に別のオブジェクトを追加する必要があります。少し洗練されているように見えるかもしれませんが、何ができるでしょうか?さらに、3 番目のオブジェクトはめったに使用されません。結果は次のようになります。
private static final Object tieLock = new Object();
private void doTransfer(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	if (fromAcct.getBalance().compareTo(amount) < 0)
		throw new InsufficientFundsException();
	else {
		fromAcct.debit(amount);
		toAcct.credit(amount);
	}
}
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	int fromHash = System.identityHashCode(fromAcct);
	int toHash = System.identityHashCode(toAcct);
	if (fromHash < toHash) {
		synchronized (fromAcct) {
			synchronized (toAcct) {
				doTransfer(fromAcct, toAcct, amount);
			}
		}
	} else if (fromHash > toHash) {
		synchronized (toAcct) {
			synchronized (fromAcct) {
				doTransfer(fromAcct, toAcct, amount);
			}
		}
	} else {
		synchronized (tieLock) {
			synchronized (fromAcct) {
				synchronized (toAcct) {
					doTransfer(fromAcct, toAcct, amount)
				}
			}
		}
	}
}

オブジェクト間のデッドロック

説明されているブロック状態は、診断するのが最も簡単なデッドロックのケースを表しています。マルチスレッド アプリケーションでは、異なるオブジェクトが同じ同期ブロックにアクセスしようとすることがよくあります。これによりデッドロックが発生する可能性があります。次の例を考えてみましょう: フライト ディスパッチャ アプリケーション。飛行機は目的地に到着したことを管制官に伝え、着陸の許可を求めます。管制官は、自分の方向に飛行する航空機に関するすべての情報を保存し、地図上にその位置をプロットできます。
class Plane {
	private Point location, destination;
	private final Dispatcher dispatcher;

	public Plane(Dispatcher dispatcher) {
		this.dispatcher = dispatcher;
	}
	public synchronized Point getLocation() {
		return location;
	}
	public synchronized void setLocation(Point location) {
		this.location = location;
		if (location.equals(destination))
		dispatcher.requestLanding(this);
	}
}

class Dispatcher {
	private final Set<Plane> planes;
	private final Set<Plane> planesPendingLanding;

	public Dispatcher() {
		planes = new HashSet<Plane>();
		planesPendingLanding = new HashSet<Plane>();
	}
	public synchronized void requestLanding(Plane plane) {
		planesPendingLanding.add(plane);
	}
	public synchronized Image getMap() {
		Image image = new Image();
		for (Plane plane : planes)
			image.drawMarker(plane.getLocation());
		return image;
	}
}
このコードにはデッドロックを引き起こす可能性のあるバグがあることを理解するのは、前のコードよりも困難です。一見すると再同期はありませんが、そうではありません。おそらくすでにお気づきかと思いますが、setLocationクラスPlanegetMapクラスメソッドはDispatcher同期されており、内部で他のクラスの同期されたメソッドを呼び出します。これは一般的に悪い習慣です。これを修正する方法については、次のセクションで説明します。その結果、飛行機がその場所に到着した場合、誰かがカードを取得しようと決めた瞬間にデッドロックが発生する可能性があります。つまり、 メソッドgetMapとメソッドが呼び出され、それぞれsetLocationインスタンス モニターDispatcherと を占有しますPlane。次に、このメソッドは(特に現在ビジー状態のインスタンス上で)getMapを呼び出し、各インスタンスのモニターが空くまで待機します。同時に、メソッドが呼び出されますが、インスタンス モニターはマップの描画で忙しいままです。結果は行き詰まりです。 plane.getLocationPlanePlanesetLocationdispatcher.requestLandingDispatcher

オープンコール

前のセクションで説明したような状況を回避するには、他のオブジェクトのメソッドへのパブリック呼び出しを使用することをお勧めします。つまり、同期ブロックの外側にある他のオブジェクトのメソッドを呼び出します。オープンコールの原則を使用してメソッドを書き換えるとsetLocationgetMapデッドロックの可能性が排除されます。たとえば、次のようになります。
public void setLocation(Point location) {
	boolean reachedDestination;
	synchronized(this){
		this.location = location;
		reachedDestination = location.equals(destination);
	}
	if (reachedDestination)
		dispatcher.requestLanding(this);
}
………………………………………………………………………………
public Image getMap() {
	Set<Plane> copy;
	synchronized(this){
		copy = new HashSet<Plane>( planes);
	}
	Image image = new Image();
	for (Plane plane : copy)
		image.drawMarker(plane.getLocation());
	return image;
}

リソースのデッドロック

デッドロックは、一度に 1 つのスレッドのみが使用できるリソースにアクセスしようとしたときにも発生する可能性があります。例としては、データベース接続プールがあります。一部のスレッドが 2 つの接続に同時にアクセスする必要があり、それらの接続に異なる順序でアクセスすると、デッドロックが発生する可能性があります。基本的に、この種のロックは、コードを実行しようとしたときではなく、リソースにアクセスしようとしたときに発生する点を除けば、同期順序ロックと変わりません。

デッドロックを回避するにはどうすればよいですか?

もちろん、コードがエラーなしで記述されている場合 (例は前のセクションで説明しました)、コード内でデッドロックは発生しません。しかし、彼のコードがエラーなく書かれていることを誰が保証できるでしょうか? もちろん、テストはエラーの重要な部分を特定するのに役立ちますが、前に見たように、マルチスレッド コードのエラーを診断するのは簡単ではなく、テスト後であってもデッドロック状況がないことを確信することはできません。どうにかしてブロックから身を守ることはできないでしょうか?答えは「はい」です。同様の手法がデータベース エンジンでも使用されており、多くの場合、デッドロック (データベース内のトランザクション メカニズムに関連する) から回復する必要があります。Lockパッケージで利用可能なインターフェイスとその実装によりjava.util.concurrent.locks、メソッドを使用して、このクラスのインスタンスに関連付けられたモニターの占有を試行できますtryLock(モニターを占有することができた場合は true を返します)。インターフェイスを実装するオブジェクトのペアがありLock、相互のブロックを回避する方法でそれらのモニターを占有する必要があるとします。次のように実装できます。
public void twoLocks(Lock A,  Lock B){
	while(true){
		if(A.tryLock()){
			if(B.tryLock())
			{
				try{
					//do something
				} finally{
					B.unlock();
					A.unlock();
				}
			} else{
				A.unlock();
			}
		}
	}
}
このプログラムでわかるように、相互にブロックされる可能性を排除しながら、2 つのモニターを占有します。try- finallyパッケージ内のクラスはモニターを自動的に解放しないため、ブロックが必要であることに注意してくださいjava.util.concurrent.locks。タスクの実行中に何らかの例外が発生すると、モニターはロック状態のままになります。デッドロックを診断するにはどうすればよいですか? JVM を使用すると、デッドロックをスレッド ダンプに表示することで診断できます。このようなダンプには、スレッドがどのような状態にあるのかに関する情報が含まれています。ブロックされている場合、ダンプにはスレッドが解放を待っているモニターに関する情報が含まれます。スレッドをダンプする前に、JVM は待機中の (ビジーな) モニターのグラフを調べ、サイクルを見つけた場合は、参加しているモニターとスレッドを示すデッドロック情報を追加します。デッドロックしたスレッドのダンプは次のようになります。
Found one Java-level deadlock:
=============================
"ApplicationServerThread":
waiting to lock monitor 0x0f0d80cc (a MyDBConnection),
which is held by "ApplicationServerThread"
"ApplicationServerThread":
waiting to lock monitor 0x0f0d8fed (a MyDBCallableStatement),
which is held by "ApplicationServerThread"
Java stack information for the threads listed above:
"ApplicationServerThread":
at MyDBConnection.remove_statement
- waiting to lock <0x6f50f730> (a MyDBConnection)
at MyDBStatement.close
- locked <0x604ffbb0> (a MyDBCallableStatement)
...
"ApplicationServerThread":
at MyDBCallableStatement.sendBatch
- waiting to lock <0x604ffbb0> (a MyDBCallableStatement)
at MyDBConnection.commit
- locked <0x6f50f730> (a MyDBConnection)
上記のダンプは、データベースを操作している 2 つのスレッドが相互にブロックしていることを明確に示しています。この JVM 機能を使用してデッドロックを診断するには、プログラム内のさまざまな場所でスレッド ダンプ操作の呼び出しを配置し​​、アプリケーションをテストする必要があります。次に、結果として得られたログを分析する必要があります。デッドロックが発生したことが示されている場合、ダンプからの情報はデッドロックが発生した条件を検出するのに役立ちます。一般に、デッドロックの例のような状況は避けるべきです。このような場合、アプリケーションは安定して動作する可能性が高くなります。ただし、テストとコードレビューを忘れないでください。これは、問題が発生した場合に問題を特定するのに役立ちます。デッドロック フィールドの回復が重要なシステムを開発している場合は、「デッドロックを回避する方法」セクションで説明されている方法を使用できます。この場合、からのlockInterruptiblyインターフェイスメソッド。このメソッドを使用すると、モニターを占有しているスレッドを中断することができます (したがって、モニターを解放できます)。 Lockjava.util.concurrent.locks
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION