Метод newFixedThreadPool у класу Executors створить нам executorService з фіксованою кількістю потоків. Порівняно з методом newSingleThreadExecutor, ми вказуємо, скільки потоків ми хочемо бачити в пулі. Під капотом викликається


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

Як параметри corePoolSize (скільки потоків буде готово (запущено) при старті executor сервісу) і maximumPoolSize (максимальна кількість потоків, що може створити executor сервіс) передається те саме число — кількість потоків, що передається до newFixedThreadPool(nThreads). Так само ми можемо передати в параметри і власну реалізацію ThreadFactory.

Отже, давай розберемося, навіщо нам потрібен такий ExecutorService.

ExecutorService з фіксованою кількістю (n) потоків має наступну логіку:

  • Максимум n потоків будуть активними для обробки завдань.
  • Якщо передано більше n завдань, вони будуть утримуватись у черзі до моменту, поки потоки не звільняться.
  • Якщо в роботі одного з потоків відбудеться збій і він завершить свою роботу, буде створено новий потік замість зламаного.
  • Будь-який потік з пулу активний доти, доки пул не закритий.

Як приклад уяви чергу в аеропорту, де всі стоять в одній черзі, а безпосередньо перед контролем розходяться на працюючу кількість пунктів. Якщо в одному з пунктів буде затримка, черга йтиме лише через другий, доки перший не звільниться, а якщо один із пунктів зовсім зламається, то відкриють інший пункт на заміну цьому і контроль продовжиться через два пункти.

Одразу варто відзначити, що умови хоч і ідеальні, де обіцяні n потоків стабільно працюють і завжди буде підміна для потоків, що завершилися з помилкою (чого досягти в реальній роботі аеропорту неможливо через обмежені ресурси), все одно система має кілька неприємних особливостей, оскільки потоків більше не стане за жодних умов, навіть у разі, коли черга збільшуватиметься швидше, ніж потоки оброблятимуть завдання.

Пропоную розібратися на практиці, як працює 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("Опрацьовано запит користувача №" + taskNumber + " на потоці " + Thread.currentThread().getName());
    }
}
    

У методі run() ми блокуємо потік на 2 секунди, імітуючи навантаження, і виводимо номер поточного завдання та ім'я потоку, на якому виконуємо це завдання.


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

Для початку в main створимоExecutorService та надішлемо 30 завдань на виконання.

Опрацьовано запит користувача №1 на потоці pool-1-thread-2
Опрацьовано запит користувача №0 на потоці pool-1-thread-1
Опрацьовано запит користувача №2 на потоці pool-1-thread-3
Опрацьовано запит користувача №5 на потоці pool-1-thread-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-thread-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-thread-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-thread-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-thread-1

У консолі ми бачимо, як завдання виконуються на різних потоках у міру їхнього звільнення від попереднього завдання.

Тепер ми збільшимо кількість завдань до 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-thread-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[Shutting down, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 15]
Опрацьовано запит користувача №17 на потоці pool-1-thread-3
Опрацьовано запит користувача №15 на потоці pool-1-thread-1
Опрацьовано запит користувача №16 на потоці pool-1-thread-2

Далі йдуть 3 InterruptedException, які викидають методи sleep з 3 активних завдань.

Ми бачимо, що на момент завершення у нас виконалося 15 завдань, але ще залишається 3 активні потоки в пулі, які не закінчили виконання свого завдання. У цих трьох потоків викликається interrupt(), а це означає, що виконання завдання завершиться, але в нашому випадку метод sleep викидає нам InterruptedException. Також ми бачимо, що після виклику методу shutdownNow() очистилася черга із завдань.

Таким чином, варто зазначити, що принцип роботи слід враховувати при використанні ExecutorService із фіксованою кількістю потоків у пулі. Використовувати цей вид варто для завдань із заздалегідь відомим постійним навантаженням.

Є ще одне цікаве питання: якщо потрібно використовувати екзекутор на один потік, який метод викликати — newSingleThreadExecutor() чиnewFixedThreadPool(1)?

У поведінці обидва екзекутори будуть еквівалентними. Різниця полягає лише в тому, що метод newSingleThreadExecutor() поверне такий екзекутор, який надалі не можна переналаштувати на використання додаткових потоків.