Javaマルチスレッドの理解


OSでのプロセス、スレッド
  • プロセス:実行中のプログラムであり、開いているファイル、保留中の信号、カーネル内部データ、プロセッサ状態、カーネルアドレス空間、1つ以上の実行スレッド、データセグメントなどの他のリソースが含まれます.
  • スレッド:プロセス内のアクティブなオブジェクト、カーネルスケジューリングのオブジェクトはプロセスではなくスレッドです.従来のUnixシステムのプロセスには、1つのスレッドしか含まれていません.

  • Linuxでのスレッドの実装
    Linuxカーネルの観点からはスレッドという概念はありません.Linuxはすべてのスレッドをプロセスとして実現し,カーネルはスレッドに特別なスケジューリングアルゴリズムと特別なデータ構造を用意していない.スレッドは、他のプロセスとリソースを共有するプロセスとしてのみ見なされます.だから、カーネルから見れば、それは普通のプロセスです.
    WindowsやSolarisなどのオペレーティングシステムの実装では、スレッドを専門にサポートするメカニズムが提供されています(lightweight processes).
    書き込み時コピー
    従来のfork()システム呼び出しは、コピーされたデータが不要である可能性があるため、すべてのリソースを直接新しく作成されたプロセスにコピーします.
    Linuxのfork()は,書き込み時コピーを用いて実現する.カーネルは、プロセスアドレス空間全体をコピーするのではなく、親プロセスとサブプロセスにコピーを共有させます.
    書き込みが必要な場合にのみデータがコピーされ、それ以前は読み取り専用で共有されていました.この最適化は、まったく使用されないデータの大量コピーを回避することができる(アドレス空間は、しばしば数十Mのデータを含む).
    したがって,Linux作成プロセスとスレッドの違いは,共有アドレス空間,ファイルシステムリソース,ファイル記述子,信号処理プログラムなどである.
    以下はStackOverflowの答えです.
    すなわち、Linuxの下で、プロセスはfork()を使用して作成され、スレッドはpthread_create()を使用して作成される.fork()およびpthread_create()は、clone()関数によって実現されるが、伝達されるパラメータが異なる、すなわち共有されるリソースが異なるだけである.(LinuxNPTLによってPOSIX Threadを実現する仕様であり、すなわち軽量レベルのプロセスによってPOSIX Threadを実現し、以前Unixにあったライブラリ、ソフトウェアをLinuxにスムーズに移行させることができる)
    JavaスレッドをOSスレッドにマッピングする方法
    JVMはlinuxプラットフォーム上でスレッドを作成し、pthreadインタフェースを使用する必要があります.pthreadはPOSIX規格の一部であり、スレッドを作成および管理するC言語インタフェースを定義している.Linuxはpthreadの実装を提供します.
    pthread_t tid;
    if (pthread_create(&tid, &attr, thread_entry_point, arg_to_entrypoint))
    {
          fprintf(stderr, "Error creating thread
    "); return; }
  • tidは、新たに作成するスレッドのID
  • である.
  • attrは、設定する必要があるスレッド属性
  • です.
  • thread_entry_pointは、新しく作成するスレッドによって呼び出される関数ポインタ
  • である.
  • arg_to_entrypointは、thread_entry_pointに伝達するパラメータ
  • である.thread_entry_pointが指す関数がThreadオブジェクトのrunメソッドである.
    戻り値なしスレッドと戻り値付きスレッド
  • 戻り値なし:1つはThreadを直接継承することであり、もう1つはRunnableインタフェース
  • を実装することである.
  • 帯域戻り値:CallableとFutureで
  • を実現
    戻り値を持つスレッドは、実際にはより一般的に使用されています.
    競合条件
    ある計算の正確性が複数のスレッドの交互実行タイミングに依存すると,競合条件が発生する.
    最も一般的な競合条件タイプは、「先に検査してから実行」(Check-Then-Act)操作、すなわち、失効する可能性のある観測結果によって次の動作を決定することである.
    「チェックしてから実行」を使用する一般的な例は、初期化の遅延です.
    public class LazyInitRace {
        private ExpensiveObject instance = null;
        
        public ExpensiveObject getInstance() {
            if (instance == null) {
                instance = new ExpensiveObject();
            }
            return instance;
        }
    }

    そうするな.
    Executorフレームワーク
    裸スレッドの使用の欠点prod環境では、 の方法には深刻な欠陥があり、特に大量のスレッドを作成する必要がある場合:
  • スレッドのライフサイクルのオーバーヘッドは非常に高いです.スレッドの作成と破棄には代価がありません.
  • リソース消費:メモリとCPUが消費され、大量のスレッドがCPUリソースを競合するとパフォーマンスオーバーヘッドが発生します.すべてのCPUをビジー状態にするのに十分なスレッドがある場合は、より多くのスレッドを作成するとパフォーマンスが低下します.
  • 安定性:作成可能なスレッドの数に制限があります.JVMの起動パラメータ、オペレーティングシステムのスレッドに対する制限が含まれています.これらの制限を超えると、OutOfMemoryErrorの異常が放出される可能性があります.

  • Executor基本原理Executorは、生産者-消費者モードに基づいて、タスクをコミットする操作は生産者に相当し、タスクを実行するスレッドは消費者に相当する.
    スレッドプールの構築関数は次のとおりです.
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue workQueue,
                                  RejectedExecutionHandler handler) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), handler);
        }

    スレッドプールサイズ
  • corePoolSize:コアスレッド数、スレッドプールのスレッド数がcorePoolSize未満の場合、新しいスレッド
  • が直接作成されます.
  • スレッド数はcorePoolSizeより大きいがmaximumPoolSizeより小さい:タスクキューがまだ満たされていない場合、このタスクはタスクキューの最後に挿入されます.タスクキューがいっぱいになると、このタスクを実行するために新しいスレッドが作成されます.
  • スレッド数はmaximumPoolSizeに等しい:タスクキューがまだ満たされていない場合、このタスクはタスクキューの最後に挿入されます.この時点でタスクキューがいっぱいになった場合、RejectedExecutionHandlerによって処理されます.

  • keep-alive
  • keepAliveTime:スレッドプール内のスレッド数がcorePoolSizeより大きい場合、スレッドがアイドル(Idle)状態で指定された時間(keepAliveTime)を超えると、スレッドプールはスレッドを破棄します.

  • ワークキュー
    ワークキュー(WorkQueue)は、コミットされたタスクを格納するために使用されるBlockingQueueですが、実行するためのスレッドがまだ空いていません.
    一般的なワークキューには、次のようなものがあります.
  • 直接切替(Direct handoffs)
  • 無境界キュー(Unbounded queues)
  • 境界キュー(Bounded queues)
  • 生産環境では、キューに蓄積されたタスクが多すぎると、大量のメモリが消費され、最後のOOMになるため、無境界キューの使用は禁止されています.通常は固定サイズの境界キューを設定し、スレッドプールがいっぱいで、キューもいっぱいの場合、新しく提出されたタスクを直接拒否し、RejectedExecutionException を投げ出します.本質的には、サービス自体に対する保護メカニズムであり、サービスが新しい提出されたタスクを処理するリソースがなく、直接拒否するためです.
    Javaオリジナルスレッドプールの生産環境における問題
    サービス化の背景の下で、私たちのフレームワークは一般的に の機能を統合し、呼び出しチェーン全体を直列に接続するために使用され、主にTraceIdSpanIdを記録します.TraceIdおよびSpanIdは、一般にThreadLocalに記録され、事業者にとって透明である.
    同じスレッドでRPC呼び出しを同期する場合、問題はありません.しかし、スレッドプールを使用してクライアントの非同期呼び出しを行うと、Traceの情報が失われます.根本的な原因は、Traceの情報がプライマリスレッドのThreadLocalからスレッドプールのThreadLocalに伝達されないことです.
    この痛みに対して、アリ開源のtransmittable-thread-localはこの問題を解決したが、実現は難しくない.ソースコードを読むことができる.
    https://github.com/alibaba/transmittable-thread-local

    パフォーマンスと伸縮性
    性能に対する考え方
    パフォーマンスの向上は、より少ないリソースでより多くのことをすることを意味します.「リソース」は、CPUクロックサイクル、メモリ、ネットワーク帯域幅、ディスク領域などの他のリソースを意味します.動作性能が特定のリソースによって制限される場合、通常、CPU密集型、IO密集型など、リソース密集型の動作と呼ぶ.
    マルチスレッドを使用すると、サービス全体のパフォーマンスが理論的に向上しますが、マルチスレッドを使用すると、単一スレッドに比べて追加のパフォーマンスオーバーヘッドが導入されます.スレッド間の調整(ロック、トリガ信号、メモリ同期など)、コンテキストの増加、スレッドの作成と破棄、スレッドのスケジューリングなどが含まれます.スレッドを過度に使用すると、同じ機能を実現するシリアルプログラムよりもパフォーマンスが低下する可能性があります.
    パフォーマンスモニタリングの観点から、CPUはできるだけ忙しい状態を保つ必要がある.プログラムがコンピューティングが密集している場合は、プロセッサを増やすことでパフォーマンスを向上させることができます.しかし、プログラムがCPUをビジー状態に保つことができない場合は、より多くのプロセッサを追加しても始まらない.
    伸縮性
    伸縮性とは、計算リソース(例えば、CPU、メモリ、記憶容量、IO帯域幅)を増加させると、プログラムのスループットまたは処理能力が応答して増加することを意味する.
    プログラム内の表現層、ビジネスロジック層、および永続層は互いに独立しており、異なるサービスによって処理される可能性があるという3層モデルをよく知っています.これは、伸縮性の向上が通常性能損失をもたらすことをよく示しています.パフォーマンス・レイヤ、ビジネス・ロジック・レイヤ、および永続レイヤを単一のアプリケーションに統合すると、負荷が高くない場合、アプリケーションを多層化するよりもパフォーマンスが高くなります.この単体アプリケーションは,異なる階層間でタスクを伝達する際に存在するネットワーク遅延を回避し,多くのオーバーヘッドを低減する.
    しかしながら、モノマー応用が自己処理能力の限界に達すると、その処理能力を向上させることは非常に困難であり、水平に拡張できないという深刻な問題に直面する.
    Amdahlの法則
    ほとんどのコンカレント・プログラムは、一連のパラレル・ワークとシリアル・ワークから構成されています.Amdahlの法則は、計算リソースを増加させる場合、 が占める割合に応じて、プログラムが理論的に最高加速比を実現できることを記述する.Fが直列に実行されなければならない部分であると仮定すると、Amdahlの法則に従って、N個のプロセッサを含む機械において、最も高い加速比は以下の通りである.
    Nが無限大に近づくと、最大加速比は1/Fに近づく.したがって,プログラムの50%の計算がシリアルで実行される必要がある場合,最高の加速比は2にすぎない.
    コンテキストの切り替え
    スレッドスケジューリングではコンテキストの切り替えが発生し、コンテキストの切り替えではオーバーヘッドが発生します.CPU プログラムが大量のスレッド切替を生成すると、システムのスループットが低下する.UNIXシステムのvmstatコマンドは、コンテキスト切替回数やカーネルでの実行時間の割合などの情報を報告することができる.カーネル占有率が高い(10%を超える)場合、スケジューリングアクティビティは通常頻繁に発生していることを示し、これはI/Oまたはロック競合によるブロックに起因する可能性が高い.
    >> vmstat
    procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
     r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
     1  0      0 3235932 238256 3202776    0    0     0    11    7    4  1  0 99  0  0
     
     cs:         
     sy:             
     us:           

    以上です.
    テキストリンク
    https://segmentfault.com/a/11...