エントリから高度な(8)までのマルチスレッド-スレッドプール管理-スレッドプール

1つは、Callableを介してスレッドを作成する

1.1違い

class Thread1 implements Runnable{
    
    
    @Override
    public void run() {
    
    

    }
}

class Thread2 implements Callable<String>{
    
    
    @Override
    public String call() throws Exception {
    
    
        return null;
    }
}
  1. これは、Runnableインターフェイスを実装してスレッドを作成し、Callableインターフェイスを実装するcallメソッドを書き換えるrunメソッドです。
  2. Callableインターフェースを実装して、例外をスローする可能性のあるプロセスを作成します
  3. Callableインターフェースを実装して、戻り値を持つプロセスを作成します。戻り値は、ジェネリックと一致している必要があります。

1.2スレッドを開始します

では、Callableを実装するスレッドを作成するにはどうすればよいでしょうか。公式ドキュメントを見ると、Callableを使用して作成されたスレッドはRunnableに類似しているため、Theradで開始できます。

画像-20201212201203490

スレッドのCallableインターフェースに直接渡すことができるパラメーターはありません。

画像-20201213102530382

スレッドでCallableを使用するには、RunnableとCallableを実装するこの種のインターフェイスまたはクラスを想像できますか?そしてFutureTaskはこの種のクラスです。最初にRunnableインターフェースを実装し、次に構築メソッドによってCallableインターフェースの実装クラスを渡すことができます。

画像-20201213103047068

FutureTask(Callable<V> callable) 

次に、Callableスレッドを開始するのは簡単です

class Thread2 implements Callable<String>{
    
    
    @Override
    public String call() throws Exception {
    
    
        System.out.println("线程启动了");
        return null;
    }
}

public class CallableTest {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        FutureTask<String> task = new FutureTask<>(new Thread2());
        new Thread(task,"A").start();
    }
}

1.3戻り値を取得する

getメソッドを呼び出して戻り値を取得します

FutureTask<String> task = new FutureTask<>(new Thread2());
new Thread(task,"A").start();
task.get();

1.4原則

getメソッドthreadを呼び出すと、スレッドの実行が終了するまでブロックされ、戻り値が返されます。メインスレッドでより時間のかかる操作を実行する必要があるが、メインスレッドをブロックしたくない場合は、これらのタスクをFutureオブジェクトに渡して、バックグラウンドで完了することができます。メインスレッドで将来必要になった場合は、 Futureオブジェクトまたは実行ステータスを介してバックグラウンドジョブの計算結果を取得できます。

FutureTask<Integer> futureTask = new FutureTask(()->{
    
    
    System.out.println(Thread.currentThread().getName()+"  come in callable");
    TimeUnit.SECONDS.sleep(4);
    return 1024;
});
new Thread(futureTask,"A").start();

while(!futureTask.isDone()){
    
    
    System.out.println("***wait");
}
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName()+" come over");

スレッドは、より時間のかかるタスクをシミュレートおよび計算するために上記で開始され、最終結果が返されます。メインスレッドでは、isDoneメソッドが実行が終了したかどうかを判断し、実行終了(完了/キャンセル/異常)が返されます。 true。上記のプログラムはgetを呼び出すことを証明できます。メソッドはブロックされ、Aスレッドのみが結果を返し、実行を継続します。

画像-20201213110946521また、タスクタスクは1回だけ計算されることに注意してください。次のコードを参照してください。

FutureTask<Integer> futureTask = new FutureTask(()->{
    
    
    System.out.println(Thread.currentThread().getName()+"  come in callable");
    TimeUnit.SECONDS.sleep(4);
    return 1024;
});
new Thread(futureTask,"A").start();
while(!futureTask.isDone()){
    
    
    System.out.println("***wait");
}
System.out.println(futureTask.get());
System.out.println("--------");
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName()+" come over");

画像-20201213111158770

2回目の取得では、スレッドは4秒間スリープしませんでした。直接返されるので、各タスクは1回だけ計算されることがわかります。

総括する:

一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。 一个task任务只计算一次,所以一般将get方法放到最后。

2、スレッドプール

2.1スレッドプールの利点

スレッドプールと言えば、データベース接続プールc3p0やその他のデータベース接続プールテクノロジを考えるのは簡単です。データベース接続プールとスレッドプールの利点は同じです。

  • リソース消費の削減:作成されたスレッドを再利用することにより、スレッドの作成と破棄によって引き起こされる消費を削減します
  • 応答速度の向上:タスクが到着すると、スレッドが作成されるのを待たずにタスクをすぐに実行できます
  • スレッドの管理性の向上:スレッドはリソースが不足しています。無制限に作成すると、システムリソースを消費するだけでなく、システムの安定性も低下します。スレッドプールテクノロジは、統合された割り当て、調整、および監視に使用できます。

2.2エグゼキュータの概要

Executorフレームワークは、主に次の3つの部分で構成されています。

  • タスク:実行されたタスクが実装する必要のあるインターフェースを含む:実行可能インターフェースと呼び出し可能インターフェース
  • タスクの実行:タスク実行メカニズムのコアインターフェイスExecutor、およびExecutorインターフェイスから継承されたExecutorServiceインターフェイスを含みます。ExecutorServiceインターフェイスを実装する2つの主要なクラスがあります:ThreadPoolExecutor、ScheduledThreadPoolExecutor
  • 非同期計算の結果:FutureインターフェイスとFutureインターフェイスを実装するFutrueTaskクラスを含む

画像-20201214110522001

2.3スレッドプールを使用する

Executorsツールクラスを使用して、次の3種類のThreadPoolExecutorを作成できます。

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool
2.3.1 FixedThreadPool

FixedThreadPoolは、固定数のスレッドを持つ再利用可能なスレッドプールと呼ばれ、スレッドプール内のスレッドの最大数を示すパラメーターが渡されます。

public class MyThreadPoolDemo1 {
    
    
    public static void main(String[] args) {
    
    
        //创建固定数量的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(4);
        for (int i = 1; i <= 10; i++) {
    
    
            //执行每个线程里的方法
            threadPool.execute(() -> {
    
    
                System.out.println(Thread.currentThread().getName() + "\t进来了");
                try {
    
    
                    //暂停1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            });
        }
        //关闭线程池
        threadPool.shutdown();
    }
}

画像-20201214110522001

2.3.2 SingleThreadExecutor

SingleThreadExecutorは、単一のワーカースレッドを使用するエグゼキューターであり、使用方法はFixedThreadPoolと同じです。

ExecutorService  threadPool = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 10; i++) {
    
    
    //执行每个线程里的方法
    threadPool.execute(() -> {
    
    
        System.out.println(Thread.currentThread().getName() + "\t进来了");
        try {
    
    
            //暂停1秒
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
}
//关闭线程池
threadPool.shutdown();
2.3.3 CachedThreadPool

CachedThreadPoolは、必要に応じて新しいスレッドを作成するスレッドプールであり、使用方法は上記の2つと同じです。

2.4スレッドプールの7つのパラメータ

Executorsツールクラスは3つの利用可能なスレッドプールを提供し、それぞれが1つのスレッドと指定された数のスレッドを持ち、必要に応じて拡張できますが、ソースコードを見てみましょう。

//固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
//单个work工作的线程
public static ExecutorService newSingleThreadExecutor() {
    
    
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
//可根据需要扩展的线程池
public static ExecutorService newCachedThreadPool() {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

上記のソースコードを見ると、共通点が見つかります。ThreadPoolExecutorメソッドが呼び出されます。つまり、Executorsによって提供される3つのスレッドプールは、一部のパラメーターをThreadPoolExecutorに渡すのに役立ちます。以下はThreadPoolExecutorのソースコードです。

画像-20201214125022682

ThreadPoolExecutorには合計7つのパラメーターがあり、以下に1つずつ紹介されています。

2.4.1 corePoolSize

corePoolSizeは、スレッドプールの基本サイズ、つまりスレッドプール内の常駐コアスレッドの数です。

スレッドプールにタスクを送信すると、スレッドプールはタスクを実行するためのスレッドを作成します。他のアイドル状態の基本スレッドが新しいタスクを実行できる場合でも、スレッドも作成します。実行する必要のあるタスクの数が多い場合スレッドプールの基本サイズよりも大きくなると、作成されません。スレッドプールのprestartAllCoreThreads()メソッドが呼び出されると、スレッドプールはすべての基本スレッドを事前に作成して開始します。

2.4.2 maximumPoolSize

maximumPoolSizeは、スレッドプールの最大数であり、スレッドプールが作成できるスレッドの最大数です。キューがいっぱいで、作成されたスレッドの数が最大スレッド数より少ない場合、スレッドプールは、タスクを実行するための新しいスレッドを作成します。無制限のタスクキューのパラメータを使用しても効果がないことは注目に値します。この値は1以上である必要があります

2.4.3keepAliveTime和ユニット

keepAliveTimeは、スレッドアクティビティの保持時間、冗長アイドルスレッドの存続時間です。現在のプール内のスレッド数がcorePoolSizeを超えると、アイドル時間がkeepAliveTimeに達すると、corePoolSizeスレッドのみが残るまで冗長スレッドが破棄されます。

unitはkeepAliveTimeの単位です

2.4.4 workQueue

workQueue:タスクキュー、送信されたがまだ実行されていないタスク。次から選択できます。

  • ArrayBlockingQueue:配列構造に基づく制限付きブロッキングキューです。このキューは、FIFO(先入れ先出し)の原則に従って
    要素を並べ替えます
  • LinkedBlockingQueue:リンクリスト構造に基づくブロッキングキュー。このキューはFIFOに従って要素
    を並べ替え、通常、スループットはArrayBlockingQueueのスループットよりも高くなります。静的ファクトリメソッドExecutors.newFixedThreadPool()はこのキューを使用します。
  • SynchronousQueue:要素を格納しないブロッキングキュー。各挿入操作は、別のスレッドが
    削除操作を呼び出すまで待機する必要があります。そうしないと、挿入操作はブロック状態のままになり、スループットは通常、Linked-BlockingQueueよりも高くなり
    ます。静的ファクトリメソッドExecutors.newCachedThreadPoolはこのキューを使用します。
  • PriorityBlockingQueue:優先度のある無限のブロッキングキュー。

これらのキューは、実際には以前のブロッキングキューです

2.4.5 threadFactory

スレッドの作成に使用される、スレッドプール内のワーカースレッドを生成するスレッドファクトリを表します。通常、デフォルトで十分です。

2.4.6ハンドラー

ハンドラー飽和戦略は、キューとスレッドプールがいっぱいで、スレッドプールが飽和状態にあることを示している場合、送信された新しいタスクを処理するための戦略を採用する必要があります。このポリシーはデフォルトでAbortPolicyです。これは、新しいタスクを処理できない場合に例外がスローされることを意味します。JDK 1.5では、Javaスレッドプールフレームワークは次の4つの戦略を提供します。

  • AbortPolicy:例外を直接スローします。
  • CallerRunsPolicy:タスクを実行するために呼び出し元のスレッドのみを使用します。
  • DiscardOldestPolicy:キュー内の最新のタスクを破棄し、現在のタスクを実行します。
  • DiscardPolicy:処理せず、破棄します。

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

画像-20201214131123192

ここに画像の説明を挿入

飽和ポリシーはデフォルトでAbortPolicyを使用していることがわかります

2.5スレッドプールの基本的な動作原理

上記でスレッドプールを使用すると、executeメソッドが呼び出されます。このメソッドは、スレッドプールの動作原理を理解するために非常に重要です。

public void execute(Runnable command) {
    
    
    	//如果任务为null,则直接抛出异常
        if (command == null)
            throw new NullPointerException();
    	//The main pool control state, ctl, is an atomic integer packing
    	//ct1保存线程的运行状态
        int c = ctl.get();
    	//首先判断当前线程池执行的任务数是否小于corePoolSize 如果小于新建一个线程,并将任务添加到该线程中 然后启动线程
        if (workerCountOf(c) < corePoolSize) {
    
    
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
    	//如果当前线程数大于corePoolSize,那么就来到下面的逻辑
    	//通过isRunning判断线程的状态只有运行的线程,并且队列不满时才能加入到工作队列中
        if (isRunning(c) && workQueue.offer(command)) {
    
    
            int recheck = ctl.get();
            //再次获取线程的状态,如果不是Running就从队列里移除
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
    	//如果addWorker失败 则直接启动拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }
  1. スレッドプールを作成したら、リクエストの待機を開始します。

  2. execute()メソッドを呼び出して要求タスクを追加すると、スレッドプールは次の判断を下します。

    1. 実行中のスレッドの数がcorePoolSize未満の場合は、すぐにこのタスクを実行するスレッドを作成します。
    2. 実行中のスレッドの数がcorePoolSize以上の場合は、タスクをキューに入れます。
    3. この時点でキューがいっぱいで、実行中のスレッドの数がまだmaximumPoolSize未満の場合でも、タスクをすぐに実行するには、非コアスレッドを作成する必要があります。
    4. キューがいっぱいで、実行中のスレッドの数がmaximumPoolSize以上の場合、スレッドプールは飽和拒否戦略を開始して実行します。
  3. スレッドがタスクを完了すると、実行のためにキューから次のタスクが削除されます。

  4. スレッドが特定の時間(keepAliveTime)を超えて何の関係もない場合、スレッドは次のように判断します。

    1. 現在実行中のスレッドの数がcorePoolSizeより大きい場合、このスレッドは停止します。

    2. すべてのスレッドプールのすべてのタスクが完了すると、最終的にはcorePoolSizeのサイズに縮小されます。

下の図からもう一度理解を深めましょう

画像-20201214131914509

  1. スレッドプールは、コアスレッドプール内のスレッドがすべてタスクを実行しているかどうかを判別します。そうでない場合は、タスクを実行するための新しいワーカースレッドを作成します。コアスレッドプール内のスレッドがすべてタスクを実行している場合は、次のプロセスに入ります。
  2. スレッドプールは、ワークキューがいっぱいかどうかを判断します。ワークキューがいっぱいでない場合、新しく送信されたタスクはこのワークキューに保存されます。ワークキューがいっぱいの場合は、次のプロセスに入ります。
  3. スレッドプールは、スレッドプールのスレッドがすべて機能しているかどうかを判断します。そうでない場合は、タスクを実行するための新しいワーカースレッドを作成します。いっぱいになっている場合は、飽和戦略に渡してこのタスクを処理します。

つまり、スレッドプールのワークフローは3箇所をチェックする必要があります。スレッド数が増えている場合は、corePoolSize、workQueueブロッキングキューがいっぱいかどうか、maxmumPoolSizeのスレッドの最大数に達しているかどうかを確認してください。スレッドの数を増やす場合は、スレッドプールの7つのパラメーターに関連する飽和戦略を開始します

2.6スレッドプールの合理的な構成

Executorsはスレッドを作成する3つの方法を提供し、CachedThreadPoolは必要に応じてスレッドを作成することもできますが、実際の作業では、スレッドプールのパラメーターを自分で定義する必要があります。特定の理由により、のソースコードを確認できます。 CachedThreadPool。

public static ExecutorService newCachedThreadPool () {
    
    
    	//可以看到最大容量为Integer.MAX_VALUE
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

他の2つはどうですか?FixedThreadPoolとSingleThreadExecutorはunboundedLinkedBlockingQueueを使用します(ただし、これはboundedと呼ばれますが、最大容量はInteger.MAX_VALUEであるため、ここではunboundedとも呼ばれます。「JavaConcurrent Art Programming」ではこれは無制限であると言われているため、説明を強制します) 、CachedThreadPoolはSynchronousQueueを使用します。

したがって、FixedThreadPool、SingleThreadExecutor、およびCachedThreadPoolはメモリをバーストし、oomを引き起こす可能性があるため、通常、カスタムパラメータを使用したスレッドプールが使用されます。

スレッドプールを合理的に構成する場合は、最初にそのタスクの特性を分析する必要があります。これは、タスクの性質、タスクの優先度、タスクの実行時間、およびタスクの依存関係の観点から分析できます。タスク

おすすめ

転載: blog.csdn.net/weixin_44706647/article/details/114916765