-4

次のコードは、テスト用に作成されています。このテストでは、読者は、コードを開始してから 1 秒以内にコードがデッドロックに入る理由を説明するよう求められます。

このコードのデッドロックの原因を正確に説明できる人はいますか?

public class Test {

  static class FailerThread implements Runnable {

    final Object[] objects;
    final Random random;
    final int number;

    public FailerThread(final Object[] objects, final int number) {
      this.objects = objects;
      this.random = new Random();
      this.number = number;
    }

    @Override
    public void run() {
      final boolean isWriter = number % 2 == 0;
      int index = random.nextInt(objects.length);
      try {
        while (Thread.interrupted() == false) {
          synchronized (objects) {
            if (isWriter) {
              while (objects[index] == null) {
                System.out.println(number + ": Index " + index + " is null, waiting...");
                objects.wait();
              }
              for (int copyIndex = 0; copyIndex < objects.length; ++copyIndex) {
                if (objects[copyIndex] == null) {
                  objects[copyIndex] = this.objects[index];
                }
              }
              objects.notifyAll();
            } else {
              objects[index] = null;
            }
          }

          ++index;
          if (index >= objects.length) {
            index = 0;
          }
        }
      } catch (InterruptedException e) {
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    final Object[] objects = new Object[10];
    for (int i = 0; i < objects.length; ++i) {
      objects[i] = new Object();
    }

    final int NUM_THREADS = 32;
    final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
    for (int i = 0; i < NUM_THREADS; ++i) {
      executor.execute(new FailerThread(objects, i));
    }
  }
}

編集:このテストに対する公式の回答(チューダーが書いたものに似ていますが、より詳細です)

上記の構成では、ある時点ですべての「ライター」が null を待機するため、デッドロックが発生しますが、これらのライターはそれらを解放できる唯一のライターであるため、無期限にハングします。ただし、より重要な質問は次のとおりです。なぜですか?

一見すると、コードはこれらのライターが優勢であるように見えます。各ループでは、単一のスレッド (ライターまたは nuller) が選択されて配列で処理されますが、nuller は 1 つの null のみを書き込みますが、ライターは配列内のすべての null を削除します。したがって、デッドロックが発生する可能性は非常に低いと考えられます (ただし、驚くべきことに、コードは 1 秒以内にデッドロックします)。しかし、よく見ると、スレッドを扱っているため、この仮定は誤りであることがわかります。

十分な実行時間が与えられた場合、マルチスレッド アプリケーションで問題になるのは、コードのどの部分を実際にブロックできるかということです。ライター/ヌラーで起こりうる最悪のシナリオを見てみましょう。

  • nuller は、最悪の場合、何の影響もなく実行できます。つまり、すでに null である配列内の位置に null を書き込みます。

  • ライターは、最悪の場合、無期限にブロックする可能性があります。

さらに、同期ブロックの開始時に、(多かれ少なかれ) ランダムな候補が選択されて入力されます。最初は、これはライターと nuller の両方で 50% ですが、ブロックされた各ライターについては、nuller の方向に有利になります。書き込みが成功するとすべての null が除去されますが、nuller の可能性は常に 50% 以上になります。したがって、スレッド化された観点からは、システム全体が同期ブロックの候補としてヌラーを優先するように設計されているため、実際にはヌラーが支配的な部分です。

さらに、これが重要な部分ですが、スレッドの実行順序は定義されていません。どのスレッドが代替の実行を許可されているかというのは素朴な印象ですが、そうではありません。同期されたブロックには設定がなく、どのスレッドがアクセスを取得するかは定義されていません (完全にランダムであると言えますが、ランダムは関係ありません)。したがって、16 個のスレッドすべてが同期を待機している場合、20 回の実行内でスレッドが完全に交互に実行される可能性は、20 個のライターまたは 20 個のヌラーが連続して呼び出される可能性とまったく同じです。しかし、nuller が支配的であるため (20 個のライターが何もしない)、20 個の nuller を続けて呼び出すと、配列全体が null に設定されることがほぼ保証され、その後のライターは無期限にブロックされます。

コードにログ出力を追加して、実際に選択されているスレッドを確認すると、通常は最初の 200 ループ内で、10 個以上のヌラーが連続して呼び出されていることがすぐにわかります。その直後、システムがハングします。

この質問をした理由

私は現在、熟練した Java プログラマー向けの評価用のテスト セットを開発しており、すべてのコードを記述して、最終的にテストする必要があります。良いニュース: 成功しました。;)

StackOverflow の不適切な使用について文句を言う前に、こちらを Q&A としてご覧ください。そして、マルチスレッド アーキテクチャを実際に実装するために、この例から学ぶべきことがたくさんあります。これは専門家レベルの質問であるため、予想どおり、多くの人が回答できず、理解することさえできませんでした。ただし、専門家レベルの質問の良い点は、専門家レベルの回答から多くを学ぶことができることです。そのため、完全に詳細な回答を含めました。

候補者の評価方法

この質問は評価テストには難しすぎると考える人がいると予想し、テスターの視点を与えるために、候補者は次のように評価されます。

はい、問題は難しすぎます。テスト中に誰も正しい答えを見つけることは期待されていません。問題は、問題にどのように取り組むかです。プログラマーは毎日、これまでに解決したことのないタスクに遭遇し、すぐに解決する方法がわからないため、このビジネスでは問題解決の優れたスキルを持っていることが重要です。誰もすべてを知ることはできませんが、誰もが学ぶことができます。

一般に、4 つの可能な結果があります。

  1. 候補者は答えを知らず、そう言う。これは、受験者がストレスの多いテスト状況でそれを認める強さを持っているため、初心者に適したレベルです。良い生徒は耳を傾ける生徒であり、したがって教えられる生徒です。

  2. 候補者は答えを知っていますが、「悪い」質問を非難するか(別名反対票)、間違った答えを思いつき、それを猛烈に擁護します。これは基本的に最悪の候補者です。彼は初級/中級レベルですが、自分は専門家であると考えているため、学習を拒否し、このレベルで行き詰まるでしょう。チーム内では、この候補者はチームの昇進を妨害するか (彼が「専門家」であると彼らが信じている場合)、すぐに厄介者になります。

  3. 候補者は (多かれ少なかれ正しい) 答えを思いつき、それを見つけるために系統的なアプローチを使用します。これは、中級/エキスパート レベルの優れた候補です。彼/彼女は困難な課題に対して整然としたアプローチを開発しており、答えによってはさらに前進することが期待できます。

  4. 候補者は系統的なアプローチを使用して、正しい答えを導き出します。これは可能な限り最良の結果ですが、おそらく 100 万分の 1 にすぎません。

4

3 に答える 3

0

これがあなたが期待している答えかどうかはわかりませんが、次の 2 つの条件が満たされるとデッドロックが発生することがわかります。

  1. 少なくとも 10 人の「リーダー」(非ライター) が、ライターの進行を許可せずに、連続して同期ブロックに入ることができます。
  2. 0 から 9 までの各配列インデックスは、ロックを通過するリーダーの 1 つによって少なくとも 1 回はランダムに取得されます。

16 のリーダーと 16 のライターがあり、上記が適用されるため、乱数ジェネレーターで 0 から 9 を取得する 10 のリーダーが配列全体を null にする可能性があり、対応するインデックスが null になるまでにすべてのライターがブロックされる可能性があります。whileループに入る。

編集: 実際には、それよりもさらに単純です。10 人のリーダーが連続してロックに入る必要さえありません。K 人のリーダーがロックに入り、配列内の i 番目の位置が null である場合0 < i <= K(インデックスが重複する可能性があるため)、それらの後に来るライターのすべてが前のリーダーによって使用されたセットにインデックスを持っている場合、それらはブロックされます。リーダーは最終的に配列全体を無効にするため、説明されている状況が繰り返されると、すべてのライターが有限回の繰り返しでブロックされる可能性があります。

于 2013-11-22T18:51:44.800 に答える
-1

質問者が投稿した「公式の回答」は多くの点で間違っています。今後この質問に出くわす人のために別の回答を投稿します。

そして、誰かが私が「最悪のケース」の候補者であり、「間違った答えを猛烈に擁護する」と主張し始める前に(ポスターが暗示しているように):

  • ロギングとブレークポイントのデバッグを行いました (元のポスターとは明らかに異なります。または、彼はこれに十分な時間を割いていないだけかもしれません)。
  • 私は候補者ではありません。意図したとおりに Stackoverflow を使用しようとしているだけです。質問者の利益のためでなくても、訪問する他の人のためにできる限りの最善の回答を提供します。
  • そして、私がこれを行うのは、元の投稿者の回答から、他の人が同期とは何かについて誤った考えを得るからです。
  • 質問に反対票を投じたのは私ではありませんでしたが、皮肉なことに元の投稿者が私の答えに反対票を投じました

言うまでもなく、基本に関する誤った仮定で「専門家」レベルの質問に答えても、正しい答えは得られません。

同期

答えのポスターでは、ヌラーが単一の値のみを書き込むことを何度か言及しています。

ただし、ヌラーは単一のヌルのみを書き込みますが、ライターは配列内のすべてのヌルを削除します。

nuller は、最悪の場合、何の影響もなく実行できます。つまり、すでに null である配列内の位置に null を書き込みます。

それを真にするコードは何もありません。

同期句を残すだけで、スレッドが別のスレッドの実行を放棄することはありません。

同期の唯一の目的は、2 つのスレッドが同時に同じクリティカル セクションに入らないことを保証することです。基本的に、これは単なるミューテックスです。

固有のロックと同期

相互排除

Java には、スレッドの制御を停止させる方法がいくつかあります。

  • Thread.yield () -別のスレッドに実行を与える可能性があることを jvm に示唆します。基本的には、jvmに「ここでたくさんの作業を行いました。必要に応じて他の人にプロセッサ時間を与えることができますが、続行することもできます。」
  • Thread.sleep() - スレッドを一定時間中断します。
  • Object.wait() - 同じオブジェクトで、誰かが notify (1 つのウェイターのみをウェイクする)/notifyAll (すべてのウェイターをウェイクする) を呼び出すまで、スレッドを一時停止します。
  • 他にもありますが、それらは類似しており、通常はこれらに基づいています

しかし、どれも nuller には使用されません! したがって、nuller が 1 つの値のみを書き込むという保証はまったくありません。

実際、1 回の反復の計算強度が低いため、スレッドが受け取る実行ウィンドウ中に複数の反復を実行できる可能性が高くなります。

しかし、繰り返しますが、私を信じる必要はありません。ログとデバッグを行うだけです。実際のテストがなければ、それはすべて単なるデマゴジーです。(更新:ログは同期ブロック内にある必要があることに注意してください)

プログラムの出力のスクリーンショット。nuller が連続して複数の値をリセットし、1 つのライターがすべての値を上書きしてから nuller がすべての要素をリセットし、問題で説明されている問題が発生する様子を示しています。 最初の nuller は、ライターによって中断されることなく (ポスターの言うこととは逆に) 行内のいくつかの値をリセットし、次に writer は配列を上書きし、次に nuller はすべての配列をリセットします (一度にいくつかの要素)。問題。

更新:さらに著者は言う

さらに、これは重要な部分ですが、スレッドの実行順序は定義されていません。どのスレッドが代替の実行を許可されているかというのは素朴な印象ですが、そうではありません。

しかし、nuller を実行した後、そのスレッドが同期に入るスレッドではないことを期待することで、自分自身がその「素朴な印象」の犠牲者になります。

確率主導の開発

書き込みが成功するとすべての null が除去されますが、nuller の可能性は常に 50% 以上です。

それは確率論全体を窓の外に放り出すだけであり、確率に基づいてコードを書くことは本当に悪い考えであることは言うまでもありません.マルチスレッドと長い実行時間(無限猿定理)を伴う場合は特にそうです.

ただし、1 つの点では著者は正しい: 時間が経つにつれて、より多くの nuller とより少ないライターが存在するようになります。

更新:それが重要な理由は、確率の観点からマルチスレッドについてまったく話すべきではないためです.

マルチスレッドは、著者が最初に述べたように、最悪のシナリオの観点から議論する必要がありますが、彼は nuller の実際の最悪のシナリオ、つまり、単一の nuller スレッドが中断されることなくすべての配列を反復し、すべての値を null に設定する場合を特定できませんでした。

更新: 50/50 のチャンスがどのように不正確に分割されるかを示すために、次の単純化された例を検討してください。

2 つのスレッドがあり、それらに優先度を設定していないため、両方ともデフォルトで java.lang.Thread.NORM_PRIORITY を持っているとしましょう。スケジューラは、その能力を最大限に発揮するために、プロセッサー時間をそれらの間でほぼ均等に分割しようとします。

ただし、1 つのスレッドが大きな配列を反復処理するため、1 分ほどかかります。
もう 1 つは、配列の 1 つの要素を設定するだけで、1 秒かかります。
また、両方とも 1 つのオブジェクトで同期するため、同時に実行することはできません。

物乞いでは、スケジューラーが最初のスレッドに制御を渡し、配列の反復処理を開始します。スケジューラーがそれを中断して 2 番目のスレッドに時間を与えようとしても、最初のスレッドが既にロックを取得しているため、2 番目のスレッドは続行できません。

したがって、1 分が経過して最初のスレッドがロックを解放すると、スケジューラはそれで十分であると判断し、2 番目のスレッドの優先度が同じであるため、2 番目のスレッドに 1 分程度の時間を与える必要がありますが、クリティカル セクションの 2 番目のスレッドは 1 秒しかかからないため、 〜60回入力できます。

もちろん、例は単純化されており、ジッターが発生し、スケジューラーが不均等な時間のチャンクを与えることもありますが、全体としては、優先度に応じてスレッドにプロセッサー時間を与えようとします。

したがって、古い逸話に似た「50% のケースでは、ライターと nuller の数が等しいため、ライターがクリティカル セクションに入る」という推論:

- What's the probability that leaving your home you will see an alive dinosaur?
- 50% either I will see it or not.

于 2013-11-24T09:17:50.290 に答える