Why might you need an ExecutorService for 1 thread?

You can use the Executors.newSingleThreadExecutor method to create an ExecutorService with a pool that includes a single thread. The pool's logic is as follows:

  • The service executes only one task at a time.
  • If we submit N tasks for execution, all N tasks will be executed one after another by the single thread.
  • If the thread is interrupted, a new thread will be created to execute any remaining tasks.

Let's imagine a situation where our program requires the following functionality:

We need to process user requests within 30 seconds, but no more than one request per unit of time.

We create a task class for processing a user request:


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());
   }
}
    

The class models the behavior of processing an incoming request and displays its number.

Next, in the main method, we create an ExecutorService for 1 thread, which we will use to sequentially process incoming requests. Since the task conditions stipulate "within 30 seconds", we add 30-second wait and then forcibly stop the 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();
}
    

After starting the program, the console displays messages about request processing:

Processed request #0 on thread id=16
Processed request #1 on thread id=16
Processed request #2 on thread id=16

Processed request #29 on thread id=16

After processing requests for 30 seconds, executorService calls the shutdownNow() method, which stops the current task (the one being executed) and cancels all pending tasks. After that, the program ends successfully.

But everything is not always so perfect, because our program could easily have a situation where one of the tasks picked up by our pool's only thread works incorrectly and even terminates our thread. We can simulate this situation to figure out how executorService works with a single thread in this case.

To do this, while one of the tasks is being executed, we terminate our thread using the unsafe and obsolete Thread.currentThread().stop() method. We're doing this intentionally to simulate the situation where one of the tasks terminates the thread.

We'll change the run method in the Task class:


@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());
}
    

We'll interrupt task #5.

Let's see what the output looks like with the thread interrupted at the end of task #5:

Processed request #0 on thread id=16
Processed request #1 on thread id=16
Processed request #2 on thread id=16
Processed request #3 on thread id=16
Processed request #4 on thread id=16
Processed request #6 on thread id=17
Processed request #7 on thread id=17

Processed request #29 on thread id=17

We see that after the thread is interrupted at the end of task 5, the tasks begin to be executed in a thread whose identifier is 17, although they had previously been executed on the thread with whose identifier is 16. And because our pool has a single thread, this can only mean one thing: executorService replaced the stopped thread with a new one and continued to execute the tasks.

Thus, we should use newSingleThreadExecutor with a single-thread pool when we want to process tasks sequentially and only one at a time, and we want to continue processing tasks from the queue regardless of the completion of the previous task (e.g. the case where one of our tasks kills the thread).

ThreadFactory

When speaking of creating and recreating threads, we can't help but mention ThreadFactory.

A ThreadFactory is an object that creates new threads on demand.

We can create our own thread creation factory and pass an instance of it to the Executors.newSingleThreadExecutor(ThreadFactory threadFactory) method.


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
We override the method for creating a new thread, passing a thread name to the constructor.

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;
            }
        });
                    
We changed the name and priority of the created thread.

So we see that we have 2 overloaded Executors.newSingleThreadExecutor methods. One without parameters, and a second with a ThreadFactory parameter.

Using a ThreadFactory, you can configure the created threads as needed, for example, by setting priorities, using thread subclasses, adding an UncaughtExceptionHandler to the thread, and so on.