Изпълнители newCachedThreadPool () срещу newFixedThreadPool ()

1. Общ преглед

Що се отнася до реализациите на пула от нишки, стандартната библиотека на Java предлага много възможности за избор. Пуловете с фиксирана и кеширана нишка са доста повсеместни сред тези изпълнения.

В този урок ще видим как пуловете от нишки работят под капака и след това ще сравним тези имплементации и техните случаи на употреба.

2. Кеширан пул за нишки

Нека да разгледаме как Java създава кеширан пул от нишки, когато извикаме Executors.newCachedThreadPool () :

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

Кешираните пулове от нишки използват „синхронно предаване“ за поставяне на нови задачи в опашката. Основната идея за синхронно предаване е проста и същевременно противоинтуитивна: Човек може да постави на опашка елемент, ако и само ако друга нишка вземе този елемент едновременно. С други думи, на SynchronousQueue не може да побере всички задачи, каквато.

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

Кешираният пул започва с нула нишки и потенциално може да нарасне до нишки Integer.MAX_VALUE . На практика единственото ограничение за кеширан пул от нишки е наличните системни ресурси.

За по-добро управление на системните ресурси кешираните пулове от нишки ще премахнат нишките, които остават неактивни за една минута.

2.1. Случаи на употреба

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

Ключът тук е „разумен“ и „краткотраен“. За да изясним тази точка, нека оценим сценарий, при който кешираните пулове не са подходящи. Тук ще изпратим един милион задачи, всяка от които отнема 100 микросекунди за завършване:

Callable task = () -> { long oneHundredMicroSeconds = 100_000; long startedAt = System.nanoTime(); while (System.nanoTime() - startedAt  task).collect(toList()); var result = cachedPool.invokeAll(tasks);

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

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

3. Пул с фиксирана нишка

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

public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }

За разлика от кеширания пул от нишки, този използва неограничена опашка с фиксиран брой неизтичащи нишки . Следователно, вместо постоянно нарастващ брой нишки, фиксираният пул от нишки се опитва да изпълнява входящи задачи с фиксирано количество нишки . Когато всички нишки са заети, изпълнителят ще постави на опашка нови задачи. По този начин имаме по-голям контрол върху потреблението на ресурси на нашата програма.

В резултат на това фиксираните пулове от нишки са по-подходящи за задачи с непредсказуемо време за изпълнение.

4. Нещастни прилики

Досега изброихме само разликите между кешираните и фиксираните пулове от нишки.

Като се вземат предвид всички тези разлики, и двамата използват AbortPolicy като своя политика за насищане . Следователно очакваме тези изпълнители да хвърлят изключение, когато не могат да приемат и дори да поставят на опашка повече задачи.

Нека да видим какво се случва в реалния свят.

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

Тъй като и двата пула няма да бъдат наситени, когато натоварването е изключително голямо, те ще изразходват много памет за създаване на нишки или задачи за опашки. Добавяйки обида към нараняването, кешираните пулове от нишки също ще доведат до много превключватели на контекста на процесора.

Както и да е, за да имате по-голям контрол върху консумацията на ресурси, силно се препоръчва да създадете персонализиран ThreadPoolExecutor :

var boundedQueue = new ArrayBlockingQueue(1000); new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy()); 

Тук нашият пул от нишки може да има до 20 нишки и може да постави на опашка до 1000 задачи. Също така, когато не може да приеме повече товар, той просто ще изведе изключение.

5. Заключение

В този урок имахме надникване в изходния код на JDK, за да видим как различните изпълнители работят под капака. След това сравнихме фиксираните и кеширани пулове от нишки и техните случаи на употреба.

В крайна сметка се опитахме да се справим с потреблението на ресурси извън контрола на тези пулове с персонализирани пулове от нишки.