blog

Java並行プログラミング ブログノートを読む

頻繁なコンテキストスイッチ:スレッドの実行が中断された時点から再開できるように、CPUの状態を保存して復元するプロセス 通常、コンテキストスイッチは、ロックフリーの並行プログラミング[コンテキストスイ...

May 21, 2020 · 13 min. read
シェア
## Javaコンカレント・プログラミング ブログノートを読む

I: 理由

  • バンテージ

     	マルチコアCPUの計算能力を活用する;
     	ビジネス分割を促進し、アプリケーションのパフォーマンスを向上させる;
    
  • 欠点

     	 頻繁なコンテキスト・スイッチ: スレッドの実行が中断された時点から再開できるようにCPUの状態を保存・復元するプロセスは、通常、コンテキスト・スイッチを減らすことができ、ロックフリーの並行プログラミングに利用できる[ロック競合による不要なコンテキスト・スイッチの一部を減らす]最も少ないスレッド数と連結を使用するCASアルゴリズム[シングルスレッドでマルチタスクをスケジューリングする] 
    

    スレッドセーフ[デッドロック,同期]でデッドロックを回避:

    1. スレッドが同時に複数のロックを取得しないようにします;

    2. スレッドがロック内で複数のリソースを占有することは避け、各ロックが1つのリソースのみを占有するようにします;

    3. 時限ロックを使ってみてください。

  • 概念的

    同時、並列 クリティカル・ゾーン 同期、非同期

スレッドの状態遷移

  • スレッドの作成

    Threadクラスを継承してrunメソッドをオーバーライドします;

    実行可能なインターフェイスを実装することで

    呼び出し可能なインターフェイスを実装するには、次の3つの方法があります。

  • スレッドの状態遷移

  • スレッド状態の基本操作 スレッド間の通信形態

    1. Interrupted: 実行中のスレッドが他のスレッドによって割り込まれたかどうかを示します。 boolean interrupted() :現在のスレッドが割り込まれたかどうかを検出します。isInterrupted とは異なり、このメソッドは現在のスレッドが割り込まれたことを検出すると割り込みフラグをクリアします。

      スレッドが正しく終了する前に強制終了させることは危険な行為です。 保持しているロックと一緒に永遠に冬眠してしまったり、ロックの返却を遅らせてしまったりなど、まったく予期しない深刻な結果をもたらす可能性があるからです。 Thread.suspendやThread.stopなどのメソッドがDeprecatedになっているのはそのためです。

    よりエレガントで安全なアプローチは、wait/notifyメカニズムを使うか、スレッドに割り込みシグナルを与えて、スレッドが何をするか決めるようにすることでしょう。

    1. 割り込みは同期メソッドやロックオブジェクトの取得を待つブロックでは機能しません。ロックを取得して実行を継続するか、割り込み先のスレッドのメソッドが呼び出されても、待ち時間を保存して有効になりません

      • 割り込みとは、ブロッキング/スリープ状態にあるスレッドに割り込みがかかり、ブロッキング状態を終了して実行を継続することです。CPU 上で実行されていないスレッドが割り込み状態を無効にすることはできません。これは InterruptedException を生成します。

      • 実行中のスレッドに割り込みをかけると、割り込みフラグがセットされます。割り込まれたスレッドは、割り込みにどのように応答するかを決めることができます。
    2. joinメソッドは、スレッド間の共同作業の方法として見ることができ、あるスレッドの入力が他のスレッドの出力に大きく依存することが非常によくあります。

    3. sleep()メソッドはCPUを放棄するだけで、オブジェクト・ロックを解放しません。wait()メソッドは、同期メソッドまたはブロックの中で呼び出されなければなりません。

    4. yield() メソッドは、現在のスレッドと同じ優先順位を持つスレッドだけが、解放された CPU タイムスライスにアクセスできるようにします。yield メソッドを呼び出すと、スレッドはブロッキング状態にならず、レディ状態に戻ります。CPUを明け渡す正確な時間を制御することはできません。

JMMとhappens-before

  • スレッドセーフ:複数のスレッドが同じオブジェクトにアクセスする場合、このオブジェクトを呼び出す動作が、実行環境におけるこれらのスレッドのスケジューリングや交替を考慮することなく、追加の同期や呼び出し側の他の調整操作なしに、正しい結果を得ることができれば、そのオブジェクトはスレッドセーフです。

  • スレッドセーフの問題は一般的に、メインメモリとワーキングメモリ間のデータの不整合や並び替えのために発生します。

  • マルチスレッドの条件下では、複数のスレッドが互いに協力して何かを完了させることは確実であり、一般的には、複数のスレッドが互いに通信して、互いの状態や現在の実行結果などを知らせます

  • スレッドが協力する方法: 1) 共有変数 2) 通知待ち 3) 割り込み

  • 1.共有メモリとメッセージパッシングの観点から、スレッドがどのように互いに通信するか。2.異なるスレッド間で発生する操作の相対的な順序を制御するために、スレッド間の同期をどのように実現するか。

  • JMMは、スレッドの共有変数への書き込みが他のスレッドから見えるようになるタイミングを決定します。

  • 命令をコンパイラやプロセッサが並べ替えて性能を向上させることが多い データ依存性がない マルチスレッド状態で問題が発生する可能性があります

  • happens-beforeの概念は、2つの操作の間の実行順序を指定するために使用されます。2つの操作はスレッド内でも、異なるスレッド間でも可能です。したがって、JMM は happens-before の関係を通じて、プログラマにクロススレッドでのメモリ可視性を保証することができます

  • プログラマの視点から見ると、happens-before関係はこのように理解することができます: AがBの前にhappens-beforeする場合、Javaのメモリモデルは、Aの操作の結果がBに見えることをプログラマに保証し、Aの実行順序がBの実行順序に先行することを保証します。before関係は最終的な実行順序を表すものではありません。あくまで論理的な順序です。結果が一貫していれば、順序の入れ替えは許されます。

  • 上位レベルでは、JMMベースのキーワードとJ.U.Cパッケージの具体的なクラスが用意され、プログラマが迅速かつ効率的に並行プログラミングを実行できるようにします。

    要約すると、1.JMMの抽象的な構造、2.並べ替えとhapens-beforeルール。

同期化入門

  • オブジェクトへの任意のスレッドアクセスは、オブジェクトのモニタを取得する最初の事は、取得に失敗した場合、スレッドが同期状態に入ると、スレッドの状態は、オブジェクトのモニタの所有者が解放されたときに、スレッドの同期キューで、モニタを再取得する機会を持つことになりますブロックされます。

  • スレッドは、同じロックスレッドで同じロックを再度取得する必要はありません。

  • JMMのコアには、happens-beforeルールとメモリ抽象化モデルの2つの部分があります。

  • ロックを解放するとメインメモリに値がフラッシュされ、ロックを取得した他のスレッドはメインメモリから最新の値を取得するように強制されます。つまり、近接領域の値は常に最新です。

    CAS
  • CAS操作は効率を高めるための楽観的ロック戦略で、共有リソースにアクセスするすべてのスレッドが競合していないと仮定します。競合がある場合は再試行します。

  • CASの比較・交換処理は、大まかにCAS(V,O,N)と理解することができ、Vのメモリ・アドレスに格納されている実際の値、Oの期待値、Nの新しい値の3つの値から構成されます。VとOが同じ、つまり古い値とメモリ上の実際の値が同じである場合、他のスレッドによって値が変更されていないことを意味します。VとOが同じ、つまり古い値とメモリ上の実際の値が同じということは、他のスレッドによって値が変更されていない、つまり古い値Oが今のところ最新の値であり、当然新しい値NをVに割り当てることができるということです。

  • 同期(最適化前)の問題は次のとおりです。スレッドの競合が存在すると、性能の問題によってもたらされるスレッドのブロッキングやウェイクアップロックが発生します。CAS操作が失敗した場合、それは非ブロッキング同期と呼ばれる、特定の試みを行います。

  • 1.ABA問題 2.スピン時間が長すぎる 3.アトミック操作は1つの共有変数に対してのみ保証されるAtomicReferenceは、参照されるオブジェクト間のアトミック性を保証するためにatomicで提供されます。

    Java
  • 同期とは、オブジェクトのモニターを獲得すること、つまりオブジェクトのロックを獲得することです。では、オブジェクト・ロックをどのように理解すればよいのでしょうか?何もオブジェクトのフラグに似ているわけではなく、このフラグはJavaオブジェクトのヘッダーに格納されています。

  • マーク・ワード32ビットには、デフォルトでhasdcode、age値、ロック・フラグ・ビットなどの情報が格納されます。

  • ロックには全部で4つの状態があり、そのレベルは低い順に、ロックなし、偏ったロック状態、軽量ロック状態、重量ロック状態です。

  • スレッドが同期ブロックにアクセスしてロックを取得すると、ロック・バイアスのスレッド ID がオブジェクト・ヘッダのロック・レコードとスタック・フレームに格納され、スレッドが後で同期ブロックに入ったり出たりするときに、CAS 操作を実行してロックを追加したり解除したりする必要がなくなります。オブジェクト・ヘッダの Mark Word に現在のスレッドの偏ったロックが格納されているかどうかをテストします。テストが成功すれば、スレッドはロックを獲得しています。テストに失敗した場合は、Mark Wordのバイアス・ロックの識別子が1に設定されているかどうかを再度テストする必要があります。設定されていない場合は、CASを使用してロックを競合させてもバイアス・ロックはオンになりません。設定されている場合は、CASを使用してオブジェクト・ヘッダのバイアス・ロックを現在のスレッドに向けるようにします。バイアス・ロックの競合

  • バイアス・ロックは、競合が発生するまでロックを解放するのを待つメカニズムを使用しているため、バイアス・ロックを保持しているスレッドは、他のスレッドがバイアス・ロックを競合しようとするまでロックを解放しません。

  • スレッドが同期ブロックを実行すると、JVMはまず現在のスレッドのスタックフレームにロックレコードを格納するためのスペースを作成し、オブジェクトヘッダ内のマークワードをロックレコードにコピーします。成功した場合は、現在のスレッドがロックを取得し、失敗した場合は、他のスレッドがロックを競合していることを示すため、現在のスレッドがスピンを使用してロックを取得しようとします。

  • ライト級がアンロックされると、アトミックCASオペレーションが使用され、置き換えられたマーク・ワードをオブジェクト・ヘッダに戻します。失敗した場合は、現在のロックに競合が発生し、ロックがヘビーウェイト・ロックに拡張されたことを意味します。

    V. 揮発性

  • Java仮想マシンの特別な慣例に揮発性変更された変数については、揮発性変数の変更にスレッドがすぐに他のスレッドで認識される、つまり、データがない汚れた読み取り現象は、このように "可視性 "のデータを確保することはありません。

  • 現在のプロセッサのキャッシュ・ラインからシステム・メモリにデータを書き戻します。この書き戻しは、他のCPUでそのメモリ・アドレスにキャッシュされているデータを無効にします。キャッシュコヒーレンス

    3物件

    順序性:ロックの二重チェック:アドレスの割り当てや初期化を順番通りに行わないと、if(instance==null)の判定でtrueになります。

    すべての操作は、このスレッド内で観測された場合は順序付けされ、あるスレッドで別のスレッドで観測された場合は順序付けされません

    バイトコード生成時に揮発性メモリ・セマンティクスを実装するために、コンパイラは命令シーケンスの特定のタイプのプロセッサの並べ替えを無効にするメモリ・バリアを挿入します。

    これはシングルスレッド環境では安全です。しかし、マルチスレッド環境では、データ依存性のない命令の並び替えは問題を引き起こす可能性があります。

    Javaの正しいマルチスレッドプログラミングのJMM実装は、基礎となるデータ非依存命令並べ替えに対する上位レベルの制約を提供します;

    casは上書きがないことを保証し、volatileは可視性を保証します。ダーティリードを回避

    volatileは原子性を保証し、以下の2つのルールに従わなければなりません:

    1. 操作の結果が変数の現在値に依存しないか、または1つのスレッドだけが変数の値を変更することを保証することができます;
    2. 変数は、他の状態変数との不変制約に参加する必要はありません。

    volatileはCASとは異なります。volatileの可視性は、メインメモリとキャッシュ・コヒーレンシ・プロトコルを強制的にフラッシュすることで達成されます。 CASは、CPUリソースを消費する楽観的ロックであり、ロックのアップグレードを最適化して同期をとるために使用されます。

.lock

  • synchronizedブロックが終了するか例外が発生すると、ロックは自動的に解放されます。一方、ロックはunlock()メソッドを呼び出して解放する必要があるため、finallyブロックで解放されます
  • 同時実行パッケージにおけるこれらのクラスの実装は、volatileとCASに大きく依存しています。
  • AQSサブクラスは、カスタム同期コンポーネントの静的な内部クラスとして定義することが推奨されます。 ステッパーは、同期状態への排他アクセスと共有アクセスの両方をサポートしており、異なるタイプの同期コンポーネントを簡単に実装できます。
  • ロックはユーザー指向であり、ユーザーがロックとやりとりするためのインターフェイスを定義し、実装の詳細を隠します。シンクロナイザーはロック実装者指向であり、ロックの実装を簡素化し、同期状態の管理、スレッドのキューイング、待機とウェイクアップ、およびその他の基礎となる操作を隠します

AQS: 共有リソースへのマルチスレッドアクセスのための同期化フレームワーク

  • AQSは単なるフレームワークであり、リソースの取得/解放方法はカスタムのシンクロナイザーに任されています。

    カスタムのシンクロナイザー実装は、以下のメソッドを実装している:[待ち行列を維持する\同期状態獲得のサイクルはすでに設計されている]
    isHeldExclusively()スレッドは起こされた時点で実行可能状態にあり、スレッドがリソースを排他的に所有しているかどうかという挙動は同じである。コンディションを使用する場合のみ、コンディションを実装する必要がある。
    tryAcquire(int)排他モード。リソースの獲得を試み、成功すればtrue、失敗すればfalseを返す。
    tryRelease(int)排他モード。リソースの解放を試み、成功すればtrue、失敗すればfalseを返す。
    tryAcquireShared(int)共有アプローチ。リソースの獲得を試みる。負の数は失敗を意味し、0は成功を意味するが、利用可能なリソースが残っていないことを意味し、正の数は成功と残りのリソースを意味する。
    tryReleaseShared(int)共有アプローチ。リソースの解放を試み、成功すればtrueを返し、失敗すればfalseを返す。
    
  • シンクロナイザーのデータ構造

    • ノードのデータ構造、すなわちAQSの静的な内部クラスNode、ノードの待機状態に関する情報など
    • 同期キューは双方向キューであり、AQSはヘッドポインタとテールポインタを保持して同期キューを管理します。
    • 同期状態を volatile int で記録します。
  • ロックのフェッチに失敗するとイン・キュー操作が行われ、ロックのフェッチに成功するとアウト・オブ・キュー操作が行われます。

Acquireメソッドのロジック
tryAcquire()リソースに直接アクセスしてみて、成功したら直接戻る;
addWaiter()スレッドを待ち行列の最後尾に追加し、排他モードとしてマークする;
acquireQueued()挙動の効果は同じで、スレッドはリソースのフェッチを待っており、戻る前にリソースのフェッチを続ける。待機中にスレッドが中断された場合はtrueを返し、そうでない場合はfalseを返す。
待機中に割り込まれた場合、スレッドは応答しない。リソースを獲得した後、selfInterrupt()を実行して割り込みに対応する。
  • 排他的ロックの獲得に失敗したスレッドをノードにパッケージ化し、同期キューに挿入するプロセス。

    • 現在のスレッドが最初に同期キューに参加する場合、compareAndSetHead(new Node())メソッドを呼び出して、連鎖キューのヘッド・ノードの初期化を完了します
    • スピンは成功するまでCASテール挿入ノードを試し続けます。
  • ロックは、ロック取得中の割り込みへの応答をサポートします。

  • acquireInterruptibly即時応答によって割り込みマーカーが消え、スレッドの状態はCancelとマークされた。
    acquireその方法は、ロック獲得後に処理する割り込みフラグを設定することである。
    
     private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);
     //現在のスレッドが中断されているかどうかを判断し、フラグをクリアする。
     return Thread.interrupted();
     }
     
    if (shouldParkAfterFailedAcquire(p, node) &&
     parkAndCheckInterrupt())
     throw new InterruptedException();
     }
    finally {
     //割り込みに応答するスレッド
     if (failed)
     cancelAcquire(node);
     }
    

    共有モデルと専用モデル

    非フェアロックがちょうどロックを取得し続けるために、ロックスレッドの次の時間を解放することがありますが、同期キュー内の最初のノードのロックを取得するたびに、時間の絶対順序でのリソースの要求を確保するために、公正なロックは、他のスレッドにつながる可能性がありますロックを取得することはできません、その結果、"飢餓 "の現象 コンテキストスイッチングがあるかもしれません

    Condition

    オブジェクトの待機と通知/通知は、スレッド間の待機/通知機構の完了とオブジェクトのモニタと、待機通知機構の完了とロックと条件であり、前者はjavaの基礎レベルであり、後者は言語レベルであり、制御性とスケーラビリティの高度です。

    ConditionObjectこのクラスはAQSの内部クラスです)。

    現在のスレッドがcondition.await()メソッドを呼び出すと、現在のスレッドはロックを解放してから待ち行列に入り、signal/signalAllになるまで、現在のスレッドは待ち行列から同期待ち行列に移動し、ロックを取得してからawaitメソッドから戻るか、待機中に割り込まれます。割り込み処理

    ConcurrentHashmap

    ロック・セグメンテーションのアイデアは、同時実行性を向上させるために利用されます

    原子クラス

    getAndIncrement()メソッドはアトミックではありません。 それは、このメソッドと他の関数が値の更新に有効であることを保証するだけです。

    CASの問題点 1.ABAの問題点 2.スピンタイム過多のノンブロッキング同期

    ブロッキング待ち行列は、以下の二つの追加操作をサポートする待ち行列です:

    • キューが一杯になると、キューは、キューが一杯になるまで、要素を挿入するスレッドをブロックします。

    • キューが空の場合、要素を取得するスレッドは、キューが空でなくなるのを待ちます。

    Executor

    • 単位
      • Runnable
      • Callable
    • 実行メカニズム
      • Executor
  • 二段階派遣モデル

    HotSpot VMのスレッドモデルでは、Javaスレッドは ネイティブOSスレッドに1対1でマッピングされます。

    これらのタスクを固定数のスレッドにマッピングします。

    execute() メソッドは、返り値を必要としないタスクを投入するために使用します。

    submit() メソッドは、返り値を必要とするタスクを送信するために使用します。

    • shutdownメソッドは通常、スレッドプールを閉じるために呼び出されます。
    • タスクを終了する必要がない場合は、shutdownNowメソッドを呼び出すことができます。
  • ワーカースレッドの run メソッドで、ブロッキングキュー内のタスクをループします。

    その他

    ブロッキングとウェイティングの違い

    ブロッキング:スレッドがオブジェクト・ロックを取得しようとして、そのロックが他のスレッドによって保持されると、スレッドはブロッキング状態になります。他のスレッドが割り込み**に応答して明示的に自分自身を目覚めさせることなく、自分自身を目覚めさせるかどうかを決定するのは、JVMスケジューラ次第です。待機: スレッドが、他のスレッドがスケジューラに状態を通知するのを待つとき、スレッドは待機状態に入ります。これは、他のスレッドが明示的に自分自身を目覚めさせるのを待つ必要があること、柔軟な実装、より豊富なセマンティクス、割り込みに応答できることが特徴です。例えば、Object.wait()、Thread.join()を呼び出し、LockやConditionを待ちます。

    synchronizedはスレッドをブロッキング状態にしますが、JUCのLockはLockSupport.park()/unpark() [これはsynchronizedキューとwaitキューに依存します]を使用してブロッキング/ウェイクアップを実装しますを使用して、スレッドを待ち状態にします。しかしまた、ロックを待つときの状態は違っても、ウェイクアップされた後はどちらも実行可能な状態になり、動作の影響という点では同じです。

Read next

OkHttpのソースコード解析 (a)

同じ Request も Builder を通してコンストラクタのインスタンスを生成し、HTTP リクエストに必要な情報の一部をカプセル化します。 Okhttp は newCall を通して Call オブジェクトの生成を要求し、このオブジェクトは Request サービスが Http リクエストのプロセスを完了させる役割を担います。同期リクエストはRealCallのexecute...によって開始されます。

May 21, 2020 · 10 min read