目次
1.デッドロック状況
デッドロックとは、複数のスレッドが同時にブロックされ、そのうちの 1 つまたはすべてがリソースが解放されるのを待っている状況です。スレッドは無期限にブロックされるため、プログラムは正常に終了できません。
1.1 1 つのスレッドに複数のロックがある
スレッドに複数のロックがある場合、最初は最初のロックの取得に成功しますが、2 番目のロックに遭遇するとブロッキング待機が発生します。このブロッキング待機を解決するには、まずロックを解放する必要があり、ロックの解放にはスレッドのエントリが必要です。ドアを開けようとしているのに、鍵は部屋の中にあるようなものです。このプロセスにより、待機していたデッドロックが発生します。
synchronized (locker) {
synchronized (locker) {
System.out.println("一个线程多把锁");
}
}
ただし、synchronized はリエントラント ロックであり、ロックが追加されたかどうかを柔軟に判断できるため、この問題によって Java ではデッドロックが発生することはありません。ただし、C++ のロックは再入不可のロックであり、このコードの記述方法によってはデッドロックが発生します。
1.1.1 Java におけるリエントラント ロックの実装メカニズム
Java の同期ロックはリエントラント ロックであるため、スレッドが複数のロックを持っている場合に繰り返しロックされることはありませんが、中間ロック処理中にロックを解放すべきではないという別の問題が発生します。 jvm はロックをいつ解除するかを知っていますか?
同期化された解決策は、カウンタを使用することです。ロック操作が発生すると +1 になり、ロック解除操作が発生すると -1 になります。カウンタが 0 に達すると、ロックは実際に解放されます。
1.2 2 つのスレッドと 2 つのロック
2 つのスレッドがそれぞれロックを取得し、次にもう一方のロックを取得すると、デッドロックが発生します。
public class Demo21 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(()->{
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("对locker2对象加锁");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1) {
System.out.println("对locker1对象加锁");
}
}
});
t1.start();
t2.start();
}
}
ここでプログラムを実行するとデッドロックが発生します。ここでlocker1がlocker2を取得したいとき、locker2はすでにロックされているため、locker2がロックを解除するまで待つ必要があります。ただし、locker2がロックを解除するときは、locker1のロックを取得する必要があります。 lock であり、locker1 も A ロック状態です。家に帰って車の中に家の鍵があり、その後家にも車の鍵があるようなものです。その結果、デッドロックになるまで待機することになります。
1.3 N スレッドと M ロック
「哲学者の食事問題」では、5 つのスレッドと 5 つのロックの状況に対応する 5 人の哲学者と 5 つの箸を紹介します。各哲学者が食事をしたい場合は、左手で箸を取り、次に右手で箸を持ち上げる必要があり、他の哲学者が食べたい場合は、どちらかの箸が置かれるのを待つだけで済みます。
通常であれば争いは起こりませんが、5人の哲学者が同時に左手で箸を取ると待ちの状況が生じます。これによりデッドロックが発生します。
2. デッドロックの解決策
2.1 デッドロックの必要条件
1. 相互排他的使用: 1 つのスレッドがこのロックを取得し、他のスレッドはこのロックを取得できません。
2. 非プリエンプティブル: ロックは前の保持者によってのみ能動的に解放でき、他のスレッドによって奪い取ることはできません。
3. リクエストとホールド: スレッドが複数のロックを取得すると、前のロックの取得ステータスが維持されます。
4. 循環待機: 保持されているロックを取得したい場合、保持者がロックを解放するまで待つ必要があります。
デッドロックの必要条件を理解した後、そのいずれかを破ることでデッドロックの問題を解決できます。ただし、最初の 2 つの条件はデッドロックの必要条件であり、ロックの特性でもあるため、この 2 つの条件を破ることはできません。したがって、デッドロック問題を解決するには 3 と 4 しかありません。ただし、3 の条件を解決しようとすると、ほとんどのコードのロジックを変更する必要があり、コストが非常に高くなります。したがって、デッドロックの問題を解決するには、ループ待機状態を解消するのが最善の解決策です。
2.2 待機サイクルを打破する
バンカーズ アルゴリズムはデッドロック問題を解決できますが、より複雑であり、デッドロック問題を解決する過程でさらに複雑な問題が発生する可能性があるため、一般的にデッドロック問題を解決するためにバンカーズ アルゴリズムを使用することはお勧めできません。
ここでは、デッドロックの問題を解決するための簡単で効果的な方法を紹介します。それは、ロックに番号を付け、ロック順序を指定することです。たとえば、各スレッドが複数のロックを取得したい場合、最初に最小番号のロックを取得する必要があります。
同様に、この状況に応じて 2 つのスレッドと 2 つのロックの状況を変更すると、デッドロックの問題が解決されます。
public class Demo21 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(()->{
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("对locker2对象加锁");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker1) {//先获取编号顺序小的锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("对locker1对象加锁");
}
}
});
t1.start();
t2.start();
}
}
再度実行すると、デッドロックの問題が解決されたことがわかります。