1 つのスレッドに ExecutorService が必要になるのはなぜですか?

Executors.newSingleThreadExecutorメソッドを使用して、単一スレッドを含むプールを持つExecutorServiceを作成できます。プールのロジックは次のとおりです。

  • サービスは一度に 1 つのタスクのみを実行します。
  • N 個のタスクを実行のために送信すると、N 個のタスクすべてが単一のスレッドによって次々に実行されます。
  • スレッドが中断された場合は、残りのタスクを実行するために新しいスレッドが作成されます。

プログラムが次の機能を必要とする状況を想像してみましょう。

ユーザーリクエストは 30 秒以内に処理する必要がありますが、単位時間あたり 1 リクエストを超えてはなりません。

ユーザーリクエストを処理するためのタスククラスを作成します。


class Task implements Runnable {
   private final int taskNumber;

   public Task(int taskNumber) {
       this.taskNumber = taskNumber;
   }

   @Override
   public void run() {
       try {
           Thread.sleep(1000);
       } catch (InterruptedException ignored) {
       }
       System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
   }
}
    

このクラスは、受信リクエストを処理する動作をモデル化し、その番号を表示します。

次に、mainメソッドで、受信リクエストを順次処理するために使用する 1 つのスレッドのExecutorServiceを作成します。タスク条件に「30秒以内」と規定されているので、30秒の待ち時間を追加して ExecutorService を強制停止します


public static void main(String[] args) throws InterruptedException {
   ExecutorService executorService = Executors.newSingleThreadExecutor();

   for (int i = 0; i < 1_000; i++) {
       executorService.execute(new Task(i));
   }
   executorService.awaitTermination(30, TimeUnit.SECONDS);
   executorService.shutdownNow();
}
    

プログラムを開始すると、コンソールにリクエストの処理に関するメッセージが表示されます。

スレッド ID=16 でリクエスト #0 を処理しました
スレッド ID=16 でリクエスト #1 を
処理しました スレッド ID=16 でリクエスト #2 を処理しました

スレッド ID=16 でリクエスト #29 を処理しました

リクエストを 30 秒間処理した後、executorService はshutdownNow()メソッドを呼び出します。これにより、現在のタスク (実行中のタスク) が停止され、保留中のタスクがすべてキャンセルされます。その後、プログラムは正常に終了します。

しかし、すべてが常に完璧であるとは限りません。プログラムでは、プールの唯一のスレッドによって選択されたタスクの 1 つが誤って動作し、さらにはスレッドを終了する状況が簡単に発生する可能性があります。この状況をシミュレートして、この場合executorService が単一のスレッドでどのように動作するかを理解できます。

これを行うには、タスクの 1 つの実行中に、安全ではなく廃止されたThread.currentThread().stop()メソッドを使用してスレッドを終了します。これは、タスクの 1 つがスレッドを終了する状況をシミュレートするために意図的に行っています。

Taskクラスのrunメソッドを変更します。


@Override
public void run() {
   try {
       Thread.sleep(1000);
   } catch (InterruptedException ignored) {
   }

   if (taskNumber == 5) {
       Thread.currentThread().stop();
   }

   System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
}
    

タスク #5 を中断します。

タスク #5 の終了時にスレッドが中断された場合の出力がどのようになるかを見てみましょう。

スレッド ID=16 でリクエスト #0 を処理しました スレッド ID=16
でリクエスト
#1 を処理しました スレッド ID=16 でリクエスト #2 を処理しました
スレッド ID=16 でリクエスト #3 を
処理しました スレッド ID=16 でリクエスト #4 を
処理しましたスレッド ID=17
スレッド ID=17 でリクエスト #7 を処理しました

スレッド ID=17 でリクエスト #29 を処理しました

タスク 5 の終わりでスレッドが中断された後、タスクは以前は識別子 16 のスレッドで実行されていたにもかかわらず、識別子 17 のスレッドで実行され始めていることがわかります。シングル スレッドの場合、これが意味することは 1 つだけです。executorService は停止したスレッドを新しいスレッドに置き換え、タスクの実行を継続しました。

したがって、タスクを一度に 1 つだけ順番に処理し、前のタスクの完了に関係なくキューからタスクの処理を継続したい場合 (たとえば、1 つのタスクが完了した場合など)、シングル スレッド プールで newSingleThreadExecutor を使用する必要がありますタスクの一部がスレッドを強制終了します)。

スレッドファクトリー

スレッドの作成と再作成について話すとき、言及せずにはいられません。スレッドファクトリー

スレッドファクトリーオンデマンドで新しいスレッドを作成するオブジェクトです。

独自のスレッド作成ファクトリーを作成し、そのインスタンスをExecutors.newSingleThreadExecutor(ThreadFactory threadFactory)メソッドに渡すことができます。


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
新しいスレッドを作成するメソッドをオーバーライドし、スレッド名をコンストラクターに渡します。

ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "MyThread");
                thread.setPriority(Thread.MAX_PRIORITY);
                return thread;
            }
        });
                    
作成したスレッドの名前と優先度を変更しました。

したがって、2 つのオーバーロードされたExecutors.newSingleThreadExecutorメソッドがあることがわかります。1 つはパラメーターなしで、もう 1 つはThreadFactoryパラメーターを使用します。

ThreadFactoryを使用すると、優先順位の設定、スレッドのサブクラスの使用、スレッドへのUncaughtExceptionHandler の追加などにより、必要に応じて作成されたスレッドを構成できます。