ホームページ  >  記事  >  Java  >  マルチスレッドの概念パートのデッドロック

マルチスレッドの概念パートのデッドロック

DDD
DDDオリジナル
2024-11-05 10:03:01824ブラウズ

マルチスレッド シリーズのパート 3 へようこそ!

  • パート 1 では、原子性不変性 について調査しました。
  • パート 2 では、飢餓について説明しました。

このパートでは、マルチスレッドにおける デッドロック のメカニズムについて詳しく説明します。その原因、コードが渋滞交差点にならないようにするために使用できる予防戦略とその特定方法。多くの場合、目に見えるエラーが表示されずにアプリケーションが停止してしまい、開発者は困惑し、システムがフリーズしてしまいます。

Multithreading Concepts Part  Deadlock

同時実行の複雑なトラックをナビゲートする

デッドロックを理解するのに役立つ例えは、交差する線路上に複数の列車がある鉄道網を想像することです。

それぞれの列車が次の列車の発車を待っているため、どの列車も進むことができず行き詰まりが発生します。このシナリオでは、非効率的な信号システムにより、各列車が次のセクションが空いているかどうかを最初に確認することなくそれぞれのセクションに進入することができ、すべての列車が破られないサイクルに閉じ込められました。

この電車の例は、マルチスレッドにおける典型的なデッドロックを示しています。スレッド (電車など) は他のリソースが解放されるのを待っている間、リソース (トラック セクション) を保持しますが、どれも先に進むことができません。ソフトウェアにおけるこの種のデッドロックを防ぐには、循環依存を回避し、各スレッドの安全な通過を確保するために、効果的なリソース管理戦略 (よりスマートな鉄道信号に類似したもの) を実装する必要があります。

1. デッドロックとは何ですか?

デッドロック は、スレッド (またはプロセス) が無期限にブロックされ、他のスレッドが保持するリソースを待機している状況です。このシナリオでは、依存関係の断ち切れないサイクルが発生し、関係するスレッドが先に進むことができなくなります。検出、防止、解決の方法を検討する前に、デッドロックの基本を理解することが不可欠です。

2. デッドロックの条件

デッドロックが発生するには、コフマン条件として知られる次の 4 つの条件が同時に満たされる必要があります。

  • 相互排他: 少なくとも 1 つのリソースを非共有モードで保持する必要があります。つまり、一度に 1 つのスレッドのみがそのリソースを使用できます。

  • 保留して待機: スレッドは 1 つのリソースを保持し、他のスレッドが保持する追加のリソースを取得するまで待機する必要があります。

  • プリエンプションなし: スレッドからリソースを強制的に奪うことはできません。彼らは自発的に解放されなければなりません。

  • 循環待機: スレッドの閉じたチェーンが存在し、各スレッドがチェーン内の次のスレッドが必要とするリソースを少なくとも 1 つ保持します。

Multithreading Concepts Part  Deadlock

シーケンス図として理解しよう

Multithreading Concepts Part  Deadlock

上のアニメーションでは、

  • スレッド A はリソース 1 を保持し、リソース 2 を待機します
  • スレッド B がリソース 2 を保持し、リソース 1 を待機している間

デッドロックに関する上記の 4 つの条件がすべて存在し、無期限のブロックが発生します。それらのいずれかを破れば、デッドロックを防ぐことができます。

3. デッドロックの検出/監視

デッドロックの検出は、特に大規模なアプリケーションでは困難な場合があります。ただし、次のアプローチはデッドロックを特定するのに役立ちます

  • ツール: Java の JConsole、VisualVM、および IDE のスレッド アナライザーは、デッドロックをリアルタイムで検出できます。
  • スレッド ダンプとログ: スレッド ダンプを分析すると、待機中のスレッドとそれらが保持しているリソースが明らかになります。

デッドロックのデバッグ/監視方法を理解するための詳細な概要については、「VisualVM と jstack を使用したデッドロックのデバッグと監視」を参照してください

4. デッドロック防止のための戦略

  • 待機ダイおよび創傷待機スキームの適用
    Wait-Die スキーム: スレッドが別のスレッドによって保持されているロックを要求すると、データベースは相対的な優先順位を (通常は各スレッドのタイムスタンプに基づいて) 評価します。要求元のスレッドの優先順位が高い場合、スレッドは待機します。それ以外の場合は、停止します (再起動します)。
    Wound-Wait スキーム: 要求元のスレッドの優先順位が高い場合、ロックを強制的に解放することで、優先順位の低いスレッドをワインディング (プリエンプト) します。

  • 共有状態の不変オブジェクト
    共有状態を可能な限り不変として設計します。不変オブジェクトは変更できないため、同時アクセスにロックが不要となり、デッドロックのリスクが軽減され、コードが簡素化されます。

  • ロック取得のタイムアウト付き tryLock の使用: 標準の同期ブロックとは異なり、ReentrantLock では、tryLock(timeout,unit) を使用して、指定された期間内にロックの取得を試行できます。ロックがその時間内に取得されない場合、リソースが解放され、無期限のブロックが防止されます。

ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

public void acquireLocks() {
    try {
        if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                    // Critical section
                }
            } finally {
                lock2.unlock();
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        lock1.unlock();
    }
}

  • ロックの注文と解放 ロック取得の厳密なグローバル順序を設定します。すべてのスレッドが一貫した順序でロックを取得すると、循環依存関係が形成される可能性が低くなり、デッドロックが回避されます。たとえば、コードベース全体で常に lock2 の前に lock1 を取得します。この方法は大規模なアプリケーションでは困難になる可能性がありますが、デッドロックのリスクを軽減するには非常に効果的です。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockOrderingExample {

    private static final Lock lock1 = new ReentrantLock();
    private static final Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            acquireLocksInOrder(lock1, lock2);
        });

        Thread thread2 = new Thread(() -> {
            acquireLocksInOrder(lock1, lock2);
        });

        thread1.start();
        thread2.start();
    }

    private static void acquireLocksInOrder(Lock firstLock, Lock secondLock) {
        try {
            firstLock.lock();
            System.out.println(Thread.currentThread().getName() + " acquired lock1");

            secondLock.lock();
            System.out.println(Thread.currentThread().getName() + " acquired lock2");

            // Perform some operations

        } finally {
            secondLock.unlock();
            System.out.println(Thread.currentThread().getName() + " released lock2");

            firstLock.unlock();
            System.out.println(Thread.currentThread().getName() + " released lock1");
        }
    }
}
  • スレッドセーフ/同時実行コレクションを使用する: Java の java.util.concurrent パッケージは、内部で同期を処理する一般的なデータ構造 (ConcurrentHashMap、CopyOnWriteArrayList など) のスレッドセーフな実装を提供し、明示的なロックの必要性。これらのコレクションは、内部パーティショニングなどの手法を使用して、明示的なロックの必要性を回避するように設計されているため、デッドロックを最小限に抑えます。

  • ネストされたロックを避ける
    循環依存関係を避けるために、同じブロック内での複数のロックの取得を最小限に抑えます。ネストされたロックが必要な場合は、一貫したロック順序を使用してください

ソフトウェア エンジニア向けの重要なポイント

  • ロックが必要なデザインを作成すると、デッドロックが発生する可能性が生じます。
  • デッドロックは、プロセス間の依存関係のサイクルによって引き起こされるブロック問題です。それぞれのプロセスが別のプロセスによって保持されているリソースを待っているため、どのプロセスも先に進むことができず、どのプロセスもリソースの解放に進むことができません。
  • デッドロックは、関係するプロセスが完全に停止し、回復するにはデッドロック サイクルを打破する必要があるため、より深刻です。
  • デッドロックは、2 つの異なるロックがある場合、つまりロックを保持し、別のロックが解放されるのを待っている場合にのみ発生します。 (ただし、デッドロックにはさらに多くの条件があります)。
  • スレッドセーフは、デッドロックフリーを意味するものではありません。複数のスレッドから呼び出された場合でも、コードがそのインターフェイスに従って動作することを保証するだけです。クラスをスレッドセーフにすることには、通常、安全な実行を保証するためのロックの追加が含まれます。

アウトロ

初心者であっても、熟練した開発者であっても、同時システムで堅牢で効率的なコードを作成するには、デッドロックを理解することが重要です。この記事では、デッドロックとは何か、その原因、およびデッドロックを防ぐための実践的な方法について説明しました。効果的なリソース割り当て戦略を実装し、タスクの依存関係を分析し、スレッド ダンプやデッドロック検出ツールなどのツールを利用することで、開発者はデッドロックのリスクを最小限に抑え、スムーズな同時実行のためにコードを最適化できます。

マルチスレッドの核となる概念についての旅を続けていきますので、このシリーズの次の記事にご期待ください。 クリティカルセクションについて詳しく説明し、複数のスレッド間で共有リソースを安全に管理する方法を理解します。また、競合状態の概念についても説明します。これは、チェックしないままにすると予測できない動作やバグを引き起こす可能性がある一般的な同時実行の問題です。

各ステップで、アプリケーションをスレッドセーフ、効率的、復元力のあるものにする方法についてより深い洞察が得られます。より優れた、よりパフォーマンスの高いソフトウェアを構築するために、マルチスレッドの知識の限界を押し広げ続けてください!

参考文献

  • スタックオーバーフロー
  • インフォグラフィック
  • デッドロックを検出して修正する方法

以上がマルチスレッドの概念パートのデッドロックの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。