Методът newFixedThreadPool на класа Executors създава executorService с фиксиран брой нишки. За разлика от метода newSingleThreadExecutor , ние определяме колко нишки искаме в пула. Под капака се извиква следният code:


new ThreadPoolExecutor(nThreads, nThreads,
                                      	0L, TimeUnit.MILLISECONDS,
                                      	new LinkedBlockingQueue());

Параметрите corePoolSize (броят нишки, които ще бъдат готови (стартирани) при стартиране на услугата изпълнител ) и maximumPoolSize (максималния брой нишки, които услугата изпълнител може да създаде) получават една и съща стойност — броят нишки, предадени на newFixedThreadPool(nThreads ) . И можем да предадем нашата собствена реализация на ThreadFactory по абсолютно същия начин.

Е, нека да видим защо имаме нужда от такава ExecutorService .

Ето логиката на ExecutorService с фиксиран брой (n) нишки:

  • Максимум n нишки ще бъдат активни за обработка на задачи.
  • Ако са изпратени повече от n задачи, те ще бъдат задържани в опашката, докато нишките се освободят.
  • Ако една от нишките се провали и прекрати, ще бъде създадена нова нишка, която да заеме нейно място.
  • Всяка нишка в пула е активна, докато басейнът не бъде изключен.

Като пример, представете си да чакате да преминете през охраната на летището. Всички стоят на една опашка, докато непосредствено преди проверката за сигурност пътниците се разпределят между всички работещи контролно-пропускателни пунктове. Ако има забавяне на един от пунктовете, опашката ще се обработва само от втория, докато първият се освободи. И ако един контролно-пропускателен пункт се затвори изцяло, тогава ще бъде открит друг контролно-пропускателен пункт, който да го замени, а пътниците ще продължат да се обработват през два контролно-пропускателни пункта.

Веднага ще отбележим, че дори ако условията са идеални — обещаните n нишки работят стабилно и нишките, които завършват с грешка, винаги се заменят (нещо, което ограничените ресурси правят невъзможно да се постигне на реално летище) — системата все още има няколко неприятни функции, защото при ниHowви обстоятелства няма да има повече нишки, дори ако опашката расте по-бързо, отколкото нишките могат да обработват задачи.

Предлагам да получите практическо разбиране за това How ExecutorService работи с фиксиран брой нишки. Нека създадем клас, който имплементира Runnable . Обектите от този клас представляват нашите задачи за ExecutorService .


public class Task implements Runnable {
    int taskNumber;
 
    public Task(int taskNumber) {
        this.taskNumber = taskNumber;
    }
 
    @Override
    public void run() {
try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
    }
}
    

В метода run() ние блокираме нишката за 2 секунди, симулирайки известно натоварване, и след това показваме номера на текущата задача и името на нишката, изпълняваща задачата.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 30; i++) {
            executorService.execute(new Task(i));
        }
        
        executorService.shutdown();
    

Като начало в основния метод създаваме ExecutorService и изпращаме 30 задачи за изпълнение.

Обработена потребителска заявка #1 в нишка pool-1-thread-2
Обработена потребителска заявка #0 в нишка pool-1-thread-1
Обработена потребителска заявка #2 в нишка pool-1-thread-3
Обработена потребителска заявка #5 в pool- Нишка 1-нишка-3
Обработена потребителска заявка #3 в нишка pool-1-thread-2
Обработена потребителска заявка #4 в нишка pool-1-thread-1
Обработена потребителска заявка #8 в нишка pool-1-thread-1
Обработен потребител заявка #6 в нишка pool-1-thread-3
Обработена потребителска заявка #7 в нишка pool-1-thread-2
Обработена потребителска заявка #10 в нишка pool-1-thread-3
Обработена потребителска заявка #9 в pool-1- нишка-1
Обработена потребителска заявка #11 в нишка pool-1-thread-2
Обработена потребителска заявка #12 в нишка pool-1-thread-3
Обработена потребителска заявка #14 в нишка pool-1-thread-2
Обработена потребителска заявка #13 в нишка pool-1-thread-1
Обработена потребителска заявка #15 в нишка pool-1-thread-3
Обработена потребителска заявка #16 в pool- Нишка 1-нишка-2
Обработена потребителска заявка #17 в нишка pool-1-thread-1
Обработена потребителска заявка #18 в нишка pool-1-thread-3
Обработена потребителска заявка #19 в нишка pool-1-thread-2
Обработен потребител заявка #20 в нишка pool-1-thread-1
Обработена потребителска заявка #21 в нишка pool-1-thread-3
Обработена потребителска заявка #22 в нишка pool-1-thread-2
Обработена потребителска заявка #23 в pool-1- нишка-1 нишка
Обработена потребителска заявка #25 в нишка pool-1-thread-2
Обработена потребителска заявка #24 в нишка pool-1-thread-3
Обработена потребителска заявка #26 в нишка от pool-1-thread-1
Обработена потребителска заявка #27 в нишка от pool-1-thread-2
Обработена потребителска заявка #28 в нишка от pool-1-thread-3
Обработена потребителска заявка #29 в pool- 1-нишка-1 нишка

Изходът на конзолата ни показва How задачите се изпълняват на различни нишки, след като бъдат освободени от предишната задача.

Сега ще увеличим броя на задачите до 100 и след като изпратим 100 задачи, ще извикаме метода awaitTermination(11, SECONDS) . Предаваме число и времева единица като аргументи. Този метод ще блокира основната нишка за 11 секунди. След това ще извикаме shutdownNow() , за да принудим ExecutorService да се изключи, без да чака всички задачи да завършат.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task(i));
        }
 
        executorService.awaitTermination(11, SECONDS);
 
        executorService.shutdownNow();
        System.out.println(executorService);
    

Накрая ще покажем информация за състоянието на executorService .

Ето конзолния изход, който получаваме:

Обработена потребителска заявка #0 в нишка pool-1-thread-1
Обработена потребителска заявка #2 в нишка pool-1-thread-3
Обработена потребителска заявка #1 в нишка pool-1-thread-2
Обработена потребителска заявка #4 в pool- Нишка 1-thread-3
Обработена потребителска заявка #5 в нишка pool-1-thread-2
Обработена потребителска заявка #3 в нишка pool-1-thread-1
Обработена потребителска заявка #6 в нишка pool-1-thread-3
Обработен потребител заявка #7 в нишка pool-1-thread-2
Обработена потребителска заявка #8 в нишка pool-1-thread-1
Обработена потребителска заявка #9 в нишка pool-1-thread-3
Обработена потребителска заявка #11 в pool-1- нишка-1
Обработена потребителска заявка #10 в нишка pool-1-thread-2
Обработена потребителска заявка #13 в нишка pool-1-thread-1
Обработена потребителска заявка #14 за нишка pool-1-thread-2
Обработена потребителска заявка #12 за нишка pool-1-thread-3
java.util.concurrent.ThreadPoolExecutor@452b3a41[Изключване, размер на пула = 3, активни нишки = 3 , задачи на опашка = 0, завършени задачи = 15]
Обработена потребителска заявка #17 в нишка от пул-1-нишка-3
Обработена заявка от потребител #15 в нишка от пул-1-нишка-1
Обработена потребителска заявка #16 в нишка от пул-1 -2 резба

Това е последвано от 3 InterruptedExceptions , хвърлени от заспиващите методи от 3 активни задачи.

Виждаме, че когато програмата приключи, 15 задачи са изпълнени, но в пула все още има 3 активни нишки, които не са завършor изпълнението на своите задачи. Методът interrupt() се извиква на тези три нишки, което означава, че задачата ще завърши, но в нашия случай методът на заспиване хвърля InterruptedException . Виждаме също, че след извикването на метода shutdownNow() , опашката със задачи се изчиства.

Така че, когато използвате ExecutorService с фиксиран брой нишки в пула, не забравяйте да запомните How работи. Този тип е подходящ за задачи с известно постоянно натоварване.

Ето още един интересен въпрос: ако трябва да използвате изпълнител за една нишка, кой метод трябва да извикате? newSingleThreadExecutor() or newFixedThreadPool(1) ?

И двамата изпълнители ще имат еквивалентно поведение. Единствената разлика е, че методът newSingleThreadExecutor() ще върне изпълнител, който не може да бъде преконфигуриран по-късно, за да използва допълнителни нишки.