W czasach, gdy nawet smartfony wyposażone są w procesory ośmiordzeniowe normalną sytuacją jest dostosowanie aplikacji do wykorzystania mnogości zasobów, które dostarcza sprzęt. Praktycznie wszystkie nowoczesne aplikacje zoptymalizowane są w celu maksymalnego wykorzystania tych własności. Pulą wątków (Thread pool) nazywamy wzorzec projektowania oprogramowania do osiągnięcia współbieżności wykonania w programach komputerowych.

Z racji, że operacje takie wymagają dużo zasobów, zarówno pamięci RAM jak i użycia procesora, dobrą praktyką przy tworzeniu aplikacji jest to, aby programista sam nie tworzył i niszczył wątków. Co więcej, w takiej sytuacji programista musi pamiętać, aby obsłużyć wszystkie sytuacje wyjątkowe.

Domyślnie dla 64 bitowego środowiska, każdy z wątków na starcie zajmuje 1MB pamięci. Jak łatwo sobie wyobrazić – tworząc nowe wątki bez ścisłej kontroli – aplikacja szybko może zaalokować całą dostępną pamięć. Klasycznym przykładem antywzorca w tym obszarze jest tworzenie nowego wątku dla każdego requestu w aplikacji webowej – takie rozwiązanie szybko położy naszą aplikację.

Java, począwszy od wersji 1.5 dostarcza nam fabrykę Executors ze statycznymi metodami do tworzenia puli wątków. Chcąc utworzyć jeden nowy wątek, skorzystać możemy z metody newSingleThreadExecutor, po prostu wołając metodę:

Executors.newSingleThreadExecutor()

Gdy potrzebujesz skorzystać z puli wątków o określonej ilości wątków – pomoże Ci w tym metoda: newFixedThreadPool, której wykorzystanie jest równie proste:

Executors.newFixedThreadPool(int numberOfThreads)

gdzie jako parametr przyjmuje ilość wątków. Wątki w takiej puli są reużywane.

Jeżeli masz potrzebę uruchomić zadania z opóźnieniem – wykorzystaj metodę:

Executors.newCachedThreadPool()

np:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(60); 

ScheduledFuture<String> scheduledFuture = scheduledExecutorService.schedule(task, 60, TimeUnit.SECONDS); 

scheduledFuture.get();

w parametrze podając ilość potrzebnych wątków.

W specyficznym przypadku potrzebować będziesz jednak puli, która nie ma na sztywno określonej ilości wątków. Swoją potrzebę zrealizować będziesz mógł przy pomocy newCachedThreadPool. Pula ta, gdy potrzebuje nowych wątków – tworzy je. Gdy wątek zakończy pracę – może być użyty ponownie, pod warunkiem, że będzie mieć co robić. W tej implementacji puli, wszystkie wątki, które są bezczynne przez co najmniej minutę – są z niej usuwane. Pula ta dobrze sprawdzi się, gdy posiadasz krótkie zadania do zrealizowania. Skorzystać z niej możesz przez wywołanie metody:

Executors.newCachedThreadPool()

Możesz też skorzystać z algorytmu Work Stealing:

Executors.newWorkStealingPool()

Algorytm ten polega na tym, że gdy bezczynnym wątkom zabraknie pracy – “podkradają” je innym wątkom. Algorytm rozdziela zadania na mniejsze podzadania i umieszcza je na kolejce. Podzadania są wykonywane w różnych wątkach, natomiast te, które skończą swoją pracę – podkradają je innym. Implementacją puli tego typu w Javie jest ForkJoinPool.

Skorzystaj z tej puli, gdy masz do zrobienia zadania, które łatwo możesz podzielić na mniejsze.

Więcej o Executorach i ExecutorService poczytać możesz w oficjalnej dokumentacji Java:

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html