Javaスレッドプールの迅速な理解

11461 ワード

私のブログ
1.概要
Threadクラスを使用してタスクを実行すると、タスクの実行時に毎回スレッドが作成され、タスクが終了するとスレッドが破棄されます.システムにとって、スレッドはリソースだけでなく、スレッドの作成と破棄もシステムのリソースを消費します.この問題に対して、直接的な解決策は、スレッドを多重化し、スレッドがタスクを実行した後も破棄ではなく他のタスクを実行し続けることができるようにすることであることは明らかです.スレッドプールでは、このようなソリューションが提供されます.
スレッドプールとは、作成後は一般的に破棄されず、新しいタスクが実行されるまで休止状態に入り、実行を再開するスレッドのセットです.
2.スレッドプールの作成
java.util.concurrent(JUC)パッケージには、スレッドプールの作成に多くのインタフェースが用意されています.
2.1 ExecutorServiceインタフェース
スレッドプールは、通常、管理を終了する方法と、Futureを生成する方法を提供するExecutorServiceを指す.簡単に言えば、ExecutorインターフェースはExecutorServiceインターフェースを継承し、ExecutorExecutorServiceによって提供されるexecuteメソッドに加えて、管理を終了する多くの方法と、スレッドの実行結果を追跡するためのFutureを生成するsubmitを提供する.以下に示す

boolean awaitTermination(long timeout, TimeUnit unit)

 List> invokeAll(Collection extends Callable> tasks)

 List> invokeAll(Collection extends Callable> tasks, long timeout, TimeUnit unit)

 T   invokeAny(Collection extends Callable> tasks)

 T   invokeAny(Collection extends Callable> tasks, long timeout, TimeUnit unit)

boolean isShutdown()

boolean isTerminated()

void    shutdown()

List  shutdownNow()

 Future   submit(Callable task)

Future>   submit(Runnable task)

 Future   submit(Runnable task, T result)

2.2 ThreadPoolExecutorによるスレッドプールの直接作成
JUCは、ExecutorServiceインタフェースを実装するAbstractThreadPoolExecutorを継承するスレッドプールを作成するためのThreadPoolExecutorクラスを提供します.ThreadPoolExecutorにはいくつかの構造パラメータがあります.一般的には、次のようにスレッドプールを作成するために呼び出すことができます.
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)

コードは次のとおりです.
ExecutorService executor = new TreadPoolExecutor(5, 10, 10L,TimeUnit.SECONDS,new BlockingArrayQueue(40));

各パラメータの意味は次のとおりです.
  • Executorスレッドプールでは、allowCoreThreadTimeOut(true)
  • が呼び出されない限り、スレッドがアイドルであっても生存するスレッドの数を維持します.
  • corePoolSizeスレッドプールで作成できる最大スレッド数
  • corePoolSizeアイドルスレッドの生存時間
  • keepAliveTime KeepAliveTimeパラメータの単位、例えばTimeUnit.SECONDES等
  • unitコミットされたrunnableのタスクを格納するキューで、executeメソッドでコミットされたrunnableタスク
  • のみが実行されます.
    上記のパラメータの説明から、ThreadPoolExecutorによって作成されたスレッドプールは自動的にサイズを調整できることがわかります.新しいタスクがコミットされると、スレッドプール内のスレッドがworkQueue未満の場合、他のスレッドがアイドル状態であっても、スレッドプールは新しいスレッドを作成してタスクを実行します.このとき、スレッドプールにcorePoolSizeより大きいがcorePoolSizeより小さいスレッド数が存在する場合、maximumPoolSizeが満タンでない限り、新しいスレッドは作成されません.したがって、workQueuecorePoolSizeを等しく設定することによって、固定スレッド数のスレッドプールを作成することもできるし、maximumPoolSizeのような実質的に境界のない値を設定することによって、任意の同時スレッド数を収容できるスレッドプールを作成することもできる.一般的には、Integer.MAX_VALUEおよびcorePoolSizeのスレッドプール構造が決定されていますが、maximumPoolSizeおよびsetCorePoolSize(int)によってこの2つの値を動的に設定することもできます.
    2.3スレッドプールにおけるスレッドの作成
    スレッドプールはThreadFactoryを使用して新しいスレッドを作成します.特に指定されていない場合は、setMaximumPoolSize(int)を使用して、すべてのスレッドを同じThreadGroupに作成し、同じExecutors.defaultThreadFactory()優先度と非デーモン・プロセス・ステータスを有します.異なるThreadFactoryを提供することで、スレッドの名前、スレッドグループ、優先度、デーモンステータスなどを変更できます.NORM_PRIORITYがnewThreadを呼び出したときにThreadFactoryに戻ってスレッドの作成に失敗した場合、executorは続行しますが、タスクを実行できない場合があります.スレッドには「modifyThread」のRuntimePermissionがあるはずです.スレッド・プールを使用しているワーク・スレッドまたは他のスレッドにこの権限がない場合、サービスはダウングレードされます.構成の変更がタイムリーに有効にならない可能性があります.また、スレッド・プールを閉じると、終了できますが完了していない状態が維持されます.
    2.4スレッド生存時間
    プールが現在nullを超えるスレッドを有する場合、余分なスレッドがcorePoolSizeを超える場合、終了します(keepAliveTimeを参照).これにより、プールを使用しないときにリソース消費量を削減する方法が提供されます.スレッドプールが後でアクティブになると、新しいスレッドが構築されます.方法setKeepAliveTimeも使用できます.(long,TimeUnit)このパラメータを動的に変更します.getKeepAliveTime(TimeUnit)Long.MAX_VALUEのような値を使用すると、アイドルスレッドのオンライン・スレッド・プールが閉じる前に終了することが有効になります.デフォルトでは、corePoolSizeより大きいスレッドが存在する場合にのみ、アクティブなポリシーを維持できます.ただし、keepAliveTime値がゼロ.
    2.5ワークキュー
    任意のBlockingQueueは、コミットされたタスクを転送および保存するために使用できます.このキューの使用は、プール・サイズと相互に影響します.
  • 実行中のスレッドがcorePoolSizeより少ない場合、Executorは常にタスクをキューに追加するのではなく、新しいスレッドを追加するのが好きです.
  • corePoolSizeまたはそれ以上のスレッドが実行されている場合、Executorは常に新しいスレッドを追加するのではなく、キューリクエストを好む.
  • キューがいっぱいになった場合、maximumPoolSizeを超えない限り、新しいスレッドが作成されます.この場合、タスクは拒否されます.

  • キューの選択には、次の3つの一般的なポリシーがあります.
  • 直接引き継ぎます.ワークキューのデフォルトの選択はTimeUnit.NANOSECONDSであり、タスクをスレッドに渡し、別途保存しない.タスクを実行するためにスレッドがすぐに使用できない場合は、新しいタスクを追加しようとすると失敗し、新しいスレッドが構築されます.このポリシーは、内部依存性のあるリクエストセットを処理するときにロックを回避します.直接引継ぎ法は、通常、新しいコミットされたタスクを拒否することを避けるために、境界のないallowCoreThreadTimeOut(boolean)を必要とする.これは、タスクが処理よりも速い速度で到達すると、スレッドが無制限に増加する可能性があることを意味します.
  • 無限キュー.無制限キューを使用すると(たとえば、事前に定義された容量を持たないLinkedBlockingQueue)、新しいタスクは空きスレッドがない場合にキュー内で待機します.したがって、スレッドプールにはcorePoolSize数のスレッドしか作成されません.(SynchronousQueueの値なので効果はありません).各タスクが他のタスクから完全に独立している場合、これは適切である可能性があります.したがって、タスクは相互の実行に影響を与えません.例えば、ウェブサーバでは、このキューは、瞬時バーストのスムーズ化に役立ちますが、これは、タスクが平均到達速度が処理可能速度を超える場合に、ワークキューは無制限に増加する可能性があります.
  • の限られたキュー.有限キュー(ArrayBlockingQueueなど)は、有限maximumPoolSizesと一緒に使用すると、リソースの消費を防ぐのに役立ちますが、調整や制御が困難になる場合があります.キューサイズと最大プールサイズの間では、互いに折衷できます.(trade off):大規模なキューと小型プールを使用すると、CPUの使用率、OSリソース、コンテキスト切替のオーバーヘッドを最小限に抑えることができますが、人為的な低スループットを引き起こす可能性があります.タスクが常にブロックされている場合(例えば、実行時にI/O操作がある場合)、システムはmaximumPoolSizesよりも多くのスレッドスケジューリング時間を提供することができる.小さなキュー列を使用すると、通常より大きなプールサイズが必要となり、CPUがより多忙になるが、許容できないスケジューリングオーバーヘッドが発生し、スループットが低下する可能性がある.
  • 2.6タスクの発行
    タスクをコミットするには、次の2つの方法があります.1つは、戻り値のないタスクをコミットする方法です.
    ExecutorService executor = new TreadPoolExecutor(5, 10, 10L,TimeUnit.SECONDS,new BlockingArrayQueue(40));
    Runnable handler = ......;
    executor.execute(handler);

    タスクの実行結果を取得する必要がある場合は、submitメソッドを呼び出してタスクをコミットできます.コードは次のとおりです.
    ExecutorService executor = new TreadPoolExecutor(5, 10, 10L,TimeUnit.SECONDS,new BlockingArrayQueue(40));
    Callable handler = ......;
    Future future = executor.submit(handler);

    2.7スレッドプールの完全なインスタンスの作成
    import java.util.concurrent.*;
    
    public TreadPoolTest(){
        public static void main(String[] args){
            ExecutorService executor = new TreadPoolExecutor(5, 10, 10L,TimeUnit.SECONDS,new ArrayBlockingQueue(40));
            for(int i = 0;i < 10; i++){
                Handler handler = new Handler();
                executor.execute(handler);
            }
            executor.shutdown();
    
        }
    
        class Handler extends Runnable{
            public void run(){
                for(int i = 0; i < 5;i++){
                    System.out.println(i);
                }
            }
        }
    }

    3 Executorのファクトリメソッドによるスレッドプールの作成
    ExecutorsはJUCパッケージの下にあるファクトリクラスで、スレッドプールの作成に便利な方法を提供しています.
    3.1固定サイズスレッドプールの作成
    Executorsクラスを使用して、固定サイズのスレッドプールを作成できます.
    ExecutorService executor = Executors.newFixedThreadPool(10);

    固定サイズのスレッドプールは、ThreadPoolExecutorコアスレッド数と最大スレッド数を等しく設定することで実現できますが、実際にはソースコードもそうです.
    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue());
        }

    一般に、スレッド数がコアスレッド数に達すると、スレッドの作成もスレッドの破棄も行われません.異常なスレッド終了が発生したり、allowCoreTimeOutを設定したりしない限り、keepAliveパラメータが0に設定されているため、スレッドの作成と破棄が頻繁になる可能性があります(ここでは推測のみです.検証が必要です).
    3.2キャッシュ可能なスレッドプールの作成:maximumPoolSizeを使用してキャッシュ可能なスレッドプールを作成できます.現在のスレッドプールの規模が処理要件を超えている場合、空のスレッドが回収されます.需要が増加すると、スレッドの数が増加します.スレッドプールの規模に制限はありません.コードは次のとおりです.
    ExecutorService executor = Executors.newCachedThreadPool(10);

    前述したように、maximumPoolSizesパラメータは、Executors.newCachedThreadPoolのように実質的に境界のない値に設定することができ、実際にはソースコードも同様に実現される.
    public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            SynchronousQueue());
        }

    3.3単一スレッドのスレッドプールの作成
    単一スレッドのExecutorをmaxmumPoolSizeで作成し、タスクがシリアルで実行されることを確認します.
    ExecutorService executor = Executors.newSingleExecutor();
    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue()));
        }

    4まとめ
    この記事では、スレッドプールの作成方法、各パラメータの意味と役割について詳しく説明しますが、スレッドプールには、上記のほかにスケジューリング可能なスレッドプールがあります.このシリーズの次の記事では、このスレッドプールについて説明します.