2 つのスレッドが基になるデータを同時に変更できないということですか? それとも、複数のスレッドがそのコード セグメントを実行しているときに、特定のコード セグメントが予測可能な結果で実行されるということですか?
17 に答える
スレッドセーフ コードは、多数のスレッドが同時に実行されている場合でも機能するコードです。
より有益な質問は、コードがスレッド セーフではない理由です。その答えは、真でなければならない 4 つの条件があるということです...次のコードを想像してください (機械語の翻訳です)。
totalRequests = totalRequests + 1
MOV EAX, [totalRequests] // load memory for tot Requests into register
INC EAX // update register
MOV [totalRequests], EAX // store updated value back to memory
- 最初の条件は、複数のスレッドからアクセスできるメモリ ロケーションがあることです。通常、これらの場所はグローバル/静的変数であるか、グローバル/静的変数から到達可能なヒープ メモリです。各スレッドは、関数/メソッド スコープのローカル変数用の独自のスタック フレームを取得するため、これらのローカル関数/メソッド変数 (スタック上にある) は、そのスタックを所有する 1 つのスレッドからのみアクセスできます。
- 2 番目の条件は、プログラムが正しく機能するためには、これらの共有メモリ位置に関連付けられているプロパティ (しばしばinvariantと呼ばれる) が true または有効である必要があることです。上記の例では、プロパティは「<em>totalRequests は、任意のスレッドがインクリメント ステートメントの任意の部分を実行した合計回数を正確に表す必要がある」ということです。通常、この不変プロパティは、更新が正しく行われる前に true を保持する必要があります (この場合、totalRequests は正確なカウントを保持する必要があります)。
- 3 番目の条件は、実際の更新の一部で不変プロパティが保持されないことです。(処理の一部で一時的に無効または false になります)。この特定のケースでは、totalRequests がフェッチされてから更新された値が格納されるまでの間、totalRequests は不変条件を満たしません。
- 競合が発生する (したがってコードが「スレッドセーフ」ではない) ために発生する必要がある 4 つ目の最後の条件は、不変条件が壊れている間、別のスレッドが共有メモリにアクセスできる必要があることです。不正な動作。
BrianGoetzのJavaConcurrencyin Practiceの定義は、その包括性が気に入っています。
「クラスは、ランタイム環境によるそれらのスレッドの実行のスケジューリングやインターリーブに関係なく、複数のスレッドからアクセスされたときに正しく動作し、呼び出し元のコードの一部で追加の同期やその他の調整がなくても、スレッドセーフです。 「」
他の人が指摘しているように、スレッド セーフとは、一度に複数のスレッドで使用された場合、コードの一部がエラーなしで機能することを意味します。
これには、コンピューター時間とより複雑なコーディングが必要になる場合があるため、常に望ましいとは限らないことに注意してください。クラスが 1 つのスレッドでのみ安全に使用できる場合は、その方がよい場合があります。
たとえば、Java にはほぼ同等の 2 つのクラスStringBuffer
とStringBuilder
. 違いは、StringBuffer
がスレッドセーフであるため、 の 1 つのインスタンスをStringBuffer
複数のスレッドで同時に使用できることです。StringBuilder
はスレッドセーフではなく、String が 1 つのスレッドのみによって構築される場合 (大多数) のより高性能な代替として設計されています。
それを理解するためのより簡単な方法は、コードをスレッドセーフではないものにすることです。スレッド化されたアプリケーションが望ましくない動作をするようにする2つの主要な問題があります。
ロックせずに共有変数にアクセスする
この変数は、関数の実行中に別のスレッドによって変更される可能性があります。関数の動作を確認するために、ロックメカニズムでそれを防ぎたいと考えています。一般的な経験則は、可能な限り短い時間ロックを維持することです。共有変数への相互依存によって引き起こされるデッドロック
2つの共有変数AとBがある場合。ある関数では、最初にAをロックし、後でBをロックします。別の関数では、Bのロックを開始し、しばらくするとAをロックします。 2番目の関数がAのロック解除を待機するときに、最初の関数がBのロック解除を待機する潜在的なデッドロックです。この問題は、開発環境ではおそらく発生せず、たまにしか発生しません。これを回避するには、すべてのロックが常に同じ順序である必要があります。
スレッド セーフ コードは、異なるスレッドから同時に入力された場合でも、指定どおりに機能します。これは多くの場合、中断せずに実行する必要がある内部データ構造または操作が、同時に異なる変更から保護されることを意味します。
簡単に言えば、多くのスレッドがこのコードを同時に実行している場合、コードは正常に実行されます。
はいといいえ。
スレッド セーフとは、共有データが一度に 1 つのスレッドだけにアクセスされるようにするだけではありません。競合状態、デッドロック、ライブロック、およびリソース不足を回避しながら、共有データへのシーケンシャル アクセスを確保する必要があります。
複数のスレッドが実行されているときの予測不可能な結果は、スレッドセーフ コードの必須条件ではありませんが、多くの場合副産物です。たとえば、共有キュー、1 つのプロデューサー スレッド、および少数のコンシューマー スレッドを使用してプロデューサー/コンシューマースキームをセットアップすると、データ フローは完全に予測可能になる場合があります。より多くの消費者を紹介し始めると、よりランダムに見える結果が表示されます。
スレッド セーフと決定論を混同しないでください。スレッドセーフ コードは、非決定論的である場合もあります。スレッド化されたコードの問題をデバッグするのが難しいことを考えると、これはおそらく通常のケースです。:-)
スレッド セーフは、スレッドが共有データを変更または読み取りしているときに、データを変更する方法で他のスレッドがアクセスできないようにするだけです。コードが正確さのために特定の実行順序に依存している場合、これを保証するには、スレッド セーフに必要な同期メカニズム以外の同期メカニズムが必要です。
本質的に、マルチスレッド環境では多くのことがうまくいかない可能性があります (命令の並べ替え、部分的に構築されたオブジェクト、CPU レベルでのキャッシュのため、異なるスレッドで異なる値を持つ同じ変数など)。
Java Concurrency in Practice で与えられた定義が気に入っています。
[コードの一部] は、ランタイム環境によるこれらのスレッドの実行のスケジューリングまたはインターリーブに関係なく、複数のスレッドからアクセスされたときに正しく動作し、追加の同期やその他の調整が行われない場合、スレッドセーフです。コードを呼び出します。
正しくは、プログラムがその仕様に準拠して動作することを意味します。
工夫された例
カウンターを実装することを想像してください。次の場合、正しく動作すると言えます。
counter.next()
以前にすでに返された値を返すことはありません (簡単にするために、オーバーフローなどはないと仮定します)- ある段階で 0 から現在の値までのすべての値が返されました (値はスキップされません)
スレッド セーフ カウンターは、同時にアクセスするスレッドの数に関係なく、これらのルールに従って動作します (これは通常、素朴な実装の場合ではありません)。
他の良い答えに加えて、さらに情報を追加したいと思います。
スレッド セーフとは、複数のスレッドがメモリの不整合エラーなしで同じオブジェクトにデータを読み書きできることを意味します。高度にマルチスレッド化されたプログラムでは、スレッドセーフなプログラムは共有データに副作用を引き起こしません。
詳細については、この SE の質問をご覧ください。
スレッドセーフなプログラムはメモリの一貫性を保証します。
高度な並行 API に関するOracle ドキュメントページから:
メモリ一貫性プロパティ:
The Java™ Language Specification の第 17 章では、共有変数の読み取りや書き込みなどのメモリ操作に関する事前発生関係が定義されています。あるスレッドによる書き込みの結果は、書き込み操作が読み取り操作の前に発生した場合にのみ、別のスレッドによる読み取りに表示されることが保証されます。
synchronized
andコンストラクトは、 andメソッドとvolatile
同様に、先行発生関係を形成できます。Thread.start()
Thread.join()
java.util.concurrent
およびそのサブパッケージのすべてのクラスのメソッドは、これらの保証をより高いレベルの同期に拡張します。特に:
- オブジェクトを並行コレクションに配置する前のスレッド内のアクションは、別のスレッド内のコレクションからのその要素へのアクセスまたは削除に続くアクションの前に発生します。
- への送信前のスレッド内のアクションは、その実行が開始される前に発生します
Runnable
。Executor
に送信された Callable についても同様ですExecutorService
。 - 別のスレッド
Future
を介して結果を取得した後に起こる前発生アクションによって表される、非同期計算によって実行されるアクション。Future.get()
- 別のスレッドの同じシンクロナイザー オブジェクトなどで、成功した「取得」メソッドの後に発生する前発生アクションなど、シンクロナイザーメソッドを「解放」する前のアクション。
Lock.unlock, Semaphore.release, and CountDownLatch.countDown
Lock.lock, Semaphore.acquire, Condition.await, and CountDownLatch.await
- を介してオブジェクトを正常に交換するスレッドのペアごとに、各スレッド
Exchanger
の の前のアクションはexchange()
、別のスレッドの対応する exchange() の後のアクションの前に発生します。 - 呼び出し前のアクション
CyclicBarrier.await
とPhaser.awaitAdvance
(およびそのバリアント) バリア アクションによって実行される事前発生アクション、およびバリア アクションによって実行されるアクションは、他のスレッドの対応する await から正常に返された後に発生します。
他の回答を完了するには:
同期は、メソッド内のコードが次の 2 つのいずれかを実行する場合にのみ問題になります。
- スレッドセーフではない外部リソースで動作します。
- 永続オブジェクトまたはクラス フィールドの読み取りまたは変更
これは、メソッド内で定義された変数は常にスレッドセーフであることを意味します。メソッドへのすべての呼び出しには、これらの変数の独自のバージョンがあります。メソッドが別のスレッドまたは同じスレッドによって呼び出された場合、またはメソッド自体が呼び出された場合 (再帰)、これらの変数の値は共有されません。
スレッド スケジューリングは、ラウンド ロビンであるとは限りません。タスクは、同じ優先度のスレッドを犠牲にして、CPU を完全に占有する可能性があります。Thread.yield() を使用して良心を持つことができます。(Javaで) Thread.setPriority(Thread.NORM_PRIORITY-1) を使用して、スレッドの優先度を下げることができます
さらに、次の点に注意してください。
- これらの「スレッドセーフ」構造を反復処理するアプリケーションの大きなランタイム コスト (既に他の人が言及している)。
- Thread.sleep(5000) は 5 秒間スリープすることになっています。ただし、誰かがシステム時刻を変更すると、非常に長い時間スリープ状態になるか、まったくスリープ状態にならない可能性があります。OS は、ウェイクアップ時間を相対的ではなく絶対的な形式で記録します。
はい、はい。これは、データが複数のスレッドによって同時に変更されないことを意味します。ただし、根本的にそうでなくても、プログラムが期待どおりに動作し、スレッドセーフに見える場合があります。
結果の予測不可能性は、予想とは異なる順序でデータが変更される可能性がある「競合状態」の結果であることに注意してください。
簡単に言えば:Pコードブロックで複数のスレッドを安全に実行できる場合、それはスレッドセーフです*
※条件あり
条件は、1のような他の回答で言及されています。1つのスレッドまたは複数のスレッドを実行した場合など、結果は同じになるはずです。