Друг тип пул от нишки е "кеширан". Такива пулове от нишки се използват също толкова често, колкото и фиксираните.
Както е посочено от името, този вид набор от нишки кешира нишки. Той поддържа неизползваните нишки активни за ограничено време, за да се използват повторно тези нишки за изпълнение на нови задачи. Такъв пул от нишки е най-добър, когато имаме разумно количество лека работа.
Значението на „няHowва разумна сума“ е доста широко, но трябва да знаете, че такъв пул не е подходящ за всеки брой задачи. Да предположим например, че искаме да създадем мorон задачи. Дори и всяко да отнеме много малко време, пак ще използваме неразумно много ресурси и ще влошим производителността. Трябва също така да избягваме такива пулове, когато времето за изпълнение е непредвидимо, например при I/O задачи.
Под капака, конструкторът ThreadPoolExecutor се извиква със следните аргументи:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Следните стойности се предават на конструктора като аргументи:
Параметър | Стойност |
---|---|
corePoolSize (колко нишки ще бъдат готови (стартирани), когато стартира услугата изпълнител ) | 0 |
maximumPoolSize (максималният брой нишки, които една изпълнителна услуга може да създаде) | Цяло число.MAX_VALUE |
keepAliveTime (времето, през което освободена нишка ще продължи да живее, преди да бъде унищожена, ако броят на нишките е по-голям от corePoolSize ) | 60L |
единица (единици за време) | TimeUnit.SECONDS |
workQueue (имплементиране на опашка) | нов SynchronousQueue<Runnable>() |
И можем да предадем нашата собствена реализация на ThreadFactory по абсолютно същия начин.
Нека поговорим за SynchronousQueue
Основната идея за синхронно прехвърляне е доста проста и същевременно контраинтуитивна (т.е. интуицията or здравият разум ви казват, че е погрешно): можете да добавите елемент към опашката, ако и само ако друга нишка получи елемента на същото време. С други думи, синхронна опашка не може да има задачи в нея, защото веднага щом пристигне нова задача, изпълняваната нишка вече е взела задачата .
Когато нова задача влезе в опашката, ако има свободна активна нишка в пула, тогава тя взима задачата. Ако всички нишки са заети, тогава се създава нова нишка.
Кешираният пул започва с нула нишки и потенциално може да нарасне до Integer.MAX_VALUE нишки. По същество размерът на кеширания пул от нишки е ограничен само от системни ресурси.
За да спестят системни ресурси, кешираните пулове от нишки премахват нишки, които са неактивни за една minutesа.
Нека да видим How работи на практика. Ще създадем клас задачи, който моделира потребителска заявка:
public class Task implements Runnable {
int taskNumber;
public Task(int taskNumber) {
this.taskNumber = taskNumber;
}
@Override
public void run() {
System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
}
}
В основния метод създаваме newCachedThreadPool и след това добавяме 3 задачи за изпълнение. Тук отпечатваме статуса на нашата услуга (1) .
След това правим пауза за 30 секунди, започваме друга задача и показваме състоянието (2) .
След това поставяме нашата основна нишка на пауза за 70 секунди, отпечатваме състоянието (3) , след това отново добавяме 3 задачи и отново отпечатваме състоянието (4) .
На места, където показваме състоянието веднага след добавяне на задача, първо добавяме 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();
И ето резултата:
Обработена потребителска заявка #1 в нишка pool-1-thread-2
Обработена потребителска заявка #2 в нишка pool-1-thread-3
(1) java.util.concurrent .ThreadPoolExecutor@f6f4d33[Изпълнява се, размер на пула = 3, активни нишки = 0, поставени на опашка задачи = 0, завършени задачи = 3]
Обработена потребителска заявка #3 за нишка pool-1-thread-2
(2) java.util.concurrent. ThreadPoolExecutor@f6f4d33[Работи, размер на пула = 3, активни нишки = 0, поставени на опашка задачи = 0, завършени задачи = 4]
(3) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Работи, размер на пула = 0, активни нишки = 0 , поставени на опашка задачи = 0, завършени задачи = 4]
Обработена потребителска заявка #4 в нишка pool-1-thread-4
Обработена потребителска заявка #5 в нишка pool-1-thread-5
Обработена потребителска заявка #6 за нишка pool-1-thread-4
(4) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Изпълнява се, размер на пула = 2, активни нишки = 0, задачи на опашка = 0, завършени задачи = 7]
Нека да разгледаме всяка от стъпките:
стъпка | Обяснение |
---|---|
1 (след 3 изпълнени задачи) | Създадохме 3 нишки и 3 задачи бяха изпълнени на тези три нишки. Когато състоянието се покаже, всичките 3 задачи са изпълнени и нишките са готови за изпълнение на други задачи. |
2 (след 30-секундна пауза и изпълнение на друга задача) | След 30 секунди неактивност нишките са все още живи и чакат задачи. Друга задача се добавя и изпълнява върху нишка, взета от групата на останалите живи нишки. Не е добавена нова нишка към пула. |
3 (след пауза от 70 секунди) | Нишките са премахнати от пула. Няма нишки, готови да приемат задачи. |
4 (след изпълнение на още 3 задачи) | След като бяха получени повече задачи, бяха създадени нови теми. Този път само две теми успяха да обработят 3 задачи. |
Е, вече се запознахте с логиката на друг вид изпълнителска услуга.
По аналогия с други методи на помощния клас Executors , методът newCachedThreadPool също има претоварена version, която приема обект ThreadFactory като аргумент.
GO TO FULL VERSION