いつの時代も、学ぶことができる人は不当な扱いを受けません。
最近、会社で新しい同僚がロックに関して誤解をしていることに気づきました。そこで今日は「ロック」について、そして Java でセキュリティ コンテナを並行して使用する際に注意すべき点について話しましょう。
しかし、その前に、なぜこれをロックする必要があるのかを説明する必要があり、同時実行バグの原因から始める必要があります。
私は 2019 年にこの問題について記事を書きました。今その記事を振り返ると、本当に恥ずかしいです。 。
このソースが何であるかを見てみましょう。コンピューターには CPU、メモリ、ハードディスクが搭載されていることがわかります。ハードディスクの読み取り速度は次のとおりです。メモリの読み取り速度が CPU の動作に比べて遅すぎるため、CPU キャッシュが L1、L2、L3 に構築されます。
この CPU キャッシュとマルチコア CPU の現状が同時実行バグ を生み出します。
これは非常に単純なコードです。CPU-A と CPU-B でそれぞれこのメソッドを実行するスレッド A とスレッド B がある場合、それらの操作は次のようになります。メインキャッシュから各CPUのキャッシュにアクセスしますが、このとき各キャッシュ内のaの値は全て0になります。
次に、a を別々に実行します。このとき、それぞれの目での a の値は 1 です。その後、a がメイン メモリにフラッシュされても、a の値は 1 のままです。これは問題です。明らかに 2 回実行されており、1 を加算した最終結果は 2 ではなく 1 になります。
この問題は可視性問題と呼ばれます。
ステートメント a を見ると、現在の言語はすべて高級言語です。これは実際には糖衣構文に非常によく似ています。非常に使いやすそうです。実際、それは表面にすぎません。 . 実行する必要がある命令は 1 つもありません。
高級言語のステートメントは複数の CPU 命令に変換できます。たとえば、 a は少なくとも 3 つの CPU 命令に変換できます。
メモリからレジスタに a を取得;
レジスタ 1;
結果をキャッシュまたはメモリに書き込みます;
したがって、このステートメント a は Atomic を持っているため中断できないと考えられますが、実際、タイム スライスが終了すると、CPU は命令を実行することがあります。このとき、コンテキストは別のスレッドに切り替わり、そこでも が実行されます。再び元に戻すと、実際には a の値が間違っています。
この問題は原子性問題と呼ばれます。
また、パフォーマンスを最適化するために、コンパイラまたはインタプリタはステートメントの実行順序を変更することがあります。これは命令の再配置と呼ばれます。最も典型的な例は、シングルトン モードの二重チェックです。 CPUは、実行効率を高めるために、メモリデータのロードを待っているときに、次の加算命令が前の命令の計算結果に依存していないことがわかるなど、アウトオブオーダーで実行します。最初に加算命令を実行します。
この問題は順序問題と呼ばれます。
これまでのところ、同時実行性のバグの原因、つまりこれら 3 つの主要な問題を分析してきました。 CPUキャッシュであれ、マルチコアCPUであれ、高級言語であれ、アウトオブオーダー再配置であれ、実際には必要であることが分かるので、これらの問題には正面から向き合うしかありません。
これらの問題を解決するには、キャッシュを無効にする、コンパイラ命令の再配置、相互排他を禁止するなどがあります。今日のトピックは相互排他に関連しています。
相互排他とは、共有変数への変更が相互に排他的であること、つまり、同時に 1 つのスレッドのみが実行されることを保証することです。相互排他というと誰もが思い浮かべるのは lock だと思います。はい、今日のテーマはロックです!ロックは原子性の問題を解決するように設計されています。
ロックのことになると、Java 学生の最初の反応は synchronized キーワードです。言語レベル。まず同期について見てみましょう 同期についてよく理解していない学生もいるため、使用する際には多くの落とし穴があります。
まずコードを見てみましょう。このコードは賃金を増やす方法です。最終的には数百万ドルが支払われます。そして、スレッドは常に私たちの賃金が等しいかどうかを比較します。 IntStream.rangeClosed(1,1000000).forEach
について簡単に説明します。馴染みのない人もいるかもしれませんが、このコードは 100 万回の for ループに相当します。
まずはご自身で理解して問題ないか確認してみてはいかがでしょうか?最初の反応は問題ないようですが、給与の増加を見ると、1 つのスレッドで実行されており、給与の値は変更されていません。同時リソースの競合はなく、可視性を確保するために volatile で装飾されています。
結果を見てみましょう。スクリーンショットを撮りました。
第一に、ログが正しく出力されていないことがわかります。第二に、出力された値は依然として等しいです。!何か驚いたことはありますか?一部の学生は無意識のうちに raiseSalary
が変更されていると考えている可能性があるため、これはスレッドの安全性の問題に違いなく、raiseSalary
にロックを追加する必要があります。
raiseSalary
メソッドを呼び出しているのは 1 つのスレッドだけであるため、raiseSalary
メソッドを単独でロックしても意味がないことに注意してください。
これは、実際には上で述べた原子性の問題です。昇給スレッドが yesSalary
の実行を終了したが、まだ yourSalary
を実行していない後、給与スレッドが # を実行したところだと想像してください。 # #yesSalary != yourSalary それは間違いなく本当ですか?そのため、ログが出力されます。
yourSalary が実行されている可能性があり、このときのログ出力は
yesSalary = = yourSalary となります。
raiseSalary() と
compareSalary() の両方を同期して変更することです。同時にできるので、間違いなく安全です!
Parallel についてもう一度説明します。これは実際に ForkJoinPool スレッド プール操作を使用します。デフォルトのスレッド数は CPU コアの数です。
raiseSalary()
はロックを追加するため、最終結果は正しいです。これは、同期によって yesLockDemo
インスタンスが変更されるためです。メインにはインスタンスが 1 つしかないため、マルチスレッドが 1 つのロックを競合するため、最終的に計算されたデータは正確になります。
次に、給与を増やすために各スレッドに独自の yesLockDemo インスタンスが存在するようにコードを変更します。
#このロックが役に立たない理由がわかるでしょう?約束の年俸100万が10万に変更? ?幸いなことに、まだ70ワットあります。 これは、現時点ではロックがインスタンス レベルのロックである非静的メソッドを変更しているためです。 また、スレッドごとにインスタンスを作成しているため、これらのスレッドが競合します。まったくロックではありません。上記のマルチスレッド計算の正しいコードは、各スレッドが同じインスタンスを使用するため、ロックをめぐって競合するためです。現時点でのコードを正しくしたい場合は、 インスタンス レベルのロックをクラス レベルのロック に変更するだけです。 これは非常に簡単です。このメソッドを静的メソッドに変えるだけです。
synchronized 静的メソッドの変更はクラスレベルのロックです。静的フィールドと静的メソッドを変更する場合、それはクラスです。レベル ロック。非静的フィールドおよび非静的メソッドの変更がインスタンス レベルのロック
の場合。 Hashtable が推奨されないことは誰もが知っていると思いますが、Hashtable はスレッドセーフですが、使用したい場合は ConcurrentHashMap を使用してください。それはとてもひどいもので、すべてのメソッドに同じロックがかけられます。ソースコードを見てみましょう。 この内容はサイズメソッドと何の関係があると思いますか? contains を呼び出すときにサイズを調整できないのはなぜですか? これは、ロックの粒度が粗すぎるためです。評価する必要があります。スレッド セーフでの同時実行性を向上させるために、メソッドごとに異なるロックが使用されます。 しかし、メソッドごとに異なるロックを設定するだけでは十分ではありません。メソッド内の一部の操作が実際にはスレッドセーフである場合があるためです。競合するリソースに関係するコードのみをロックする必要があります。特に、次のコードのように、ロックを必要としないコードが非常に時間がかかる場合、そのコードが長時間ロックを占有し、他のスレッドはインラインで待機することしかできません。 コードの 2 番目の部分はロックを使用する通常の方法であることは明らかですが、通常のビジネス コードでは、コードに投稿したスリープほど簡単ではありません。ご覧のとおり、ロックの粒度を十分に細かくするために、コードの実行順序などを変更する必要がある場合があります。 場合によっては、ロックが十分に厚いことを確認する必要がありますが、次のコードのように、JVM のこの部分が検出され、最適化に役立ちます。 メソッドで呼び出されたロジックが したがって、JVM は、次の状況と同様に、ジャストインタイム コンパイル中にロックを粗くし、ロックの範囲を拡大します。 そして、JVM は ロック削除アクションも実行し、エスケープ分析を通じてインスタンス オブジェクトがスレッド プライベートであることを判断します。スレッド セーフであるため、オブジェクト内のロック アクションは無視され、直接呼び出されます。 読み取り/書き込みロックは、シナリオに従ってロックの粒度を下げるために上記で送信したものです。 ロックをかける 読み取りロックと書き込みロックに分かれており、特に自分で実装したキャッシュなど、読み取りが多く書き込みが少ない場合の使用に適しています。 複数のスレッドによる読み取りを許可します。同時に変数 を共有しますが、書き込み操作は相互に排他的です。つまり、書き込みと読み取りは相互に排他的です。単刀直入に言うと、書き込み時は 1 つのスレッドのみが書き込み可能で、他のスレッドは読み書きできません。 data = getFromCache() もちろん、Lock の使用パラダイムは誰もが知っていますが、ロックが確実に解除されるようにするには、 ロックのダウングレードを実現できます。書き込みロックが追加されたかどうか疑問に思う人もいるかもしれません。 . 読み取りロックが必要なものは何ですか? 現時点では、読み取りロックはまだ保持されています。書き込みロック操作後のデータはすぐに使用できることが保証されており、この時点では書き込みロックが解除されているため、他のスレッドもデータを読み取ることができます。 実際、書き込みロックのような、より強力なロックは必要ありません。そこで、誰でも読めるようにダウングレードしましょう。 要約すると、 Lock のロックは、確実にロックが解除されるように try-finally と連携する必要があります。 AQS に慣れている学生なら内部の状態を知っているかもしれません。読み取り/書き込みロックは int を分割します。タイプ状態を 2 つの半分に分割し、上位 16 ビットと下位 16 ビットにそれぞれ読み取りロックと書き込みロックのステータスが記録されます。 通常のミューテックス ロックとの違いは、これら 2 つの状態を維持する必要があることと、待機キューで 2 つのロックが異なる方法で処理される必要があることです。 したがって 。読み取り/書き込みロックは状態などのディスプレイスメント判定も実行する必要があるためです。オペレーション。 上記の分析から、読み取り/書き込みロックは に書き込むことができることがわかります。オプティミスティック読み取りは、実際には私たちが知っているデータベースのオプティミスティックロックと同じであり、データベースのオプティミスティックロックは、次のSQLのようなバージョンフィールドによって判断されます。 StampedLock Optimistic Reading も同様ですが、その簡単な使用法を見てみましょう。 ここでは ReentrantReadWriteLock と比較します。他のものは良くありません。たとえば、StampedLock は再入可能性をサポートしておらず、条件変数もサポートしていません。 もう 1 つのポイントは、StampedLock を使用する場合は、CPU が 100% になるため、割り込み操作を呼び出さないことです。 コンカレント プログラミング Web サイトで提供されているサンプルを実行して再現しました。 #具体的な理由については、ここでは詳しく説明しません。記事の最後にリンクを掲載します。上記は非常に詳細です。 したがって、何かが強力であると思われる場合、ターゲットにするにはそれを本当に理解し、精通している必要があります。 コピーオンライトは、プロセス たとえば、Java での実装 最初に、コピーオンライトはデータのコピーをコピーしますと、行う変更アクションは 最後に、比較的馴染みのある ConcurrentHashMap を例として、同時セキュリティ コンテナの使用について説明します。新しい同僚は、同時安全コンテナを使用する限り、スレッド安全でなければならないと考えているようです。実際には、必ずしもそうとは限りませんが、使い方によって異なります。 まず次のコードを見てみましょう。簡単に言うと、ConcurrentHashMap を使用して全員の給与 (最大 100) を記録します。 最終結果は標準を超えます。つまり、マップに記録されているのは 100 人だけではありません。では、どうすれば結果が正しくなるでしょうか?ロックを追加するのと同じくらい簡単です。 これを見て、ロックされているのに、なぜ ConcurrentHashMap を使用する必要があるのかと言う人もいます。HashMap にロックを追加するだけで大丈夫です。はい、その通りです!現在の使用シナリオは複合操作であるため、つまり、最初にマップのサイズを判断してから put メソッドを実行します。ConcurrentHashMap は複合操作がスレッドセーフであることを保証できません! ConcurrentHashMap は、複合操作ではなく、それによって公開されるスレッドセーフなメソッドにのみ適しています。たとえば、次のコード もちろん、私の例は十分に適切ではありませんが、実際には、ConcurrentHashMap のパフォーマンスが HashMap ロックよりも高い理由は、セグメンテーション ロックによるものです。複数のキー操作を反映させる必要があるのですが、ここで注意したいのは、「これを使えばスレッドセーフになる」という安易な考えではなく、不用意に使用してはいけないということです。 今日は、同時実行性のバグの原因、つまり、可視性の問題、原子性の問題、順序性の問題という 3 つの主要な問題について話しました。 。次に、synchronized キーワードの重要なポイントについて簡単に説明しました。つまり、静的フィールドまたは静的メソッドの変更はクラス レベルのロックであり、非静的フィールドおよび非静的メソッドの変更はインスタンス レベルのクラスです。 ロックの粒度について話しましょう。さまざまなシナリオでさまざまなロックを定義することは、1 つのロックだけでは実行できず、メソッドの内部ロックの粒度は適切である必要があります。たとえば、読み取りと書き込みが大量に行われるシナリオでは、読み取り/書き込みロック、コピーオンライトなどを使用できます。 最終的には、同時セーフティ コンテナを正しく使用する必要があります。同時セーフティ コンテナの使用がスレッド セーフである必要があると盲目的に考えることはできません。複合操作のシナリオに注意を払う必要があります。 もちろん、今日は簡単に説明しただけです。同時実行プログラミングについては、実際には多くのポイントがあります。以前と同じように、スレッドセーフなコードを書くのは簡単ではありません。プロセス全体Kafka のイベント処理を分析したところ、同様でした。オリジナルのバージョンは同時実行性のセキュリティを制御するためのさまざまなロックだけでした。その後、バグはまったく修正されませんでした。マルチスレッド プログラミングは困難で、デバッグも困難で、バグ修正も困難でした。 したがって、Kafka イベント処理モジュールは最終的に シングルスレッド イベント キュー モード、共有データ競合に関連するアクセスをイベントに抽象化し、そのイベントをブロッキング モジュールに詰め込むようになりました。 queue 、次にシングルスレッド処理。 つまり、ロックを使用する前に、ロックについて検討する必要があります。必要ですか?簡素化できるでしょうか?そうしないと、後で維持するのがどれほど苦痛になるかがわかります。
ロック粒度
lock-execute A-unlock-lock-execute B-unlock
を通過していることがわかります。 ロック - A の実行 - B の実行 - ロック解除
を実行するだけでよいことは明らかです。
読み取り/書き込みロック
ReentrantReadWriteLock
読み取り/書き込みロック に値があるかどうかを再度判断することです。
getData() の場合、キャッシュは空なので、すべてのスレッドが書き込みロックをめぐって競合します。最終的には、1 つのスレッドだけが最初に書き込みロックを取得し、その後データをキャッシュに詰め込みます。
try-finally
を使用する必要があります。読み取り/書き込みロックに関して注意すべきもう 1 つの重要な点があります。それは、 ロックはアップグレードできないということです。それはどういう意味ですか?上記のコードを変更してみましょう。 ところで、もう少し触れておきます
これについても少し触れておきます。これは で提案されました。 1.8 ReentrantReadWriteLockほど出現率は高くないようです。書き込みロック、悲観的読み取りロック、楽観的読み取りをサポートします。書き込みロックと悲観的読み取りロックは、実際には、追加の楽観的読み取りを持つ ReentrantReadWriteLock の読み取り/書き込みロックと同じです。
CopyOnWrite
fork()
など、多くの場所でも使用されます。手術。また、読み取り操作で書き込みがブロックされず、書き込み操作で読み取りがブロックされないため、ビジネス コード レベルでも非常に役立ちます。大量の読み取りと少量の書き込みが必要なシナリオに適しています。 CopyOnWriteArrayList
は、読み取りがスレッドセーフである場合は書き込みをブロックしないので、善良な人はこれを使用してください。 CopyOnWriteArrayList
Arrays で 1 回トリガーされることを理解する必要があります。 .copyOf
を作成し、コピー上でそれを変更します。変更操作が多く、コピーされたデータも大きい場合、これは大変なことになります。
同時セキュリティ コンテナ
要約すると、
以上が正しいロックを使用しましたか? Java の「ロック」に関する簡単な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。