スレッドがどのように相互作用するかの詳細の簡単な概要。前回は、スレッドがどのように相互に同期されるかを調べました。今回は、スレッドの相互作用時に発生する可能性のある問題を掘り下げ、それらを回避する方法について説明します。より詳細な学習に役立つリンクもいくつか提供します。
ここでスーパーサンプルを見ることができます: Java - Thread Starvation and Fairness。
序章
したがって、Java にはスレッドがあることがわかります。これについては、「Better together: Java and the Thread class」というタイトルのレビューで読むことができます。パート I — 実行のスレッド。また、「Better together: Java と Thread クラス」というタイトルのレビューで、スレッドが互いに同期できるという事実を調査しました。パート II — 同期。スレッドがどのように相互作用するかについて話しましょう。共有リソースをどのように共有するのでしょうか? ここでどのような問題が発生する可能性がありますか?デッドロック
最も恐ろしい問題はデッドロックです。デッドロックとは、2 つ以上のスレッドが他方のスレッドを永遠に待機している状態です。デッドロックについて説明している Oracle Web ページから例を取り上げます。
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
初めてここでデッドロックが発生することはないかもしれませんが、プログラムがハングした場合は、次のコマンドを実行してくださいjvisualvm
。 JVisualVM プラグインがインストールされている ([ツール] -> [プラグイン] 経由) と、デッドロックが発生した場所を確認できます。
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
スレッド 1 はスレッド 0 からのロックを待っています。なぜこのようなことが起こるのでしょうか? Thread-1
実行を開始し、Friend#bow
メソッドを実行します。synchronized
これは、(現在のオブジェクト) のモニターを取得していることを意味するキーワードでマークされていますthis
。メソッドの入力は他のFriend
オブジェクトへの参照でした。ここで、Thread-1
もう一方のメソッドを実行したいと考えておりFriend
、そのためにはロックを取得する必要があります。しかし、他のスレッド (この場合Thread-0
) がメソッドに入ることができた場合bow()
、ロックはすでに取得されており、Thread-1
待機します。Thread-0
、 およびその逆。この行き詰まりは解決不可能であり、私たちはそれをデッドロックと呼びます。解放できない死のグリップのように、デッドロックは打破することができない相互ブロックです。デッドロックの別の説明については、このビデオをご覧ください: Deadlock and Livelock Explained。
ライブロック
デッドロックがある場合、ライブロックもありますか? はい、あります:) ライブロックは、スレッドが表面的には生きているように見えても、作業を継続するために必要な条件が満たされないために何もできないときに発生します。基本的に、ライブロックはデッドロックに似ていますが、スレッドはモニターを待って「ハング」しません。その代わりに、彼らは永遠に何かをし続けます。例えば:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
このコードが成功するかどうかは、Java スレッド スケジューラがスレッドを開始する順序によって決まります。最初に開始するとThead-1
、ライブロックが発生します。
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
この例からわかるように、両方のスレッドが両方のロックを順番に取得しようとしますが、失敗します。しかし、行き詰まりではない。表面的にはすべて順調で、彼らは自分たちの仕事をしています。 JVisualVM によると、スリープ期間とパーク期間が見られます (これは、スレッドがロックを取得しようとするときです。スレッドの同期について説明したときに説明したように、スレッドはパーク状態に入ります) 。ここでライブロックの例を見ることができます: Java - Thread Livelock。
飢餓
デッドロックとライブロックに加えて、マルチスレッド中に発生する可能性のある別の問題、それがスタベーションです。この現象は、スレッドがブロックされないという点で以前の形式のブロックとは異なります。スレッドには単に十分なリソースがありません。その結果、一部のスレッドがすべての実行時間を費やしますが、他のスレッドは実行できません。https://www.logicbig.com/
Thread.sleep()
この例では、飢餓中にスレッドで何が起こるか、および からへの 1 つの小さな変更によりThread.wait()
負荷を均等に分散する方法を示します。
競合状態
マルチスレッドには「競合状態」というものが存在します。この現象は、スレッドがリソースを共有しているが、正しい共有が保証されない方法でコードが記述されている場合に発生します。例を見てみましょう:
public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
このコードは、最初はエラーを生成しない可能性があります。その場合、次のようになります。
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
ご覧のとおり、newValue
値の割り当て中に問題が発生しました。newValue
大きすぎます。value
競合状態のため、スレッドの 1 つが2 つのステートメントの間で変数を変更することに成功しました。スレッド間で競合が発生していることがわかります。ここで、金融取引で同様の間違いを犯さないことがいかに重要であるかを考えてみましょう...例と図は、ここでも見ることができます: Java スレッドで競合状態をシミュレートするコード。
揮発性
スレッドの相互作用について言えば、このvolatile
キーワードに言及する価値があります。簡単な例を見てみましょう。
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
最も興味深いのは、これは機能しない可能性が高いということです。新しいスレッドではフィールドの変更は認識されませんflag
。このflag
フィールドを修正するには、キーワードを使用する必要がありますvolatile
。方法と理由?プロセッサーがすべてのアクションを実行します。ただし、計算結果はどこかに保存する必要があります。このために、メイン メモリとプロセッサのキャッシュが存在します。プロセッサのキャッシュは、メイン メモリにアクセスする場合よりも高速にデータにアクセスするために使用される小さなメモリの塊のようなものです。しかし、すべてに欠点があります。キャッシュ内のデータは最新ではない可能性があります (上記の例のように、フラグ フィールドの値が更新されなかった場合)。それで、volatile
キーワードは、変数をキャッシュしたくないことを JVM に伝えます。これにより、すべてのスレッドで最新の結果を確認できるようになります。これは非常に単純化した説明です。キーワードについては、 こちらの記事volatile
をぜひ読んでみてください。詳細については、「 Java メモリ モデル 」と「 Java Volatile Keyword 」を読むことをお勧めします。さらに、これは可視性に関するものであり、変更の原子性に関するものではないことを覚えておくことが重要です。「競合条件」セクションのコードを見ると、IntelliJ IDEA にツールチップが表示されます。 この検査は、2010 年の リリース ノートに記載されていた問題IDEA-61117の一部として IntelliJ IDEA に追加されました。volatile
原子性
アトミック操作は分割できない操作です。たとえば、変数に値を割り当てる操作はアトミックである必要があります。残念ながら、インクリメント操作はアトミックではありません。インクリメントには、古い値を取得し、それに 1 を追加し、値を保存するという 3 つの CPU 操作が必要になるためです。なぜ原子性が重要なのでしょうか? インクリメント操作では、競合状態が発生すると、共有リソース (共有値) がいつでも突然変化する可能性があります。long
さらに、 やなどの 64 ビット構造を伴う操作はdouble
アトミックではありません。詳細については、「64 ビット値の読み取りおよび書き込み時にアトミック性を確保する」を参照してください。アトミック性に関連する問題は、次の例で見ることができます。
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
特別AtomicInteger
クラスは常に 30,000 を与えますが、金額はvalue
時々変わります。このトピックの短い概要は、「Java のアトミック変数の紹介」にあります。「比較交換」アルゴリズムは、アトミック クラスの中心にあります。詳細については、「ロックフリー アルゴリズムの比較 - JDK 7 と 8 の例に関する CAS と FAA」、またはWikipedia の「 比較とスワップ」の記事を参照してください。
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
前に起こる
「happens before」という面白くて不思議な概念があります。スレッドの研究の一環として、スレッドについて読んでください。前発生関係は、スレッド間のアクションが表示される順序を示します。多くの解釈や解説があります。このテーマに関する最新のプレゼンテーションの 1 つが、「Java "Happens-Before" Relationships」です。まとめ
このレビューでは、スレッドがどのように相互作用するかについていくつかの詳細を検討しました。発生する可能性のある問題と、それらを特定して排除する方法について話し合いました。このトピックに関する追加資料のリスト:- 二重チェックロック
- JSR 133 (Java メモリ モデル) FAQ
- IQ 35: デッドロックを防ぐにはどうすればよいですか?
- Douglas Hawkins による Java の並行性の概念 (2017)
GO TO FULL VERSION