12

一部のOSS単体テストでこのコードが頻繁に見られますが、スレッドセーフですか?whileループは、invocの正しい値を確認することが保証されていますか?

いいえの場合; オタクは、これが失敗する可能性のあるCPUアーキテクチャを知っている人を指しています。

  private int invoc = 0;

  private synchronized void increment() {
    invoc++;
  }

  public void isItThreadSafe() throws InterruptedException {
      for (int i = 0; i < TOTAL_THREADS; i++) {
        new Thread(new Runnable() {
          public void run() {
             // do some stuff
            increment();
          }
        }).start();
      }
      while (invoc != TOTAL_THREADS) {
        Thread.sleep(250);
      }
  }
4

5 に答える 5

17

いいえ、スレッドセーフではありません。invocは、揮発性として宣言するか、同じロックで同期中にアクセスするか、AtomicIntegerを使用するように変更する必要があります。同期メソッドを使用してinvocをインクリメントするだけで、同期して読み取るのではなく、十分ではありません。

JVMは、CPU固有のキャッシングや命令の並べ替えなど、多くの最適化を行います。volatileキーワードとlockingを使用して、いつ自由に最適化できるか、他のスレッドが読み取るために最新の値を使用できるようにする必要があるかを決定します。したがって、リーダーがロックを使用しない場合、JVMは古い値を与えないことを知ることができません。

Java Concurrency in Practice (セクション3.1.3)からのこの引用では、書き込みと読み取りの両方を同期する必要がある方法について説明しています。

図3.1に示すように、固有のロックを使用して、あるスレッドが別のスレッドの影響を予測可能な方法で確認できるようにすることができます。スレッドAが同期ブロックを実行し、続いてスレッドBが同じロックで保護された同期ブロックに入ると、ロックを解放する前にAに表示されていた変数の値は、ロックの取得時にBに表示されることが保証されます。つまり、同期ブロック内または同期ブロックの前にAが実行したことはすべて、同じロックで保護された同期ブロックを実行するときにBに表示されます。同期がなければ、そのような保証はありません。

次のセクション(3.1.4)では、volatileの使用について説明します。

Java言語は、変数への更新が他のスレッドに予測どおりに伝播されることを保証するために、代替のより弱い形式の同期、揮発性変数も提供します。フィールドが揮発性であると宣言されると、コンパイラとランタイムは、この変数が共有され、その操作を他のメモリ操作と並べ替えてはならないことを通知します。揮発性変数は、レジスターや他のプロセッサーから隠されているキャッシュにはキャッシュされないため、揮発性変数の読み取りは、常に任意のスレッドによる最新の書き込みを返します。

デスクトップにシングルCPUマシンがあったときは、コードを記述し、通常は本番環境でマルチプロセッサボックスで実行するまで問題は発生しませんでした。CPUローカルキャッシュや命令の並べ替えなど、可視性の問題を引き起こす要因のいくつかは、マルチプロセッサマシンに期待されるものです。ただし、明らかに不要な命令の排除は、どのマシンでも発生する可能性があります。リーダーに変数の最新の値を表示させるようにJVMに強制するものは何もありません。あなたは、JVMの実装者に翻弄されます。したがって、このコードはどのCPUアーキテクチャにも適していないと思います。

于 2011-08-04T20:49:24.100 に答える
2

上手!

  private volatile int invoc = 0;

トリックを行います。

そして、Javaプリミティブintは設計上または偶然にアトミックですか?を参照してください。関連するJava定義のいくつかをサイトします。どうやらintは問題ないようですが、double&longは問題ないかもしれません。


編集、アドオン。質問は、「invocの正しい値を参照してください?」と尋ねます。「正しい値」とは何ですか?時空間の連続体のように、スレッド間に同時性は実際には存在しません。上記の投稿の1つは、値が最終的にフラッシュされ、他のスレッドがそれを取得することを示しています。コードは「スレッドセーフ」ですか?この場合、シーケンスの変動に基づいて「誤動作」しないため、「はい」と言います。

于 2011-08-04T20:55:05.127 に答える
1

理論的には、読み取りがキャッシュされる可能性があります。Javaメモリモデルにはそれを妨げるものはありません。

実際には、それが発生する可能性は非常に低いです(特定の例では)。問題は、JVMがメソッド呼び出し全体で最適化できるかどうかです。

read #1
method();
read #2

JVMがread#2がread#1の結果(CPUレジスタに格納できる)を再利用できると推論するにmethod()は、同期アクションが含まれていないことをJVMが確実に認識している必要があります。これは一般的に不可能です-method()インライン化されていない限り、JVMはフラット化されたコードからread#1とread#2の間に同期/揮発性または他の同期アクションがないことを確認できます。そうすれば、read#2を安全に排除できます。

今あなたの例では、メソッドはThread.sleep()です。これを実装する1つの方法は、CPU周波数に応じて、特定の時間ビジーループを実行することです。次に、JVMはそれをインライン化し、read#2を削除します。

しかしもちろん、そのような実装sleep()は非現実的です。これは通常、OSカーネルを呼び出すネイティブメソッドとして実装されます。問題は、JVMがそのようなネイティブメソッド全体で最適化できるかどうかです。

JVMがいくつかのネイティブメソッドの内部動作についての知識を持っていて、したがってそれら全体で最適化できる場合でも、sleep()そのように扱われることはありそうにありません。sleep(1ms)戻るまでに数百万のCPUサイクルが必要ですが、読み取りを数回節約するために最適化する意味はありません。

-

この議論は、データ競合の最大の問題を明らかにします-それについて推論するにはあまりにも多くの努力が必要です。プログラムが「正しく同期」されていなければ、必ずしも間違っているとは限りませんが、間違っていないことを証明するのは簡単な作業ではありません。プログラムが正しく同期され、データ競合が含まれていない場合、作業ははるかに簡単になります。

于 2011-08-05T11:44:56.253 に答える
0

私がコードを理解している限り、それは安全なはずです。はい、バイトコードは並べ替えることができます。しかし、最終的にはinvocはメインスレッドと再び同期するはずです。Synchronizeは、invocが正しくインクリメントされることを保証するため、一部のレジスタにinvocの一貫した表現があります。いつかこの値がフラッシュされ、小さなテストが成功します。

それは確かに良くありません、そして私は私が投票した答えで行き、それがにおいがするのでこのようなコードを修正するでしょう。しかし、それについて考えると、私はそれを安全だと思います。

于 2011-08-04T21:10:14.410 に答える
0

「int」を使用する必要がない場合は、スレッドセーフな代替手段としてAtomicIntegerをお勧めします。

于 2011-08-05T15:06:12.813 に答える