JavaRush /Java Blog /Random-JA /フロー管理。volatile キーワードと yield() メソッド

フロー管理。volatile キーワードと yield() メソッド

Random-JA グループに公開済み
こんにちは!私たちはマルチスレッドの研究を続けており、今日は新しいキーワードである volatile と yield() メソッドについて学びます。それが何なのか考えてみましょう:)

キーワードは揮発性です

マルチスレッド アプリケーションを作成する場合、2 つの重大な問題に直面する可能性があります。 まず、マルチスレッド アプリケーションの動作中に、さまざまなスレッドが変数の値をキャッシュできます(これについては、「volatile の使用」の講義で詳しく説明します)。1 つのスレッドが変数の値を変更しても、2 番目のスレッドは変数のキャッシュされた独自のコピーを操作していたため、この変更を認識できなかった可能性があります。当然のことながら、その結果は深刻になる可能性があります。これが単なる「変数」ではなく、たとえば銀行カードの残高が突然ランダムに前後に飛び始めたと想像してください :) あまり気持ちの良いものではありませんよね? 次に、Java では、longとを除くすべての型のフィールドに対する読み取りおよび書き込み操作はdoubleアトミックです。 原子性とは何ですか? たとえば、あるスレッドで変数の値を変更しint、別のスレッドでこの変数の値を読み取る場合、古い値か新しい値、つまり変更後に判明した値のいずれかを取得します。スレッド 1. そこには「中間オプション」は表示されないかもしれません。ただし、これはlongとではdouble機能しません。なぜ?クロスプラットフォームだからです。最初のレベルで、Java の原則は「一度書けばどこでも機能する」と述べたことを覚えていますか? これはクロスプラットフォームです。つまり、Java アプリケーションはまったく異なるプラットフォーム上で実行されます。たとえば、Windows オペレーティング システム、さまざまなバージョンの Linux または MacOS など、どこでもこのアプリケーションは安定して動作します。 longそしてdouble- Java で最も「重い」プリミティブ: 重さは 64 ビットです。また、一部の 32 ビット プラットフォームでは、64 ビット変数の読み取りおよび書き込みのアトミック性が実装されていません。このような変数は 2 回の操作で読み書きされます。まず最初の 32 ビットが変数に書き込まれ、次に別の 32 ビットが書き込まれます。したがって、これらの場合には問題が発生する可能性があります。1 つのスレッドが変数に 64 ビット値を書き込みますХそして彼はそれを「2つのステップ」で実行します。同時に、2 番目のスレッドはこの変数の値を読み取ろうとし、最初の 32 ビットはすでに書き込まれていますが、2 番目のビットはまだ書き込まれていないとき、ちょうど真ん中で読み取りを実行します。その結果、中間の誤った値が読み取られ、エラーが発生します。たとえば、このようなプラットフォームで変数に数値 (9223372036854775809) を書き込もうとすると、64 ビットを占有します。バイナリ形式では次のようになります: 100000000000000000000000000000000000000000000000000000000000000001 最初のスレッドはこの数値を変数に書き込み始め、最初に最初の 32 ビットを書き込みます: 100000000000000000000 0000000 00000、そして 2 番目の 32: 00000000000000000000000000000001 そして 2 番目のスレッドがこのギャップに食い込み、変数の中間値 - 10000000000000000000000000000000 (既に書き込まれている最初の 32 ビット) を読み取ります。10 進法では、この数値は 2147483648 に相当します。つまり、数値 9223372036854775809 を変数に書き込みたかっただけですが、一部のプラットフォームではこの操作がアトミックではないため、「左側」の数値 2147483648 が得られました。必要のないものが突然現れ、それがプログラムの動作にどのような影響を与えるかは不明です。2 番目のスレッドは、最終的に書き込まれる前に変数の値を読み取るだけです。つまり、最初の 32 ビットは参照されますが、2 番目の 32 ビットは参照されません。もちろん、これらの問題は昨日発生したわけではなく、Java では、たった 1 つのキーワード、volatileを使用して解決されます。プログラム内で volatile という単語を使用して変数を宣言すると...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…だということだ:
  1. 常にアトミックに読み書きされます。64 ビットdoubleまたはlong.
  2. Java マシンはそれをキャッシュしません。したがって、10 個のスレッドがローカル コピーで動作する状況は除外されます。
これは、2 つの非常に深刻な問題を一言で解決する方法です :)

yield() メソッド

クラスの多くのメソッドをすでに見てきましたThreadが、初めて知る重要なメソッドが 1 つあります。これがyield() メソッドです。英語から「ギブイン」と訳されます。そして、それがまさにこのメソッドが行うことです。 フロー管理。 volatile キーワードと yield() メソッド - 2スレッド上で yield メソッドを呼び出すと、実際には他のスレッドに次のように伝えます。急いでない。" これがどのように機能するかを示す簡単な例を次に示します。
public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
3 つのスレッド ( および ) を順番に作成して起動Thread-0Thread-1ますThread-2Thread-0最初にスタートし、すぐに他の人に道を譲ります。それが始まるとThread-1、また道を譲ります。その後、 が始まりますがThread-2、これも劣ります。これ以上スレッドはなく、Thread-2最後のスレッドがその場所を放棄した後、スレッド スケジューラは次のように調べます。以前に最後にその席を譲ったのは誰ですかThread-2? 私はそれがそうだったと思うThread-1?わかりました、それでは終わらせてください。」 Thread-1スレッド スケジューラは最後までその仕事を行い、その後、スレッド スケジューラは次のように調整を続けます。他に並んでいる人はいますか?」キューにはスレッド 0 があります。スレッド 1 の直前にその場所を放棄しました。今、問題は彼に降りかかり、彼は最後まで遂行されつつある。その後、スケジューラはスレッドの調整を終了します。「わかりました、スレッド 2、他のスレッドに道を譲りました。それらはすべてすでに動作しています。最後に道を譲ったのはあなただったので、今度はあなたの番です。」この後、スレッド 2 が完了するまで実行されます。コンソール出力は次のようになります。 Thread-0 が他のスレッドに道を譲ります Thread-1 が他のスレッドに道を譲ります Thread-2 が他のスレッドに道を譲ります Thread-1 は実行を終了しました。スレッド 0 の実行が終了しました。スレッド 2 の実行が終了しました。 もちろん、スレッド スケジューラはスレッドを別の順序 (たとえば、0-1-2 ではなく 2-1-0) で実行できますが、原理は同じです。

前に発生するルール

今日最後に触れることは、「前に起こる」原則です。すでにご存知のとおり、Java では、タスクを完了するためにスレッドに時間とリソースを割り当てる作業のほとんどは、スレッド スケジューラによって行われます。また、スレッドが任意の順序で実行される様子を何度も見たことがありますが、ほとんどの場合、それを予測することは不可能です。そして一般に、以前に行った「シーケンシャル」プログラミングの後では、マルチスレッド化はランダムなもののように見えます。すでに見たように、マルチスレッド プログラムの進行は、一連のメソッドを使用して制御できます。しかし、これに加えて、Java マルチスレッドにはもう 1 つの「安定性の島」があります。それは「happens-before」と呼ばれる 4 つのルールです。英語から文字通り、これは「前に起こる」または「前に起こる」と翻訳されます。これらのルールの意味は非常に簡単に理解できます。A2 つのスレッド (と )があると想像してくださいB。これらの各スレッドは、操作1とを実行できます2。各ルールで「A happens-before BA 」と言う場合、これは、操作前にスレッドによって行われたすべての変更と、この操作に伴う変更が、操作の実行時に1スレッドに表示されることを意味します。手術が行われた後。これらの各ルールにより、マルチスレッド プログラムを作成するときに、一部のイベントが 100% の確率で他のイベントよりも先に発生し、操作時にスレッドが操作中に行った変更を常に認識することが保証されます。。それらを見てみましょう。 B2B2А1

ルール1。

ミューテックスの解放は、別のスレッドが同じモニターを取得する前に行われます。さて、ここではすべてが明らかです。オブジェクトまたはクラスのミューテックスが 1 つのスレッド (たとえば thread ) によって取得された場合、А別のスレッド ( thread B) がそれを同時に取得することはできません。ミューテックスが解放されるまで待つ必要があります。

ルール2。

はメソッドのThread.start() 前に発生します Thread.run()。何も複雑なことはありません。すでにご存知のとおり、メソッド内のコードの実行を開始するにはrun()、スレッド上でメソッドを呼び出す必要がありますstart()。それは彼のものであり、方法そのものではありませんrun()。このルールにより、Thread.start()実行前に設定されたすべての変数の値が、実行を開始したメソッド内で確実に表示されるようになりますrun()

ルール3。

メソッドの完了はメソッド exitrun() の前に行われますjoin()А2 つのストリーム -とに戻りましょうBjoin()スレッドが作業を実行する前にB完了するまで待機する必要があるような方法でメソッドを呼び出します。Aこれは、オブジェクト A のメソッドがrun()最後まで確実に実行されることを意味します。run()また、スレッドメソッド内で発生するデータのすべての変更は、スレッドが完了を待ってスレッド自体が動作を開始するときにA完全に可視になります。 BA

ルール4。

揮発性変数への書き込みは、同じ変数から読み取る前に行われます。volatile キーワードを使用すると、実際には常に現在の値が取得されます。longとの場合でもdouble、前述した問題があります。すでに理解されているように、一部のスレッドで行われた変更は、他のスレッドに常に表示されるわけではありません。しかし、もちろん、そのようなプログラムの動作が私たちに合わない状況が非常によくあります。スレッド内の変数に値を割り当てたとしますA
int z;.

z= 555;
スレッドがB変数の値をコンソールに出力する場合z、変数に割り当てられた値がわからないため、簡単に 0 を出力する可能性があります。したがって、ルール 4 は、変数をzvolatile として宣言した場合、あるスレッドでのその値の変更は常に別のスレッドで参照できることを保証します。前のコードに volatile という単語を追加すると...
volatile int z;.

z= 555;
...ストリームがBコンソールに 0 を出力する状況は除外されます。揮発性変数への書き込みは、変数からの読み取りの前に行われます。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION