1. 概要

マルチスレッドはアプリケーションのパフォーマンスを向上させるのに役立ちますが、いくつかの問題も伴います。 このチュートリアルでは、Javaの例を使用して、デッドロックとライブロックの2つの問題を調べます。

2. デッドロック

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

デッドロックは、 2つ以上のスレッドが、別のスレッドによって保持されているロックまたはリソースを永久に待機している場合に発生します。 その結果、デッドロックされたスレッドが進行できないため、アプリケーションがストールまたは失敗する可能性があります。

古典的な食事する哲学者の問題は、マルチスレッド環境での同期の問題をうまく示しており、デッドロックの例としてよく使用されます。

2.2. デッドロックの例

まず、デッドロックを理解するための簡単なJavaの例を見てみましょう。

この例では、T1T2の2つのスレッドを作成します。  スレッドT1operation1を呼び出し、スレッドT2operationsを呼び出します。

操作を完了するには、スレッドT1が最初にlock1を取得し、次に lock2 を取得する必要がありますが、スレッドT2lock2を取得する必要があります最初に、次にlock1。 したがって、基本的に、両方のスレッドは逆の順序でロックを取得しようとしています。

それでは、DeadlockExampleクラスを作成しましょう。

public class DeadlockExample {

    private Lock lock1 = new ReentrantLock(true);
    private Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        DeadlockExample deadlock = new DeadlockExample();
        new Thread(deadlock::operation1, "T1").start();
        new Thread(deadlock::operation2, "T2").start();
    }

    public void operation1() {
        lock1.lock();
        print("lock1 acquired, waiting to acquire lock2.");
        sleep(50);

        lock2.lock();
        print("lock2 acquired");

        print("executing first operation.");

        lock2.unlock();
        lock1.unlock();
    }

    public void operation2() {
        lock2.lock();
        print("lock2 acquired, waiting to acquire lock1.");
        sleep(50);

        lock1.lock();
        print("lock1 acquired");

        print("executing second operation.");

        lock1.unlock();
        lock2.unlock();
    }

    // helper methods

}

このデッドロックの例を実行して、次の出力に注目してみましょう。

Thread T1: lock1 acquired, waiting to acquire lock2.
Thread T2: lock2 acquired, waiting to acquire lock1.

プログラムを実行すると、プログラムがデッドロックになり、終了しないことがわかります。 ログには、スレッドT1がスレッドT2によって保持されているlock2を待機していることが示されています。 同様に、スレッド T2 は、スレッドT1によって保持されているlock1を待機しています。

2.3. デッドロックの回避

デッドロックは、Javaでよくある同時実行の問題です。 したがって、潜在的なデッドロック状態を回避するようにJavaアプリケーションを設計する必要があります。

まず、スレッドに対して複数のロックを取得する必要がないようにする必要があります。 ただし、スレッドに複数のロックが必要な場合は、ロック取得の循環依存を回避するために、各スレッドが同じ順序でロックを取得するようにする必要があります。

LockインターフェースのtryLockメソッドのように、時限ロック試行を使用して、スレッドがブロックできない場合にスレッドが無限にブロックされないようにすることもできます。ロックを取得します。

3. Livelock

3.1. Livelockとは

Livelockは別の並行性の問題であり、デッドロックに似ています。 ライブロックでは、 2つ以上のスレッドが、デッドロックの例で見たように無限に待機するのではなく、相互に状態を転送し続けます。 その結果、スレッドはそれぞれのタスクを実行できなくなります。

ライブロックの優れた例は、例外が発生したときにメッセージコンシューマーがトランザクションをロールバックし、メッセージをキューの先頭に戻すメッセージングシステムです。 次に、同じメッセージがキューから繰り返し読み取られますが、別の例外が発生してキューに戻されます。 コンシューマーは、キューから他のメッセージを取得することはありません。

3.2. Livelockの例

ここで、ライブロック状態を示すために、前に説明したのと同じデッドロックの例を取り上げます。 この例でも、スレッドT1operation1を呼び出し、スレッドT2operation2を呼び出します。 ただし、これらの操作のロジックを少し変更します。

両方のスレッドは、作業を完了するために2つのロックが必要です。 各スレッドは最初のロックを取得しますが、2番目のロックが使用できないことを検出します。 したがって、他のスレッドを最初に完了させるために、各スレッドは最初のロックを解放し、両方のロックを再度取得しようとします。

LivelockExampleクラスを使用してlivelockをデモンストレーションしましょう。

public class LivelockExample {

    private Lock lock1 = new ReentrantLock(true);
    private Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        LivelockExample livelock = new LivelockExample();
        new Thread(livelock::operation1, "T1").start();
        new Thread(livelock::operation2, "T2").start();
    }

    public void operation1() {
        while (true) {
            tryLock(lock1, 50);
            print("lock1 acquired, trying to acquire lock2.");
            sleep(50);

            if (tryLock(lock2)) {
                print("lock2 acquired.");
            } else {
                print("cannot acquire lock2, releasing lock1.");
                lock1.unlock();
                continue;
            }

            print("executing first operation.");
            break;
        }
        lock2.unlock();
        lock1.unlock();
    }

    public void operation2() {
        while (true) {
            tryLock(lock2, 50);
            print("lock2 acquired, trying to acquire lock1.");
            sleep(50);

            if (tryLock(lock1)) {
                print("lock1 acquired.");
            } else {
                print("cannot acquire lock1, releasing lock2.");
                lock2.unlock();
                continue;
            }

            print("executing second operation.");
            break;
        }
        lock1.unlock();
        lock2.unlock();
    }

    // helper methods

}

それでは、この例を実行してみましょう。

Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T2: cannot acquire lock1, releasing lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: cannot acquire lock1, releasing lock2.
..

ログからわかるように、両方のスレッドが繰り返しロックを取得および解放しています。 このため、どのスレッドも操作を完了できません。

3.3. Livelockの回避

ライブロックを回避するには、ライブロックの原因となっている状態を調べて、それに応じた解決策を考え出す必要があります。

たとえば、ロックの取得と解放を繰り返してライブロックが発生する2つのスレッドがある場合、スレッドがランダムな間隔でロックの取得を再試行するようにコードを設計できます。 これにより、スレッドは必要なロックを取得するための公正な機会が得られます。

前に説明したメッセージングシステムの例での活性の問題に対処する別の方法は、失敗したメッセージを同じキューに戻すのではなく、別のキューに入れてさらに処理することです。

4. 結論

このチュートリアルでは、デッドロックとライブロックについて説明しました。 また、Javaの例を調べて、これらの問題のそれぞれを示し、それらを回避する方法について簡単に触れました。

いつものように、この例で使用されている完全なコードは、GitHubにあります。