スレッドプールエグゼキュータ

新しいスレッドのデメリット

  • 新しいスレッドが新しいオブジェクトを作成するたびに、パフォーマンスが低下します
  • スレッドには統一された管理がなく、新しいスレッドが無制限に存在し、互いに競合し、システムリソースを占有しすぎて、クラッシュやOOM(メモリ不足)が発生する可能性があります。(この問題の理由は、単に新しいスレッドではなく、プログラムのバグまたは設計上の欠陥による継続的な新しいスレッドが原因である可能性があります)
  • より多くの実行、定期的な実行、スレッドの中断など、より多くの機能の欠如。

スレッドプールの利点

  1. リソース消費を削減します作成されたスレッドを再利用して、スレッドの作成と終了のオーバーヘッドを減らします。
  2. 応答速度を向上させます。タスクが到着すると、スレッドが作成されるのを待たずにすぐに実行できます。
  3. スレッドの管理性を向上させます。スレッドは希少なリソースです。無制限に作成すると、システムリソースを消費するだけでなく、システムの安定性も低下します。スレッドプールは、均一な割り当て、調整、および監視に使用できます。(同時制御、タイミング/定期実行など)

スレッドプールのクラス図

スレッドプールのクラス図
下部のエグゼキュータが一般的に使用され、スレッドプールを作成してスレッドを使用するために使用します。
Executorフレームワークは、一連の実行戦略に従って非同期タスクの実行と制御をスケジュールします。目的は、タスクの送信をタスクの操作から分離するメカニズムを提供することです。

  • エグゼキュータ:新しいタスクを実行するためのシンプルなインターフェイス
  • ExecutorService:Executorを拡張しエグゼキュータのライフサイクルとタスクのライフサイクルを管理するメソッドを追加します
  • ScheduleExcutorService:将来のタスクスケジュールされたタスクをサポートするための拡張ExecutorService

スレッドプールの実現原理

処理フロー

corePoolSize、maximumPoolSize、およびworkQueue間の関係

  • corePoolSize(コアスレッドの数、基本スレッドの数):スレッドプールにタスクを送信すると、スレッドプールはタスクを実行するためのスレッドを作成します(アイドル状態の基本スレッドがある場合でも。タスクの数がcorePoolSize、作成されません)。スレッドプールprestartAllCoreThreads()メソッドが呼び出されると、スレッドプールはすべての基本スレッドを事前に作成して開始します。
  • runnableTaskQueue(タスクキュー):workQueueブロッキングキュー、実行を待機しているタスクを格納します(実行中のスレッドの数がcorePoolSizeより大きくmaximumPoolSizeより小さい場合)
  • maximumPoolSize:スレッドの最大数。キューがいっぱいで、作成されるスレッドの数が最大スレッド数より少ない場合、スレッドプールはタスクを実行するための新しいスレッドを作成します。(無制限のタスクキューが使用されている場合、作成できるスレッドの最大数はcorePoolSizeであり、maximumPoolSizeは機能しません)

スループット:SynchronousQueueはLinkedBlockingQueueおよびArrayBlockingQueueよりも高い
ThreadPoolExecutor実行の概略図

ThreadPoolExecutorがexecute()を実行する4つの状況

1)現在実行中のスレッドがcorePoolSizeより小さい場合は、タスクを実行するための新しいスレッドを作成します(グローバルロックを取得する必要があります)。
2)実行中のスレッドがcorePoolSize以上の場合、タスクはBlockingQueueに追加されます。
3)タスクをBlockingQueueに追加できない場合(キューがいっぱい)、タスクを処理するための新しいスレッドが作成されます(グローバルロックを取得する必要があります)。
4)新しいスレッドを作成すると、現在実行中のスレッドがmaximumPoolSizeを超える場合、タスクは拒否され、RejectedExecutionHandler.rejectedExecution()メソッドが呼び出されます。

设计思路、execute()メソッドを実行するときに、グローバルロック(深刻なスケーラビリティのボトルネックをできるだけ取得しないようにすることです。ThreadPoolExecutorがウォームアップを終了した後(現在実行中のスレッドの数がcorePoolSize以上)、ほとんどすべてのexecute()メソッド呼び出しがステップ2で実行され、ステップ2はグローバルロックを取得する必要はありません。

jdk版本不同会有区别
public void execute(Runnable command) {
    
    
        if (command == null)
            throw new NullPointerException();
         //只有当前线程池中线程数poolSize <corePoolSize 时,则创建线程并执行当前任务 
        //当poolSize >=corePoolSize 或线程创建失败,则将当前任务放到工作队列中。  
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
    
       
            if (runState == RUNNING && workQueue.offer(command)) {
    
    
                //如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,则创建一个线程执行任务。
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
           //抛出RejectedExecutionException异常
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // is shutdown or saturated
        }
    }

ワーカースレッド:スレッドプールがスレッドを作成すると、スレッドがワーカースレッドワーカーにカプセル化されます。ワーカーがタスクの実行を終了すると、ループで実行するためにワークキュー内のタスクも取得されます(GCではありません)。)。Workerクラスのrun()メソッド:

 public void run() {
	    try {
	    	Runnable task = firstTask;
	    	firstTask = null;
	    	while (task != null || (task = getTask()) != null) {
		    	runTask(task);
		    	task = null;
	    	}
	    } finally {
	    	workerDone(this);
    	}
    }

スレッドプールの作成

ThreadPoolExecutorを介してスレッドプールを作成できます。

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long  keepAliveTime,TimeUnit unit  ,
BlockingQueue<Runnable> workQueue ,ThreadFactory threadFactory, RejectedExecutionHandler handler);

ThreadFactory:スレッドファクトリ。スレッドを作成するように設定されています。スレッドファクトリを介して、作成された各スレッドの名前を設定できます。オープンソースフレームワークguavaが提供するThreadFactoryBuilderは、スレッドプール内のスレッドに意味のある名前をすばやく設定できます。コードは次のとおりです。

new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

RejectedExecutionHandler(飽和戦略):キューとスレッドプールがいっぱいになり、スレッドプールが飽和状態にあることを示す場合、送信された新しいタスクを処理するための戦略を採用する必要があります。

.AbortPolicy:直接抛出异常。
·CallerRunsPolicy:使用 【调用者 dubbo生产者主线程】所在线程(execute 方法的调用线程) 来运行任务(可能会影响主线程)。
·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
·DiscardPolicy:不处理,丢弃掉。

アプリケーションシナリオのニーズに応じて、RejectedExecutionHandlerインターフェイスのカスタム戦略を実装することもできます。ロギングや永続ストレージなどはタスクを処理できません。

keepAliveTime(スレッドアクティビティ保持時間):スレッドプールのワーカースレッドがアイドル状態になった後も存続する時間。したがって、タスクが多く、各タスクの実行時間が比較的短い場合は、時間を調整してスレッドの使用率を高めることができます。

TimeUnit:keepAliveTimeの時間単位

ファクトリクラスエグゼキュータの3つの静的メソッドの詳細な説明

スレッドプールにタスクを送信する

execute()メソッドは、戻り値を必要しないタスクを送信するために使用されるため、タスクがスレッドプールによって正常に実行されたかどうかを判断することはできません
次のコードは、execute()メソッドによって入力されたタスクがRunnableクラスのインスタンスであることを示しています。

threadsPool.execute(new Runnable() {
	@Override
	public void run() {
		// TODO Auto-generated method stub
	}
});

submit()メソッドは、戻り値必要とするタスクを送信するために使用されますスレッドプールはfuture型オブジェクトを返します。このfutureオブジェクトを介して、タスクが正常に実行されたかどうかを判断でき、futureのget()メソッドを介して戻り値を取得できます。get()メソッドはブロックします。タスクが完了するまで現在のスレッドを取得し、get(long timeout、TimeUnit unit)メソッドを使用すると、現在のスレッドが一定期間ブロックされてすぐに戻ります。この時点では、タスクが完了していない可能性があります。

Future<Object> future = executor.submit(harReturnValuetask);
	try {
		Object s = future.get();//获取返回值
	} catch (InterruptedException e) {
		// 处理中断异常
	} catch (ExecutionException e) {
		// 处理无法执行任务异常
	} finally {
		// 关闭线程池
		executor.shutdown();
	}

スレッドプールを閉じる

スレッドプールのライフサイクル
shutdownまたはshutdownNowメソッドは、スレッドプールを閉じます。
原則:スレッドプール内のワーカースレッドをトラバースし、スレッドの割り込みメソッドを1つずつ呼び出してスレッドを中断するため、中断に応答できないタスクが終了することはありません。

違い

  • shutdownNow()は、スレッドプールの状態をSTOPに設定してから、タスクを実行または一時停止しいるすべてのスレッドを停止しようとし、実行を待機しているタスクのリストを返します。
  • shutdown()は、スレッドプールの状態をSHUTDOWN状態に設定してから、タスク実行していないすべてのスレッドに割り込みます

その他のステータス

  • SHUTDOWNは新しいタスクを処理できませんが、ブロッキングキュー内のタスクを処理し続けることができます
  • 停止:新しいタスクを受信することも、キュー内のタスクを処理することもできません
  • 整理:すべてのタスクが終了した場合、有効なスレッドの数は0です。
  • 終了:最終状態
  /**
     * Initiates an orderly shutdown in which previously submitted
     * tasks are executed, but no new tasks will be accepted.
     * Invocation has no additional effect if already shut down.
     */
    public void shutdown() {
    
    
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
    
    
            mainLock.unlock();
        }
        tryTerminate();
    }

  /**
     * Attempts to stop all actively executing tasks, halts the
     * processing of waiting tasks, and returns a list of the tasks
     * that were awaiting execution. These tasks are drained (removed)
     * from the task queue upon return from this method.
     */
    public List<Runnable> shutdownNow() {
    
    
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
    
    
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

スレッドプールを合理的に構成する

タスクの特性を最初に分析する必要があります。これは、次の観点から分析できます。

  • タスクの性質:CPUを集中的に使用するタスク、IOを集中的に使用するタスク、および混合タスク。
  • タスクの優先度:高、中、低。
  • タスクの実行時間:長、中、短。
  • タスクの依存関係:データベース接続などの他のシステムリソースに依存しているかどうか。

さまざまな性質のタスクは、さまざまなサイズのスレッドプールによって個別に処理できます。

  • Ncpu + 1スレッドのスレッドプールの構成など、CPUを集中的に使用するタスクは、できるだけ少ないスレッドで構成する必要があります。
  • IOを多用するタスクスレッドは常にタスクを実行するとは限らないため、2 * Ncpuなどのできるだけ多くのスレッドを構成する必要があります。
  • 混合タスクを分割できる場合は、CPU集約型タスクとIO集約型タスクに分割します。2つのタスクの実行の時間差が大きすぎない限り、分解後のスループットはより高くなります。シリアル実行のスループット。これら2つのタスクの実行時間が大きく異なる場合は、分解する必要はありません。
  • スレッドはSQLの送信後にデータベースが結果を返すのを待つ必要があるため、データベース接続プールのタスクに依存します。待機時間が長くなるほど、CPUのアイドル時間が長くなります。次に、スレッドの数を大きく設定する必要があります。 、CPUをより有効に活用するため。

現在のデバイスのCPUの数は、Runtime.getRuntime()。availableProcessors()メソッドを介して取得できます。
優先度の異なるタスクは、優先度キューPriorityBlockingQueueを使用して処理できます。
:キューに送信される優先度の高いタスクが常に存在する場合、優先度の低いタスクが実行されることはありません。

制限付きキューの
最大maximumPoolSizeを使用することをお勧めします。これにより、システムの安定性と早期警告機能が向上し、リソース消費が削減されます。ただし、この方法では、スレッドプールのスレッドスケジューリングがより困難になります。
[スレッドプールスループット率] [処理タスク]を妥当な範囲に到達させ、[スレッドスケジューリングが比較的簡単] [リソース消費を可能な限り削減] [スレッドプール]と[キュー容量]を合理的に制限したい。

割り当てスキル

  1. リソース消費の削減[CPU使用率、オペレーティングシステムのリソース消費、コンテキスト切り替えのオーバーヘッド] [キュー容量を大きくする] [スレッドプール容量を小さくする]を設定して、スレッドプールのスループットを削減します
  2. 送信されたタスクはブロックされることがよくあります。maximumPoolSizeを調整できます
  3. キュー容量が小さく、CPU使用率が比較的高くなるようにスレッドプールサイズを大きく設定する必要があります
  4. スレッドプールの容量の設定が大きすぎてタスクの数が多すぎると、同時実行の量が増えるため、スレッド間のスケジューリングを考慮する必要があります。これにより、処理タスクのスループットが低下する可能性があります。

スレッドプールの監視

・taskCount:スレッドプールが実行する必要のあるタスクの数。
・completedTaskCount:実行中のプロセス中にスレッドプールによって完了されたタスクの数。taskCount以下です。
・LargestPoolSize:スレッドプールでこれまでに作成されたスレッドの最大数。このデータから、スレッドプールがいっぱいになっているかどうかを知ることができます。値がスレッドプールの最大サイズと等しい場合は、スレッドプールがいっぱいになったことを意味します。
・getPoolSize:スレッドプール内のスレッドの数。スレッドプールが破棄されない場合、スレッドプール内のスレッドは自動的に破棄されないため、サイズは増加するだけです。
・getActiveCount:アクティブなスレッドの数を取得します。

スレッドプールを拡張して監視します。スレッドプールを継承することでスレッドプールをカスタマイズしたり、スレッドプールのbeforeExecute、afterExecute、terminateメソッドを書き直したり、タスクの実行前、実行後、実行前、スレッドプールを閉じる前に監視するコードを実行したりできます。 。たとえば、タスクの平均実行時間、最大実行時間、および最小実行時間を監視します。

protected void beforeExecute(Thread t, Runnable r) {
	这几个方法在线程池里是空方法。
 }

おすすめ

転載: blog.csdn.net/eluanshi12/article/details/85232483