何をするために
、デッドロックがその後、悪い結果が実際に引き起こされた、オンライン環境で発生した場合、デッドロックがオンラインに発生した場合に最適な時期デッドロックを解決する「前に問題を防ぐためであることが起こる」後でそれを解決するのではなく、。火災が発生したときと同じように、一度大規模な火災が発生すると、損失を出さずに消火することはほぼ不可能です。デッドロックは同じです。オンラインでデッドロックが発生した場合、損失をできるだけ早く減らすために、JVM情報やログなどの「危機的状況」のデータを保存し、すぐにサービスを再起動するのが最善の方法です。デッドロックの修復を試みます。サービスを再起動することでこの問題を解決できると言われているのはなぜですか?多くの場合、デッドロックの前提条件が多く、同時実行性が十分に高い場合、デッドロックが再び発生する可能性があるため、再起動直後にデッドロックが再び発生する可能性はそれほど高くありません。サーバーを再起動すると、一時的に可用性を確保できます。次に、保存した犯罪現場の情報を使用して、デッドロックのトラブルシューティングを行い、コードを変更して、最後に再公開します。
一般的な修復戦略
一般的なデッドロック修復戦略にはどのようなものがありますか?以下に、3つの主要な修復戦略を紹介します。
- 回避戦略
- 検出と回復の戦略
- ダチョウ戦略
彼らはさまざまなことに焦点を当てています。回避戦略から始めましょう。
回避戦略
回避する方法
回避戦略の主なアイデアは、コードロジックを最適化して、デッドロックの可能性を根本的に排除することです。一般的に、デッドロックの主な理由の1つは、逆の順序で異なるロックを取得することです。そこで、ロック取得シーケンスを調整してデッドロックを回避する方法を示します。
送金時のデッドロックを回避する
まず、転送中のデッドロックの状況を見てみましょう。この例は、デッドロックについて学習するために作成された概略的な例であるため、実際の銀行システムの設計とは大きく異なりますが、送金ではなくデッドロックを回避する方法を主に検討しているため、問題ではありません。ビジネスロジック。
(1)デッドロックが発生しました
スレッドセーフを確保するために、転送システムは転送前に2つのロック(2つのロックオブジェクト)を取得する必要があります。これは、転送されるアカウントと転送されるアカウントです。このレベルの制限が課されていない場合、スレッドがバランスを変更している間、他のスレッドが同時に変数を変更する可能性があり、スレッドの安全性の問題が発生する可能性があります。したがって、2つのロックが取得されるまで天びんを操作することはできません。2つのロックが取得された後でのみ、次の実際の転送操作を実行できます。もちろん、送金する残高が口座の残高より多い場合は、残高が負の数になることは許されないため、送金することはできません。
この期間中、デッドロックの可能性は隠されています。コードを見てみましょう。
public class TransferMoney implements Runnable {
int flag;
static Account a = new Account(500);
static Account b = new Account(500);
static class Account {
public Account(int balance) {
this.balance = balance;
}
int balance;
}
@Override
public void run() {
if (flag == 1) {
transferMoney(a, b, 200);
}
if (flag == 0) {
transferMoney(b, a, 200);
}
}
public static void transferMoney(Account from, Account to, int amount) {
//先获取两把锁,然后开始转账
synchronized (to) {
synchronized (from) {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账" + amount + "元");
}
}
}
public static void main(String[] args) throws InterruptedException {
TransferMoney r1 = new TransferMoney();
TransferMoney r2 = new TransferMoney();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
}
コードでは、int型のフラグが最初に定義されます。これは、さまざまなスレッドを制御してさまざまなロジックを実行するために使用されるフラグビットです。次に、アカウントを表すために2つのアカウントオブジェクトaとbが作成され、当初の残高は500元でした。
次にrunメソッドを見てみましょう。このメソッドでは、transferMoneyメソッドに渡されるパラメーターの順序は、フラグ値に従って決定されます。フラグが1の場合、200元がアカウントaからアカウントbに転送されることを意味します。 ;逆に、フラグが0の場合、アカウントbからアカウントaに200元を転送します。
transferMoney転送メソッドをもう一度見てみましょう。このメソッドは、最初に2つのロック、つまり同期(to)と同期(from)の取得を試みます。すべてが正常に取得されると、最初に残高が送金金額を送金するのに十分であるかどうかが判断されます。不足している場合は、直接返品を使用して引き出します。残高が十分な場合は、送金の残高が差し引かれます。口座を引き出し、送金された口座の残高を差し引きます。残高を口座に追加し、最後に「XX元の送金に成功しました」というメッセージを印刷します。
main関数では、2つの新しいTransferMoneyオブジェクトを作成し、それらのフラグをそれぞれ1と0に設定してから、それらを2つのスレッドに渡し、すべてを開始し、最後にそれぞれの残高を出力します。
実行結果は以下のとおりです。
成功转账200元
成功转账200元
a的余额500
b的余额500
コードは正常に実行でき、印刷結果は論理的です。現時点では、各ロックの保持時間が非常に短く、解放も非常に速いため、デッドロックは発生しません。したがって、同時実行性が低い場合、デッドロックが発生しにくくなります。次に、コードを少し調整してデッドロックにします。
銀行のネットワーク遅延などをシミュレートするために同期された2つの間にThread.sleep(500)を追加すると、transferMoneyメソッドは次のようになります。
public static void transferMoney(Account from, Account to, int amount) {
//先获取两把锁,然后开始转账
synchronized (to) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (from) {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账" + amount + "元");
}
}
}
transferMoneyの変更は、同期された2つの間、つまり、最初のロックを取得した後、2番目のロックを取得する前に、500ミリ秒スリープするステートメントを追加したことです。この時点でプログラムを再実行すると、デッドロックが発生する可能性が高くなり、コンソールにステートメントが出力されず、プログラムが停止しません。
デッドロックの理由を分析しましょう。主な理由は、2つの異なるスレッドが2つのロックを反対の順序で取得するためです(最初のスレッドによって取得された2つのアカウントと2番目のスレッドによって取得された2つのアカウント)アカウントの順序はまったく逆です。最初のスレッドの「転送アカウント」は2番目のスレッドの「転送アカウント」)であるため、この「逆順」の観点からデッドロックの問題を解決できます。
(2)実際にはロックの取得順序は気にしない
考えてみると、実際、送金するときは、2つのロックの相対的な取得順序は気にしないことがわかります。送金の際は、先に振込口座ロックオブジェクトを取得する場合でも、入金口座ロックオブジェクトを取得する場合でも、最終的に2つのロックを取得できれば、安全な運用が可能です。それでは、ロックを取得する順序を調整して、最初に取得したアカウントがアカウントが「転送された」か「転送された」かとは関係がないようにします。代わりに、HashCodeの値を使用して、スレッドセーフを確保するためのシーケンスを決定します。
修復されたtransferMoneyメソッドは次のとおりです。
public static void transferMoney(Account from, Account to, int amount) {
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账" + amount + "元");
}
}
} else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账" + amount + "元");
}
}
}
}
ご覧のとおり、これら2つのアカウントのHashCodeを個別に計算し、HashCodeのサイズに応じてロックを取得する順序を決定します。このように、どのスレッドが最初に実行されても、転送されるか転送されるかに関係なく、ロックを取得する順序はHashCodeの値に厳密に従って決定されるため、全員がロックを取得する順序は次のようになります。同じで、ロックの取得はありません。逆の順序で、デッドロックが回避されます。
(3)主キーを持つ方が安全で便利です
主キーを使用してロック取得シーケンスを決定する方法を見てみましょう。これにより、より安全で便利になります。HashCodeはより一般的であり、すべてのオブジェクトを持っているため、今はソート標準としてHashCodeを使用しましたが、同じHashCodeが発生する可能性は非常に低いです。実際の本番環境では、ソートが必要なのはエンティティクラスであることが多く、エンティティクラスには通常主キーIDがあります。主キーIDは一意で反復性がないため、クラスに主キーが含まれているとはるかに便利です。属性。HashCodeを計算する必要はなく、ソートに主キーIDを直接使用します。ロックを取得する順序は、主キーIDのサイズによって決定されるため、デッドロックを確実に回避できます。
上記では、デッドロック回避戦略を紹介しました。
検出と回復の戦略
2番目の戦略である検出と回復の戦略をもう一度見てみましょう。
デッドロック検出アルゴリズムとは
これは、デッドロックを回避する以前の戦略とは異なります。デッドロックを回避することは、ロジックを介してデッドロックが発生するのを防ぐことです。ここでの検出および回復戦略は、システムが最初にデッドロックしてから解放できるようにすることです。たとえば、システムは、ロックが呼び出されるたびに呼び出し情報を記録して「ロック呼び出しリンクグラフ」を形成し、デッドロック検出アルゴリズムを使用して間隔を置いて検出し、このグラフにループがあるかどうかを検索できます。デッドロックが発生すると、特定のリソースを奪うなどのデッドロック回復メカニズムを使用して、デッドロックのロックを解除して回復できます。そのため、その考え方は以前のデッドロック回避戦略とは大きく異なります。
デッドロックを検出した後、デッドロックのロックを解除するにはどうすればよいですか?
方法1-スレッドの終了
デッドロックのロックを解除する最初の方法は、スレッド(または以下と同じプロセス)を終了することです。ここで、システムはデッドロックに陥ったスレッドを1つずつ終了します。スレッドは終了し、リソースはで解放されます。同時に、デッドロックが解決されるようにします。
もちろん、この終了は注文に注意を払う必要があります。一般的に、次の考慮事項があります。
(1)優先度
一般的に、スレッドまたはプロセスの優先度は終了時に考慮され、優先度の低いスレッドが最初に終了します。たとえば、フォアグラウンドスレッドはインターフェイスの表示に関与しますが、これはユーザーにとって非常に重要であるため、フォアグラウンドスレッドの優先度はバックグラウンドスレッドの優先度よりも高いことがよくあります。
(2)占有され必要なリソース
同時に、スレッドが占有するリソースの数と、まだ必要なリソースの数は?スレッドがすでに多くのリソースを占有していて、タスクを正常に完了するために最後の数個のリソースのみが必要な場合、システムはそのようなスレッドを最初に終了することを選択せず、他のスレッドを終了して、糸。
(3)すでに実行時間
考慮できるもう1つの要素は実行時間です。たとえば、現在のスレッドが何時間も、あるいは何日も実行されていて、タスクがすぐに完了するため、このスレッドを終了することは賢明な選択ではないかもしれません。実行を開始したばかりのスレッドを終了させ、後で再起動して、コストを削減します。
さまざまなアルゴリズムや戦略があり、実際のビジネスに応じて調整することができます。
方法2-リソースのプリエンプション
デッドロックを解決する2番目の方法は、リソースのプリエンプションです。実際、スレッド全体を終了する必要はありませんが、すでに取得したリソースをスレッドから奪うだけで済みます。たとえば、スレッドを数ステップ戻してリソースを解放するだけで、スレッドを解放する必要はありません。スレッド全体を終了します。これにより結果が発生します。スレッド全体を終了した場合の結果は小さくなり、コストは低くなります。
もちろん、この方法には欠点もあります。つまり、アルゴリズムが適切でない場合、プリエンプションするスレッドは常に同じスレッドである可能性があり、スレッドの枯渇を引き起こします。つまり、このスレッドはすでに取得したリソースを奪われているため、長時間実行できなくなります。
上記は、デッドロックの検出と回復の戦略です。
ダチョウ戦略
ダチョウの戦略をもう一度見てみましょう。ダチョウの戦略は、危険に遭遇したときに頭を砂に埋めて危険が見えないという特徴があるため、ダチョウにちなんで名付けられました。
ダチョウの戦略は、システムのデッドロックの可能性が高くなく、結果が特に深刻でなくなったら、最初にそれを無視することを選択できることを意味します。サービスの再起動などのデッドロックが発生するまで、手動で修復することは不可能ではありません。内部システムなど、システムの使用人数が少ない場合、同時実行量が極端に少ない場合、数年間はデッドロックしない可能性があります。この点で、入出力比を考慮しており、当然デッドロックの問題に対処する必要はありません。これは、当社のビジネスシナリオに基づいた合理的な選択です。
まとめ
デッドロックを解決するための戦略は何ですか。オンラインでデッドロックが発生した場合、重要なデータを保存した後、最初にオンラインサービスを復元する必要があります。次に、3つの特定の修復戦略を導入します。1つは戦略を回避すること、主なアイデアはロックの取得順序を変更して逆の発生を防ぐことです。シーケンシャルロック取得。2つ目はデッドロックの発生を可能にする検出および回復戦略ですが、一度発生すると解決策があります。3つ目はダチョウ戦略です。