Защо може да се нуждаете от ExecutorService за 1 нишка?

Можете да използвате метода Executors.newSingleThreadExecutor , за да създадете ExecutorService с пул, който включва една нишка. Логиката на пула е следната:

  • Услугата изпълнява само една задача наведнъж.
  • Ако изпратим N задачи за изпълнение, всичките N задачи ще бъдат изпълнени една след друга от единичния поток.
  • Ако нишката бъде прекъсната, ще бъде създадена нова нишка за изпълнение на всички останали задачи.

Нека си представим ситуация, в която нашата програма изисква следната функционалност:

Трябва да обработваме потребителски заявки в рамките на 30 секунди, но не повече от една заявка за единица време.

Създаваме клас задачи за обработка на потребителска заявка:


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

Класът моделира поведението при обработка на входяща заявка и показва нейния номер.

След това в основния метод създаваме ExecutorService за 1 нишка, която ще използваме за последователна обработка на входящи заявки. Тъй като условията на задачата предвиждат „в рамките на 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();
}
    

След стартиране на програмата конзолата показва съобщения за обработка на заявка:

Обработена заявка #0 в нишка id=16
Обработена заявка #1 в нишка id=16
Обработена заявка #2 в нишка id=16

Обработена заявка #29 в нишка id=16

След като обработи заявките за 30 секунди, executorService извиква метода shutdownNow() , който спира текущата задача (тази, която се изпълнява) и отменя всички чакащи задачи. След това програмата приключва успешно.

Но не винаги всичко е толкова перфектно, защото нашата програма може лесно да има ситуация, в която една от задачите, взети от единствената нишка на нашия пул, работи неправилно и дори да прекрати нашата нишка. Можем да симулираме тази ситуация, за да разберем How executorService работи с една нишка в този случай.

За да направим това, докато една от задачите се изпълнява, ние прекратяваме нашата нишка, използвайки опасния и остарял метод Thread.currentThread().stop() . Правим това умишлено, за да симулираме ситуацията, при която една от задачите прекратява нишката.

Ще променим метода за изпълнение в класа Task :


@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.

Нека да видим How изглежда резултатът с нишката, прекъсната в края на задача #5:

Обработена заявка #0 в нишка id=16
Обработена заявка #1 в нишка id=16
Обработена заявка #2 в нишка id=16
Обработена заявка #3 в нишка id=16
Обработена заявка #4 в нишка id=16
Обработена заявка #6 в нишка id=17
Обработена заявка #7 на нишка id=17

Обработена заявка #29 на нишка id=17

Виждаме, че след като нишката е прекъсната в края на задача 5, задачите започват да се изпълняват в нишка, чийто идентификатор е 17, въпреки че преди това са бor изпълнени в нишката, чийто идентификатор е 16. И тъй като нашият пул има единична нишка, това може да означава само едно нещо: executorService замени спряната нишка с нова и продължи да изпълнява задачите.

По този начин трябва да използваме newSingleThreadExecutor с пул с една нишка, когато искаме да обработваме задачи последователно и само една по една, и искаме да продължим да обработваме задачи от опашката, независимо от завършването на предишната задача (напр. случаят, когато една от нашите задачи убива нишката).

ThreadFactory

Когато говорим за създаване и пресъздаване на теми, не можем да не споменемThreadFactory.

АThreadFactoryе обект, който създава нови нишки при поискване.

Можем да създадем наша собствена фабрика за създаване на нишки и да предадем нейно копие на метода 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 . Един без параметри и втори с параметър ThreadFactory .

С помощта на ThreadFactory можете да конфигурирате създадените нишки според нуждите, например чрез задаване на приоритети, използване на подкласове на нишки, добавяне на UncaughtExceptionHandler към нишката и т.н.