スレッドプールを使用して複数のタスクを実行するJavaの例

この記事では、主にスレッドプールを使用して複数のタスクを実行するJavaの例を紹介し、誰もがJavaの使用法をよりよく理解して学習できるようにし、興味のある友人が

IO操作(ファイルのダウンロードなど)を使用して一連の無関係な非同期タスクを実行する場合、マルチスレッドを使用すると、操作効率を大幅に向上させることができます。スレッドプールには一連のスレッドが含まれており、これらのスレッドを管理できます。例:スレッドの作成、スレッドの破棄など。この記事では、Javaでスレッドプールを使用してタスクを実行する方法を紹介します。

1タスクタイプ

スレッドプールを使用してタスクを実行する前に、スレッドプールから呼び出すことができるタスクを把握します。タスクに戻り値があるかどうかに応じて、タスクは、Runnableを実装するタスククラス(パラメーターなし、戻り値なし)とCallableインターフェイスを実装するタスククラス(パラメーターなし、戻り値)の2つのタイプに分けることができます。 。コーディングするときは、必要に応じて対応するタスクタイプを選択してください。

1.1Runnableインターフェースを実装するクラス

マルチスレッドタスクタイプの場合、最初に自然に頭に浮かぶのは、Runnableインターフェイスを実装するクラスです。Runnableインターフェイスは、パラメーターや戻り値を持たない抽象メソッドrunを提供します。例えば:

Runnable task = new Runnable() {
    
    
  @Override
  public void run() {
    
    
    System.out.println("Execute task.");
  }
};

または、Java8以降でのより簡単な記述方法

Runnable task = ()->{
    
    
  System.out.println("Execute task.");
};

1.2 Callableインターフェースを実装するクラス
Runnableと同様に、Callableには抽象メソッドが1つしかありませんが、抽象メソッドには戻り値があります。このインターフェイスを実装するときは、戻り値のタイプを定式化する必要があります。例えば:

Callable<String> callableTask = ()-> "finished";

Runnableと同様に、Callableには抽象メソッドが1つしかありませんが、この抽象メソッドには戻り値があります。このインターフェイスを実装するときは、戻り値のタイプを定式化する必要があります。例えば:

Callable<String> callableTask = ()-> "finished";

2スレッドプールタイプ

java.util.concurrent.Executorsは、さまざまなスレッドプールを作成するための一連の静的メソッドを提供します。主なスレッドプールと特性の一部を以下に示します。リストされていない他のスレッドプールの特性は、以下から導き出すことができます。

2.1スレッド数が固定された固定スレッドプール

名前が示すように、このタイプのスレッドプール内のスレッドの数は固定されています。スレッド数がnに設定されている場合、スレッドプール内の最大n個のスレッドがいつでも実行されています。スレッドプールが飽和実行状態にある場合、スレッドプールに送信されたタスクは実行キューに配置されます。スレッドプールが飽和していない場合、ExecuteServiceのshutdownメソッドが呼び出されるまで、スレッドプールは常に存在し、スレッドプールはクリアされます。

// 创建线程数量为5的线程池。
ExecutorService executorService = Executors.newFixedThreadPool(5);

2.2キャッシュされたスレッドプール
このタイプのスレッドプールの初期サイズは0スレッドです。タスクは継続的にプールに送信されるため、スレッドプールにアイドルスレッドがない場合(0スレッドはアイドルスレッドがないことも意味します)、タスクが待機していないことを確認するために新しいスレッドが作成されます。アイドル状態のスレッドがある場合、アイドル状態のスレッドはタスクを実行するために再利用されます。アイドル状態のスレッドは60秒間だけスレッドプールにキャッシュされ、60秒間アイドル状態のスレッドはシャットダウンされてスレッドプールから削除されます。多数の短期間の(正式には:短期間の)非同期タスクを処理するときに、プログラムのパフォーマンスを大幅に向上させることができます。

//创建一个可缓存的线程池 
ExecutorService executorService = Executors.newCachedThreadPool();

2.3シングルスレッドプール
これはスレッドプールとは呼ばれない場合があります。これは、スレッドが常に1つだけであり、最初から最後まで1つしかないためです(Executors.newFixedThreadPool(1)とは異なると言われる理由) 、それでも「シングルスレッドプールハンドル」と呼んでください。可能な限りシングルスレッドプールにタスクを追加できますが、毎回実行されるのは1つだけであり、タスクは順番に実行されます。前のタスクで例外が発生した場合、現在のスレッドは破棄されますが、次のタスクを実行するために新しいスレッドが作成されます。上記は、スレッドが1つしかない固定スレッドプールと同じです。2つの違いは、Executors.newFixedThreadPool(1)が実行時にスレッド数を変更できるのに対し、Executors.newSingleThreadExecutor()は常に1つのスレッドしか持てないことです。

//创建一个单线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();

2.4ワークスティーリングスレッドプール
ソースコードを開くと、ワークスティーリングスレッドプールは本質的にForkJoinPoolであることがわかります。このタイプのスレッドプールは、CPUマルチコア処理タスクを最大限に活用し、より多くのCPUリソースを消費するタスクの処理に適しています。スレッドの数は固定されておらず、複数のタスクキューが維持されています。タスクキューが完了すると、対応するスレッドが他のタスクキューからタスクの実行を盗みます。つまり、タスクの実行順序は送信と同じです。注文。需要が高い場合は、ForkJoinPoolを介してスレッドプールを直接取得できます。

//创建一个工作窃取线程池,使用CPU核数等于机器的CPU核数
ExecutorService executorService = Executors.newWorkStealingPool();

//创建一个工作窃取线程池,使用CPU 3 个核进行计算,工作窃取线程池不能设置线程数
ExecutorService executorService2 = Executors.newWorkStealingPool(3);

2.5スケジュールされたタスクスレッドプール
スケジュールされたタスクスレッドプールは、計画に従って特定のタスクを実行できます。たとえば、特定のタスクを定期的に実行します。

// 获取一个大小为2的计划任务线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
// 添加一个打印当前线程信息计划任务,该任务在3秒后执行
scheduledExecutorService.schedule(() -> {
    
     System.out.println(Thread.currentThread()); }, 3, TimeUnit.SECONDS);
// 添加一个打印当前线程信息计划任务,该任务在2秒后首次执行,之后每5秒执行一次。如果任务执行时间超过了5秒,则下一次将会在前一次执行完成之后立即执行
scheduledExecutorService.scheduleAtFixedRate(() -> {
    
     System.out.println(Thread.currentThread()); }, 2, 5, TimeUnit.SECONDS);
// 添加一个打印当前线程信息计划任务,该任务在2秒后首次执行,之后每次在任务执行之后5秒执行下一次。
scheduledExecutorService.scheduleWithFixedDelay(() -> {
    
     System.out.println(Thread.currentThread()); }, 2, 5, TimeUnit.SECONDS);
// 逐个清除 idle 状态的线程
scheduledExecutorService.shutdown();
// 阻塞,在线程池被关调之前代码不再往下走
scheduledExecutorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);

3スレッドプールを使用してタスクを実行する

前述のように、タスクタイプは戻り値のあるタイプと戻り値のないタイプに分けられ、ここでの呼び出しも戻り値のある呼び出しと戻り値のない呼び出しに分けられます。

3.1戻り値のないタスクの呼び出し

戻り値のないタスクの呼び出しの場合は、executeメソッドまたはsubmitメソッドを使用できます。この場合、2つは基本的に同じです。戻り値を使用してタスクを呼び出す際の均一性を維持するために、submitメソッドを使用することをお勧めします。

//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);

//提交一个无返回值的任务(实现了Runnable接口)
executorService.submit(()->System.out.println("Hello"));

executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);

一連のタスクがある場合は、それらを1つずつ送信できます。

//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
List<Runnable> tasks = Arrays.asList(
    ()->System.out.println("Hello"),
    ()->System.out.println("World"));

//逐个提交任务
tasks.forEach(executorService::submit);

executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);

3.2戻り値のあるタスクの呼び出し戻り値のあるタスク
は、Callableインターフェースを実装する必要があります。実装するときは、一般的な位置に戻り値のタイプを指定します。submitメソッドが呼び出されると、Futureオブジェクトが返され、Futureメソッドget()を介して戻り値を取得できます。ここで、get()を呼び出すと、タスクが完了して戻り値が返されるまで、コードがブロックされることに注意してください。

ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<String> future = executorService.submit(()->"Hello");
System.out.println(future.isDone());//false
String value = future.get();
System.out.println(future.isDone());//true
System.out.println(value);//Hello

タスクのバッチを1つずつ送信するだけでなく、ExecutorServiceはinvokeAllを呼び出して1回限りの送信を行うこともできます。実際、invokeAllの内部実装では、タスクを1つずつループで送信します。invokeAllによって返される値はFutureListです。

ExecutorService executorService = Executors.newFixedThreadPool(2);
List<Callable<String>> tasks = Arrays.asList(()->"Hello", ()->"World");
List<Future<String>> futures = executorService.invokeAll(tasks);

invokeAnyメソッドも非常に便利です。スレッドプールは、Callableを実装するいくつかのタスクを実行し、実行が完了した最初のタスクの値を返します。その他の未完了のタスクは、例外なく通常どおりキャンセルされます。次のコードは「Hello」を出力しません

ExecutorService executorService = Executors.newFixedThreadPool(2);
List<Callable<String>> tasks = Arrays.asList(
    () -> {
    
    
      Thread.sleep(500L);
      System.out.println("Hello");
      return "Hello";
    }, () -> {
    
    
      System.out.println("World");
      return "World";
    });
String s = executorService.invokeAny(tasks);
System.out.println(s);//World

出力:

World
World

さらに、ExecutorServiceのソースコードを見ると、Future submit(Runnable task、T result);メソッドも提供されていることがわかりました。このメソッドを使用して、Runnableインターフェイスを実装するタスクを送信し、値を返すことができます。戻り値がない場合は、Runnableインターフェイスのrunメソッド。その戻り値はどこから来るのですか?実際、問題はsubmitメソッドの後のパラメーターにあり、パラメーター値は戻り値です。submitメソッドを呼び出した後、操作が行われ、結果パラメーターが直接返されます。

ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<String> future = executorService.submit(() -> System.out.println("Hello"), "World");
System.out.println(future.get());//输出:World

マルチスレッドを使用してタスクを処理する場合は、状況に応じて適切なタスクタイプとスレッドプールタイプを選択する必要があります。戻り値がない場合は、RunnableまたはCallableインターフェイスを実装するタスクを使用できます。戻り値がある場合は、Callableインターフェイスを実装するタスクを使用する必要があり、戻り値はFutureのgetメソッドを介して取得されます。 。スレッドプールを選択するときに、使用するスレッドが1つだけの場合は、単一のスレッドプールまたは容量が1の固定容量のスレッドプールを使用します。多数の短期間のタスクを処理するには、キャッシュ可能なスレッドプールを使用します。いくつかのループを計画または実行したいタスクはスケジュールされたタスクスレッドプールを使用できます。タスクが大量のCPUリソースを消費する必要がある場合、アプリケーションの作業はスレッドプールを盗みます。

以上が本稿の内容ですので、皆様のご勉強に役立てていただければ幸いです。

おすすめ

転載: blog.csdn.net/p1830095583/article/details/114701223