詳細なスレッドプールThreadPoolExecutor

免責事項:この記事はブロガーオリジナル記事です、続くBY-SAのCC 4.0を著作権契約、複製、元のソースのリンクと、この文を添付してください。
このリンク: https://blog.csdn.net/ThinkWon/article/details/102541900

なぜ、スレッドプールを使用します

適切に管理されないスレッドが簡単にシステムの問題につながることができれば、実際の使用では、スレッドは、システムリソースを占有しています。このように、同時フレームワークが使用するほとんどのスレッドプールをスレッドを管理するために、スレッドプール管理スレッドは、主に次のような利点があります。

  1. リソース消費量を削減多重化することにより、スレッドと既存のスレッドの数は、できるだけ多くのシステムパフォーマンスの損失を低減するために閉じ減らします。
  2. システムの応答性を向上させますしたがって、スレッドを再利用するスレッドを作成するプロセスを排除し、そしてすることによって、システムの全体的な応答速度を向上させます。
  3. スレッドには、管理性を向上させますスレッドは希少資源である、無限の創造場合、システムリソースを消費するだけでなく、システムの安定性を減らすため、スレッドを管理するスレッドプールを使用する必要はありませんのみ。

詳細なスレッドプール

スレッドプールを作成します。

主にあるスレッドプールを作成します。ThreadPoolExecutor完了するために、クラス、コンストラクタをオーバーロードされた多くのThreadPoolExecutorコンストラクタによってほとんどのパラメータがあり、スレッド・プールを作成するように設定する必要があるパラメータを理解することがあります。ThreadPoolExecutorコンストラクタメソッド:

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

次のパラメータについて説明します。

  1. corePoolSizeは:コアスレッドプールのサイズを示しています。スレッドプール内のスレッドの現在の数は、コアcorePoolSizeに達しない場合は、ジョブをサブミットすると、現在のコアスレッドプールアイドルスレッドがあっても提出されたタスクを実行するための新しいスレッドを作成します。スレッドの現在のコア数はスレッドプールにcorePoolSizeに達した場合、それがスレッドを再作成されていません。あなたが呼び出す場合prestartCoreThread()prestartAllCoreThreads()、スレッド・プールのすべてのコアスレッドが作成され、発売されるときに作成されます。
  2. maximumPoolSizeは:スレッドプールは、スレッドの最大数を作成することができることを示しています。キューがいっぱいになる、そして現在は、スレッドプールのスレッドmaximumPoolSizeの数を超えていない場合にはブロックされた場合は、タスクを実行するために、新しいスレッドを作成します。
  3. keepAliveTimeが:アイドルスレッドの生存期間。スレッドプール内のスレッドの現在の数がcorePoolSizeを超えている、とのスレッドがkeepAliveTimeがより多くのためのアイドル状態であれば、それは可能な限り、システムリソースの消費を減らすことができ、これらのアイドルスレッドを破壊するであろう。
  4. 単位:時間の単位。指定された時間単位はkeepAliveTimeがあります。
  5. ワークキュー:キューを遮断します。キューをブロックの上にタスクを保存するためのキューをブロックするこの記事を見ることができますあなたは使用することができますArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue
  6. threadFactory:ファクトリクラスのスレッドを作成します。あなたは、問題の原因を見つけるのも簡単並行性の問題ならば、スレッド名のうち、各工場ごとに作成されたスレッドを指定することで、より有意義に設定することができますが。
  7. ハンドラ:飽和戦略。キューをブロックしたスレッド・プールがいっぱいで、指定されたスレッドが現在のスレッド・プールがすでに飽和していることを示す、開かれている場合は、我々はこのような状況に対処するための戦略を採用する必要があります。使用戦略のこれらの種類は次のとおりです。
    1. AbortPolicy:直接拒否タスク提出、およびスローRejectedExecutionExceptionに例外を。
    2. CallerRunsPolicy:のみに配置され、呼び出し元のスレッドでタスクを実行します。
    3. DiscardPolicy:直接廃棄されたタスクを扱いません。
    4. DiscardOldestPolicy:現在のタスクのブロッキングキュー記憶を捨て最長のタスクの実行

実行論理スレッドプール

スレッドプールが作成したThreadPoolExecutor、タスク実行プロセス何を提出した後、のは、ソースコードを見てみましょう。次のようにソースは、メソッドを実行します。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * 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.
     */
    int c = ctl.get();
	//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
	//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
	//如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务
    else if (!addWorker(command, false))
        reject(command);
}

ThreadPoolExecutorは、メソッドの実行ロジックは、コメントを参照してください実行します。ThreadPoolExecutorの実行方法は、以下の概略図を示して行います。

ここに画像を挿入説明

実行ロジックは、メソッドを実行するいくつかの状況があります。

  1. スレッドが現在corePoolSize未満を実行している場合は、新しいタスクを実行する新しいスレッドを作成します。
  2. スレッドの実行数がcorePoolSize以上である場合、タスクは、ワークキューに格納されているブロッキングキューに提出されます。
  3. 現在のワークキューキューがいっぱいの場合、これはタスクを実行する新しいスレッドを作成します。
  4. スレッドの数がmaximumPoolSizeを超えた場合、飽和戦略のRejectedExecutionHandlerを処理するために使用されます。

これは、スレッドプールの設計を使用することであることに留意すべきであるキューのワークキューとスレッドプールのスレッドmaximumPoolSizeの最大数を遮断するコアスレッドプールcorePoolSizeを、タスクを処理するために、このキャッシュ戦略は、実際には、このような設計は、フレームワークで使用される必要があるであろう。

スレッドプールを閉じます

スレッドプールを閉じて、することができますshutdownし、shutdownNowこれらの2つの方法。彼らの原則は、割り込みスレッドに続く、プール内のすべてのスレッド、通過しています。shutdownそして、shutdownNow同じ場所ではありません。

  1. shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;
  2. shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程

可以看出shutdown方法会将正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown方法都会返回true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated方法才会返回true。

线程池的工作原理

当一个并发任务提交给线程池,线程池分配线程去执行任务的过程如下图所示:

ここに画像を挿入説明

从图可以看出,线程池执行所提交的任务过程主要有这样几个阶段:

  1. 先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步;
  2. 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步;
  3. 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理

线程池阻塞队列

作用:用来存储等待执行的任务

线程的公平访问队列:指阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞先访问线程。为了保证公平性,通常会降低吞吐量

常见阻塞队列

ArrayBlockingQueue:一个用数组实现的有界阻塞队列,按照先入先出(FIFO)的原则对元素进行排序。
不保证线程公平访问队列,使用较少

PriorityBlockingQueue:支持优先级的无界阻塞队列,使用较少

LinkedBlockingQueue:一个用链表实现的有界阻塞队列,队列默认和最长长度为Integer.MAX_VALUE。
队列按照先入先出的原则对元素进行排序,使用较多

  • 吞吐量通常要高于 ArrayBlockingQueue
  • Executors.newFixedThreadPool() 使用了这个队列

SynchronousQueue:不储存元素(无容量)的阻塞队列,每个put操作必须等待一个take操作,
否则不能继续添加元素。支持公平访问队列,常用于生产者,消费者模型,吞吐量较高,使用较多

  • 每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
  • 吞吐量通常要高于 LinkedBlockingQueue
  • Executors.newCachedThreadPool使用了这个队列

线程池的饱和策略

定义:当队列和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理新提交的任务。

常见策略

AbortPolicy:中断策略,直接抛出异常

CallerRunsPolicy:调用者运行策略,让调用者所在线程来运行策略

DiscardOldestPolicy:舍弃最旧任务策略,丢弃队列中最旧的任务,然后重试任务的提交执行( execute() )

DiscardPolicy:舍弃策略,不处理,直接丢弃

自定义策略

如何合理配置线程池参数?

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置CPU个数+1的线程数的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如配置两倍CPU个数+1。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。

当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

スレッドの最適数 =(スレッド待機スレッドCPU時間比率+ 1)CPUの数*

結論を引き出すことができます
スレッドがより高い割合を待って、より多くの必要性はスレッドします。CPU時間の割合が高いスレッド、少数のスレッドを必要としています。
基本的には上記式スレッドの前に設定CPUとIO集中型タスクの数。

参考文献「アートのJava並行プログラミング。」

おすすめ

転載: blog.csdn.net/ThinkWon/article/details/102541900