上のレベルの私の先生Javaスレッドのクラスは、私が確信していないことを言った。
彼は、次のコードがready
変数を必ずしも更新するとは限らないと述べました。彼によると、特に各スレッド(メインスレッドとReaderThread
)が独自のプロセッサで実行されているため、同じレジスタ/ cache/etcと1つのCPUはもう一方を更新しません。
基本的に、彼はready
がメインスレッドで更新される可能性はあるが、ReaderThread
では更新されないため、ReaderThread
が無限にループする可能性があると言いました。
彼はまた、プログラムが0
または42
を出力できると主張しました。 42
は印刷できますが、0
は印刷できません。彼は、これがnumber
変数がデフォルト値に設定されている場合だと述べました。
スレッド間で静的変数が更新されることは保証されないかもしれないと思いましたが、これはJavaにとって非常に奇妙なことです。 ready
をvolatileにすると、この問題は修正されますか?
彼はこのコードを示しました:
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready) Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
可視性に関しては、静的変数について特別なことは何もありません。それらがアクセス可能である場合、どのスレッドもそれらにアクセスできます。したがって、それらはより公開されているため、並行性の問題が発生しやすくなります。
JVMのメモリモデルによって課せられる可視性の問題があります。 メモリモデルと書き込みがスレッドに対してどのように見えるようになるかについて説明した記事があります 。 を確立しない限り、1つのスレッドが他のスレッドからタイムリーに見えるようになるまでの変更をカウントすることはできません(実際、JVMには、それらの変更をすべての時間枠で表示する義務はありません)起こる前の関係 。
以下はそのリンクからの引用です(Jed Wesley-Smithによるコメントで提供されています):
Java Language Specificationの第17章では、共有変数の読み取りや書き込みなどのメモリ操作に関する発生前関係を定義しています。1つのスレッドによる書き込みの結果は、読み取りに対して可視であることが保証されます読み取り操作の前に書き込み操作が発生する場合にのみ、別のスレッドによって同期化された揮発性の構成体、およびThread.start()およびThread.join()メソッドは、発生前の関係を形成できます。
スレッド内の各アクションは、プログラムの順序で後になるそのスレッド内のすべてのアクションの前に発生します。
モニターのロック解除(同期ブロックまたはメソッド終了)は、同じモニターの後続のロック(同期ブロックまたはメソッドエントリ)が発生する前に発生します。また、発生前の関係は推移的であるため、ロックを解除する前のスレッドのすべてのアクションは、そのモニターをロックするスレッドに続くすべてのアクションの前に発生します。
揮発性フィールドへの書き込みは、同じフィールドの後続の読み取りのたびに発生します。揮発性フィールドの書き込みおよび読み取りは、モニターの開始および終了と同様のメモリー整合性効果を持ちますが、相互排他ロックを伴いません。
スレッドで開始する呼び出しは、開始されたスレッドのアクションの前に発生します。
スレッド内のすべてのアクションは、他のスレッドがそのスレッドの結合から正常に戻る前に発生します。
彼はvisibilityについて話していましたが、文字通りに解釈されるべきではありません。
静的変数は確かにスレッド間で共有されますが、1つのスレッドで行われた変更はすぐに別のスレッドに表示されず、変数の2つのコピーがあるように見えます。
この記事は、彼が情報を提示した方法と一致するビューを示しています。
まず、あなたはJavaメモリモデルについて少し理解する必要があります。私は長年にわたってそれを簡潔かつうまく説明するのに少し苦労しました。あなたがそれをこのように想像するなら、それを説明するのは:
Javaの各スレッドは、個別のメモリ空間で行われます(これは明らかに正しくありませんので、この点についてはご容赦ください)。
メッセージパッシングシステムの場合と同様に、特別なメカニズムを使用して、これらのスレッド間で通信が行われることを保証する必要があります。
あるスレッドで発生したメモリ書き込みは、「リーク」して別のスレッドから見える可能性がありますが、これは決して保証されません。明示的な通信がなければ、どの書き込みが他のスレッドに表示されるか、またはそれらが表示される順序を保証することはできません。
...
繰り返しますが、これは単に文字通りJVMがどのように機能するかではなく、スレッド化と揮発性について考えるための単なるメンタルモデルです。
基本的には真実ですが、実際には問題はより複雑です。共有データの可視性は、CPUキャッシュだけでなく、命令のアウトオブオーダー実行によっても影響を受ける可能性があります。
したがって、Javaは メモリモデル を定義します。これは、スレッドが共有データの一貫した状態を確認できる状況を示します。
特定のケースでは、volatile
を追加すると可視性が保証されます。
それらは両方とも同じ変数を参照するという意味でもちろん「共有」されていますが、必ずしも互いの更新を見ることはありません。これは、静的だけでなく、あらゆる変数に当てはまります。
また、理論的には、変数がvolatile
として宣言されているか、書き込みが明示的に同期されていない限り、別のスレッドによって行われた書き込みは異なる順序に見える可能性があります。
単一のクラスローダー内では、静的フィールドは常に共有されます。データをスレッドに明示的にスコープするには、ThreadLocal
などの機能を使用する必要があります。
静的プリミティブ型変数を初期化するときJava defaultは静的変数の値を割り当てます
public static int i ;
このような変数を定義すると、デフォルト値i = 0になります。そのため、0を取得する可能性があります。その後、メインスレッドはブール値の値をtrueに更新します。 readyは静的変数であるため、メインスレッドと他のスレッドは同じメモリアドレスを参照するため、ready変数が変更されます。そのため、セカンダリスレッドはwhileループから抜け出し、値を出力します。値を印刷するとき、numberの初期化値は0です。メインスレッド更新番号変数の前にスレッドループがwhileループを通過した場合。 0を印刷する可能性があります