Ще один тип пулу потоку – кешований. Ці пули потоків так само поширені у використанні, як і фіксовані.

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

Розуміння “певної раціональної кількості” доволі розмите, але слід розуміти, що не для кожної кількості завдань підійде такий пул. Наприклад, якщо ми захочемо створити мільйон завдань, кожне з яких займає навіть зовсім невелику кількість часу, ми все одно необґрунтовано будемо використовувати ресурси та погіршимо продуктивність. Також ми повинні уникати такого пулу, коли час виконання непередбачуваний, наприклад, при завданнях введення-виведення.

Під капотом викликається конструктор ThreadPoolExecutor з наступними параметрами:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>());
}

Як параметри до конструктора передаються такі значення:

Параметр Значення
corePoolSize (Яка кількість потоків буде готова (запущена) при старті executor сервісу) 0
maximumPoolSize (Максимальна кількість потоків, що може створити executor сервіс) Integer.MAX_VALUE
keepAliveTime (Час, протягом якого потік, що звільнився, житиме і згодом буде знищений, якщо кількість потоків більша за corePoolSize) 60L
unit (Одиниці часу) TimeUnit.SECONDS
workQueue (Реалізація черги) new SynchronousQueue<Runnable>()

Також до параметрів ми можемо передати власну реалізацію ThreadFactory.

Поговоримо про SynchronousQueue

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

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

Кешований пул починається з нуля потоків і потенційно може зрости до кількості потоків Integer.MAX_VALUE. Практично єдиним обмеженням для кешованого пулу потоків є системні ресурси.

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

Давайте на практиці перевіримо, як це працює. Ми створюємо клас завдання, моделюючи запит користувача:

public class Task implements Runnable {
   int taskNumber;

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

   @Override
   public void run() {
       System.out.println("Опрацьовано запит користувача №" + taskNumber + " на потоці " + Thread.currentThread().getName());
   }
}

У main ми створюємо newCachedThreadPool, після чого додаємо 3 завдання на виконання. Тут ми виводимо статус нашого сервісу (1).

Далі ми робимо паузу в 30 секунд, після чого запускаємо на виконання ще одне завдання та виводимо статус (2).

Потім ми ставимо на паузу наш основний потік на 70 секунд, виводимо статус (3), знову додаємо 3 завдання на виконання та знову друкуємо статус (4).

Перед виведенням статусів у місцях, де він виводиться одразу після додавання завдання, ми поставимо sleep на 1 секунду для актуального виведення.

ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            executorService.submit(new Task(i));
        }

        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(1)

        TimeUnit.SECONDS.sleep(30);

        executorService.submit(new Task(3));
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(2)

        TimeUnit.SECONDS.sleep(70);

            System.out.println(executorService);	//(3)

        for (int i = 4; i < 7; i++) {
            executorService.submit(new Task(i));
        }

        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(4)
        executorService.shutdown();

Отже, результат виконання:

Опрацьовано запит користувача №0 на потоці pool-1-thread-1
Опрацьовано запит користувача №1 на потоці pool-1-thread-2
Опрацьовано запит користувача №2 на потоці pool-1-thread-3
(1) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Running, pool size = 3, active threads = 0, queued tasks = 0, completed tasks = 3]
Опрацьовано запит користувача №3 на потоці pool-1-thread-2
(2) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Running, pool size = 3, active threads = 0, queued tasks = 0, completed tasks = 4]
(3) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 4]
Опрацьовано запит користувача №4 на потоці pool-1-thread-4
Опрацьовано запит користувача №5 на потоці pool-1-thread-5
Опрацьовано запит користувача №6 на потоці pool-1-thread-4
(4) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Running, pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 7]

Розберемо кожен із кроків:

Крок Роз'яснення
1 (після трьох виконаних завдань) Створилися 3 потоки, на цих трьох потоках виконувались 3 завдання.
У момент виведення статусу всі 3 завдання виконані, а потоки готові до виконання інших завдань.
2 (після паузи в 30 секунд та виконання ще одного завдання) Після 30 секунд бездіяльності потоки все ще живі та очікують на завдання.
Додається ще одне завдання і виконується на потоці з пулу живих потоків, що залишилися.
Нового потоку в пулі не з'явилося.
3 (після паузи у 70 секунд) Потоки видалилися з пулу.
Потоків, готових прийняти завдання, немає.
4 (після виконання ще 3х завдань) Після того, як надійшли завдання на виконання, створилися нові потоки, причому цього разу лише два потоки змогли обробити 3 завдання.

Таким чином, ми познайомилися з роботою ще одного виду сервісу зі своєю логікою керування потоками.

За аналогією з іншими утильними методами класу Executors, у методу newCachedThreadPool теж є його перевантажений варіант, що приймає як параметр об'єкт типу ThreadFactory.