JAvaにおけるスレッドプール技術と最適化


この文章を書いたのは、チームの1つのプロジェクトがこの技術を使用し、性能上の問題が発生したからです.個人的にはこの問題にも大きな興味を持っています.
 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("ThreadPoolName").build();
 private static ThreadPoolExecutor executorService = new ThreadPoolExecutor(200, 400, 0, TimeUnit.MILLISECONDS, new SynchronousQueue<>(), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());

スレッドプールのdemoの作成
//     
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//        
private static final int corePoolSize = Math.max(2, Math.min(CPU_COUNT - 1, 4));
//          
private static final int maximumPoolSize = CPU_COUNT * 2 + 1;
//          
private static final int keepAliveTime = 30;

//     ,           
BlockingQueue  workQueue = new SynchronousQueue<>();

//       
ThreadFactory threadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        return new Thread(r, "AdvacnedAsyncTask #" + mCount.getAndIncrement());
    }
};

//                 
RejectedExecutionHandler rejectHandler = new ThreadPoolExecutor.DiscardOldestPolicy();

//     ,    
ThreadPoolExecutor mExecute = new ThreadPoolExecutor(
        corePoolSize, 
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.SECONDS,
        workQueue,
        threadFactory, 
        rejectHandler
);

自分でネット上の文章を研究して参考にしてみました.
スレッドプールの概要:http://www.jianshu.com/p/87bff5cc8d8c
スレッドプールの原理と提案:http://www.infoq.com/cn/articles/java-threadPool#anch151278
ポリシーの拒否:http://wangkuiwu.github.io/2012/08/15/juc-executor05/
以上の資料では,newScheduledThreadPoolというスレッドプールの適用シーンについて述べた.
スレッドプールの定期的な同期データは、実際のビジネスシーンで使用できます.
その中で最も興味深いのはexecuteメソッドです.
具体的な実行プロセスは以下の通りです.
1.workerCountOfメソッドはctlの29ビット低い位置からスレッドプールの現在のスレッド数を得る.スレッド数がcorePoolSizeより小さい場合、addWorkerメソッドを実行して新しいスレッド実行タスクを作成する.そうでなければステップ(2)を実行する.
2、スレッドプールがRUNNING状態にあり、コミットされたタスクがブロックキューに正常に入れられた場合、ステップ(3)を実行し、そうでなければステップ(4)を実行する.
3、スレッドプールの状態を再度チェックし、スレッドプールにRUNNINGがなく、ブロックキューからタスクを削除することに成功した場合、rejectメソッド処理タスクを実行する.
4、addWorkerメソッドを実行して新しいスレッド実行タスクを作成し、addWokerが失敗した場合、rejectメソッド処理タスクを実行する.
/*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */

スレッドプールの使用手順:
この時点で現在のプール内の作業スレッド数がcorePoolSizeより小さい場合、作業スレッドセットにスレッドがアイドル状態であるかどうかにかかわらず、新しい作業スレッドを作成してこのタスクを実行します.
プールにcorePoolSizeより大きいがmaximumPoolSizeより小さいワークスレッドがある場合、タスクはまずキューに入れてみます.ここでは、a、タスクが正常にキューに入れられた場合、新しいスレッドを開いてタスクを実行する必要があるかどうかを確認する必要があります.現在のワークスレッド数が0の場合にのみ、新しいスレッドが作成されます.以前のスレッドは、すべてアイドル状態にあるか、作業が終了したために削除される可能性があります.b、キューの挿入に失敗した場合、新しいワークスレッドが作成されます.
corePoolSizeとmaximumPoolSizeが同じ場合、スレッドプールのサイズは固定されます.
maximumPoolSizeを無限大に設定することで,上限のないスレッドプールを得ることができる.
パラメータを構築してこれらのスレッドプールパラメータを設定するほか、実行時に設定することもできます.
コアスレッドWarmUp最適化
デフォルトでは、コアワークスレッド値は初期に作成され、新しいタスクが来たときに起動されますが、prestartCoreThreadメソッドまたはprestartCoreThreadsメソッドを書き換えることで、この動作を変更できます.通常、アプリケーションが起動したときにWarmUpコアスレッドに来ることができ、タスクがすぐに実行できる結果を達成し、初期タスク処理の時間を一定に最適化することができます.
カスタムワークスレッドの作成最適化
新しいスレッドはThreadFactoryによって作成され、指定されていない場合はデフォルトのExecutors#defaultThreadFactoryが使用され、このとき作成されたスレッドは同じスレッドグループに属し、同じ優先度とdaemon状態を有します.拡張構成ThreadFactoryでは、スレッドの名前、スレッドの組合せdaemonステータスを構成できます.ThreadFactory#createThreadの呼び出しに失敗した場合、nullが返され、executorはタスクを実行しません.
コアスレッド回収
現在のプール内の作業スレッド数がcorePoolSizeより大きい場合、この数値を超えるスレッドがkeepAliveTimeよりも空き時間が大きい場合、これらのスレッドは終了します.これは、不要なリソース消費を削減する戦略です.このパラメータは実行時に変更でき、コアスレッドにも適用できます.allowCoreThreadTimeoutを呼び出すことで実現できます.
正しい選択キュー
次は主に異なるキュー・ポリシーの表現です.
直接提出:SynchronousQueueを使用するのがデフォルトの選択です.このポリシーでは、提出されたタスクは、持っていないワークスレッドに直接送信されます.現在作業スレッドが処理されていない場合、タスクがキューに入れられなかった場合、スレッドプールの実装に基づいて新しい作業スレッドの作成が開始されるため、新しくコミットされたタスクが処理されます.このポリシーは、コミットされたタスクの間に依存関係がある場合に、ロック競合の消費を回避します.特筆すべきは、タスクが拒否されないようにunboundedスレッド数に合わせて使用することが望ましいことです.同時に、タスクが到来する速度がタスク処理の速度より大きい場合、制限のないスレッド数が増加するシーンを考慮する必要があります.
≪無境界キュー|UnderQueue|oem_src≫:LinkedBlockingQueueのような無境界キューを使用して最大容量が指定されていない場合、コア・スレッドが忙しい場合、新しいタスクがキューに配置されるため、corePoolSizeよりも大きなスレッドが作成されることはありません.したがって、maximumPoolSizeパラメータは失効します.このポリシーは、すべてのタスクが互いに依存せず、独立して実行されるのに適しています.例えば、ウェブサーバでは、各スレッドが独立して要求を処理する.しかし、タスクの処理速度がタスクの進入速度より小さい場合、キューの無限膨張を引き起こす.
境界キュー:ArrayBlockingQueueなどの境界キューは、リソースの消費を制限するのに役立ちますが、制御しにくいです.キュー長とmaximumPoolSizeの2つの値は互いに影響し合い、大きなキューと小さなmaximumPoolSizeを使用するとCPUの使用、オペレーティングシステムリソース、コンテキスト切替の消費が減少しますが、スループットが低下し、タスクが頻繁にIOスレッドなどのブロックされている場合、システムは実際にはより多くのスレッドをスケジューリングすることができます.小さなキューを使用するには、通常、大きなmaximumPoolSizeが必要です.これにより、CPUはより忙しくなりますが、スループットを低下させるスレッドスケジューリングの消費量が増加します.まとめてみると、IO密集型はCPUの使用をバランスさせるために複数のスレッドを考慮することができ、CPU密集型はスレッドスケジューリングの消費を減らすことを考慮することができる.
スレッドプールの合理的な構成
スレッドプールを合理的に構成するには、まずタスクの特性を分析する必要があります.以下のいくつかの角度から分析できます.
タスクの性質:CPU密集型タスク、IO密集型タスクと混合型タスク.
タスクの優先度:高、中和低.
タスクの実行時間:長、中和、短.
タスクの依存性:データベース接続などの他のシステムリソースに依存するかどうか.
タスクの性質の異なるタスクは、異なる規模のスレッドプールで別々に処理できます.CPU密集型タスクは、Ncpu+1スレッドのスレッドプールを構成するなど、できるだけ小さなスレッドを構成します.IO密集型タスクは,スレッドが常にタスクを実行しているわけではないため,2*Ncpuのような可能な限り多くのスレッドを構成する.ハイブリッド型のタスクは、分割可能であれば、CPU密集型タスクとIO密集型タスクに分割されます.この2つのタスクの実行時間の差があまり大きくない限り、分解後のスループットはシリアル実行のスループットよりも高く、この2つのタスクの実行時間の差が大きすぎる場合は、分解する必要はありません.私たちはRuntimeを通じてgetRuntime().AVailableProcessors()メソッドは、現在のデバイスのCPU個数を取得する.
優先度の異なるタスクは、優先度キューPriorityBlockingQueueを使用して処理できます.優先度の高いタスクを先に実行することができます.優先度の高いタスクがキューにコミットされている場合、優先度の低いタスクは永遠に実行できない可能性があります.
実行時間の異なるタスクは、異なる規模のスレッドプールに渡して処理するか、優先度キューを使用して、実行時間の短いタスクを先に実行することもできます.
データベース接続プールのタスクに依存します.スレッドがSQLをコミットした後、データベースの戻り結果を待つ必要があるため、待機時間が長ければ長いほどCPUの空き時間が長くなる場合は、スレッド数を大きく設定してこそ、CPUをよりよく利用することができます.
有界キューを使用することをお勧めします.有界キューはシステムの安定性とアラート能力を増加させ、必要に応じて数千などの大きな設定ができます.ある時、私たちのグループが使用したバックグラウンドタスクスレッドプールのキューとスレッドプールがいっぱいになって、絶えずタスクを捨てる異常を投げ出して、調査を通じてデータベースに問題が発生したことを発見して、SQLの実行が非常に遅くなって、バックグラウンドタスクスレッドプールのタスクはすべてデータベースにデータを照会して挿入する必要があるため、スレッドプールの中の作業スレッドはすべてブロックされて、タスクがラインプールに蓄積されます.無境界キューに設定すると、スレッドプールのキューがますます多くなり、バックグラウンドタスクだけでなくシステム全体が使用できなくなる可能性があります.もちろん、システムのすべてのタスクは個別のサーバで導入されていますが、異なる規模のスレッドプールを使用して異なるタイプのタスクを実行していますが、このような問題が発生した場合、他のタスクにも影響します.
スレッドプールの監視
スレッドプールから提供されるパラメータで監視します.スレッドプールには、スレッドプールを監視するときに使用できるプロパティがあります.
taskCount:スレッドプールで実行するタスクの数.
completedTaskCount:スレッドプールが実行中に完了したタスクの数.taskCount以下.
LargestPoolSize:スレッドプールが作成した最大スレッド数.このデータにより、スレッドプールが満たされているかどうかを知ることができます.スレッドプールの最大サイズに等しい場合は、スレッドプールがいっぱいになったことを示します.
getPoolSize:スレッドプールのスレッド数.スレッドプールが破棄されなければ、プール内のスレッドは自動的に破棄されないので、このサイズは+getActiveCount:アクティブなスレッド数を取得するだけ増加しません.
スレッドプールを拡張して監視します.スレッドプールを継承し、スレッドプールのbeforeExecute、afterExecute、terminatedメソッドを書き換えることで、タスクの実行前、実行後、スレッドプールが閉じる前に何かをすることができます.タスクの平均実行時間、最大実行時間、最小実行時間などを監視します.このいくつかの方法はオンラインプールでは空の方法です.次のようになります.
protected void beforeExecute(Thread t, Runnable r) { }