スレッドセーフではないオブジェクトの公開
-
06-07-2019 - |
質問
「Java同時実行の実践」を読むと、セクション3.5にこの部分があります:
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
Holder
の2つのインスタンスを作成することによる明らかなスレッドセーフハザードに加えて、この本は発行の問題が発生する可能性があると主張しています。
さらに、次のような Holder
クラスの場合
public Holder {
int n;
public Holder(int n) { this.n = n };
public void assertSanity() {
if(n != n)
throw new AssertionError("This statement is false.");
}
}
AssertionError
がスローされる可能性があります!
これはどのように可能ですか?このようなばかげた振る舞いを許可できる唯一の方法は、 Holder
コンストラクターがブロックされない場合です。そのため、コンストラクターコードが別のスレッドで実行されている間にインスタンスへの参照が作成されます。
これは可能ですか?
解決
これが可能なのは、Javaのメモリモデルが弱いためです。読み取りと書き込みの順序は保証されません。
この特定の問題は、2つのスレッドを表す次の2つのコードスニペットで再現できます。
スレッド1:
someStaticVariable = new Holder(42);
スレッド2:
someStaticVariable.assertSanity(); // can throw
表面的には、これが発生する可能性はありません。これが発生する理由を理解するには、Java構文を通り抜けて、はるかに低いレベルに到達する必要があります。スレッド1のコードを見ると、基本的に一連のメモリの書き込みと割り当てに分割できます。
- pointer1へのメモリの割り当て
- オフセット0でpointer1に42を書き込む
- someStaticVariableへのpointer1の書き込み
Javaのメモリモデルは弱いため、スレッド2の観点からは、コードが次の順序で実際に実行される可能性があります。
- pointer1へのメモリの割り当て
- someStaticVariableへのpointer1の書き込み
- オフセット0でpointer1に42を書き込む
怖い?はい、でも起こり得る。
これは、スレッド2が n
が値42を取得する前に assertSanity
を呼び出すことができることを意味します。値 n
は、 assertSanity
の間に2回読み込まれます。1回目は操作#3が完了する前に1回、その後に2回読み込まれるため、2つの異なる値が表示され、例外がスローされます。
編集
他のヒント
Javaメモリモデルは、 Holder
参照への割り当てがオブジェクト内の変数への割り当ての前に表示されるように使用しました。
ただし、Java 5以降に有効になった最新のメモリモデルでは、少なくとも最終フィールドではこれが不可能になります。コンストラクター内のすべての割り当て" happen before"新しいオブジェクトへの参照の変数への割り当て。 Java言語仕様セクション17.4 詳細については、最も関連性の高いスニペットを次に示します。
オブジェクトは そのときに完全に初期化 コンストラクタが終了します。そのスレッド オブジェクトへの参照のみを表示できます そのオブジェクトが完全にされた後 初期化すると、 そのために正しく初期化された値 オブジェクトの最終フィールド
したがって、 n
は最終ではないので、例は失敗する可能性がありますが、 n
を最終にする場合でも問題ありません。
もちろん:
if (n != n)
JITコンパイラーがそれを最適化しないと仮定して、操作が以下の場合、非最終変数に対して確実に失敗する可能性があります:
- LHSを取得:n
- RHSを取得:n
- LHSとRHSの比較
その後、値は2つのフェッチ間で変更される可能性があります。
まあ、本では最初のコードブロックについて次のように述べています:
ここでの問題はホルダーではありません クラス自体、ただしホルダーは 適切に公開されていません。しかしながら、 ホルダーは不適切な影響を受けないようにすることができます nフィールドの宣言による公開 最終的に、ホルダーになります 不変;セクション3.5.2を参照してください
2番目のコードブロックの場合:
同期が使用されなかったため ホルダーを他の人に見えるようにする スレッド、私たちはホルダーがなかったと言います 適切に公開されました。二つのことができる 不適切に発行されたと間違っている オブジェクト。他のスレッドは ホルダーフィールドの古い値、および したがって、null参照またはその他を参照してください 値があったとしても古い値 ホルダーに入れられました。しかし、さらに悪いことに、 他のスレッドは最新のものを見ることができます ホルダー参照の値、ただし の状態の古い値 ホルダー。[16]物事をさらに減らすために 予測可能、スレッドは古くなっている可能性があります フィールドを初めて読み取るときの値 そして、より最新の値 次回、assertSanityの理由 AssertionErrorをスローできます。
JaredParは彼のコメントの中でこれをほとんど明示していると思います。
(注:ここでは投票を探していません。答えはコメントよりも詳細な情報を提供します。)
基本的な問題は、適切な同期がないと、メモリへの書き込みが異なるスレッドでどのように現れるかということです。古典的な例:
a = 1;
b = 2;
1つのスレッドでこれを行うと、2つ目のスレッドはbが2に設定されてからaが1に設定される可能性があります。更新され、他の変数が更新されます。
声明を前提とする場合、これを健全な観点から見る
if(n!= n)
アトミック(合理的だと思いますが、確かにわかりません)、アサーション例外がスローされることはありません。
この例は、「最後のフィールドを含むオブジェクトへの参照は、コンストラクターをエスケープしませんでした」の下にあります。
new演算子を使用して新しいHolderオブジェクトをインスタンス化すると、
- Java仮想マシンは最初に、Holderとそのスーパークラスで宣言されたすべてのインスタンス変数を保持するために、ヒープ上に(少なくとも)十分なスペースを割り当てます。
- 次に、仮想マシンはすべてのインスタンス変数をデフォルトの初期値に初期化します。 3.c 3番目に、仮想マシンはHolderクラスのメソッドを呼び出します。
上記を参照してください: http://www.artima.com/designtechniques/initializationP.html
仮定:第1スレッドは午前10:00に開始し、new Holer(42)を呼び出して、ホルダーオブジェクトをインスタンス化して呼び出します。 1)Java仮想マシンは、最初にHolderで宣言されたすべてのインスタンス変数とそのスーパークラスを保持するために、ヒープ上に(少なくとも)十分なスペースを割り当てます。 -10:01時間 2)次に、仮想マシンはすべてのインスタンス変数をデフォルトの初期値に初期化します-10:02に開始します 3)3番目に、仮想マシンはHolderクラスのメソッドを呼び出します。-10:04に開始します
現在Thread2は--- gt;で開始しました10:02:01時間であり、assertSanity()10:03の呼び出しを行います。その時間までに、nはデフォルトのゼロで初期化され、2番目のスレッドは古いデータを読み取ります。
//安全でない公開 パブリックホルダーホルダー;
公開の最終所有者にこの問題を解決させる場合
または
private int n;プライベート最終int nを作成する場合;この問題を解決します。
参照: http:// www。 cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html のセクションで、新しいJMMで最終フィールドはどのように機能しますか?
私もその例に非常に困惑していました。 私はトピックを徹底的に説明するウェブサイトを見つけました、そして、読者は役に立つかもしれません: https:// www.securecoding.cert.org/confluence/display/java/TSM03-J.+Do+not+publish+partially+initialized+objects
編集: リンクの関連テキストには次のように記載されています。
JMMは、コンパイラが新しいヘルパーにメモリを割り当てることを許可します オブジェクトとそのメモリへの参照をヘルパーフィールドに割り当てる 新しいヘルパーオブジェクトを初期化する前。言い換えれば、 コンパイラは、ヘルパーインスタンスフィールドへの書き込みを並べ替えることができます。 Helperオブジェクトを初期化する書き込み(つまり、this.n = n) 前者が最初に発生します。これにより、レースウィンドウが表示される可能性があります。 他のスレッドは、部分的に初期化されたヘルパーオブジェクトを監視できます。 インスタンス。