Cel: Napisać minimalną aplikację demonstrującą użycie pul wątków.
Rozwiązanie
build.gradle
src
└── main
└── java
└── dev
└── galinski
└── jedendziennie
└── threadpool
└── App.java
App.java
package dev.galinski.jedendziennie.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class App {
public static void main(String[] args) {
System.out.println("main");
int poolSize = 2; // max 2 zadania równocześnie
System.out.println(String.format("Running in pool of %d threads",
poolSize));
ExecutorService pool = Executors.newFixedThreadPool(poolSize);
for(int i=0; i < 5; i++) { // wszystkich zadań jest 5
pool.execute(new Task(i));
}
pool.shutdown(); // koniec przyjmowania nowych zadań
}
}
class Task implements Runnable {
String name;
Task(Integer id) {
this.name = "task" + Integer.toString(id);
System.out.println("Created task " + this.name);
}
public void run() {
int i = 3;
while (i > 0) {
try { Thread.sleep(300); }
catch (InterruptedException e) {}
// przedstaw się
String threadName = Thread.currentThread().getName();
String message = String.format("%s %s %d", threadName, name, i);
System.out.println(message);
i--;
}
}
}
build.gradle
plugins {
id 'java'
id 'application'
}
application {
mainClassName = 'dev.galinski.jedendziennie.threadpool.App'
}
…trzeba przyznać, minimalny build.gradle
dla Javy jest prosty.
Uruchamianie
$ gradle run
> Task :run
main
Running in pool of 2 threads
Created task task0
Created task task1
Created task task2
Created task task3
Created task task4
pool-1-thread-1 task0 3
pool-1-thread-2 task1 3
pool-1-thread-1 task0 2
pool-1-thread-2 task1 2
pool-1-thread-1 task0 1
pool-1-thread-2 task1 1
pool-1-thread-1 task2 3
pool-1-thread-2 task3 3
pool-1-thread-1 task2 2
pool-1-thread-2 task3 2
pool-1-thread-1 task2 1
pool-1-thread-2 task3 1
pool-1-thread-1 task4 3
pool-1-thread-1 task4 2
pool-1-thread-1 task4 1
Jak widać, równocześnie wykonują się najwyżej 2 zadania, pozostałe czekają w kolejce.
Więcej o pulach wątków
Pula wątków pozwala zarządzać zasobami przeznaczanymi na równoczesne wykonanie
zadań - głównie przez wielokrotne używanie obiektów wątków. Korzystanie z pul
wątków z java.util.concurrent
odbywa się za pośrednictwem obiektów
ExecutorService
, tworzonych np. metodami fabrykującymi z klasy Executors
:
newFixedThreadPool(int n)
- pula o stałej liczbie wątków dzielących jedną kolejkę zadań. Wątki, które zginą (np. z powodu wyjątku) są zastępowane przez nowe, które podejmują następne zadania.newCachedThreadPool()
- wątki, które utworzy są trzymane w gotowości przez 60 sekund; jeśli w tym czasie pojawi się nowe zadanie, wątek jest używany ponownie, jeśli nie - zostaje usunięty.newSingleThreadExecutor()
- jeden wątek, wykonanie sekwencyjne.newScheduledThreadPool(int n)
,newSingleThreadScheduledExecutor()
- pozwalają tworzyćScheduledExecutorService
, czyli egzekutory, które mogą kolejkować zadania opóźnione i cykliczne.newWorkStealingPool(int n)
- liczba wątków może zmieniać się dynamicznie, ale nie przekroczyn
. To nakładka naForkJoinPool
: każdy procesor ma swoją kolejkę zadań, ale dostęp do niej jest współdzielony. “Jeśli zużyję swoje zadania, a kolega jest zajęty, mogę mu podkraść zadanie”. Ten rodzaj puli nie gwarantuje stabilnej kolejności wykonania zadań.
Jeśli “zwykłe” metody fabrykujące z Executors
nie wystarczają), zawsze można
użyć bardziej szczegółowego interfejsu ThreadPoolExecutor()
. Więcej w dokumentacji
Oracle.
Rozmaitości
- Po chwili programowania w Kotlinie i Scali Java 8 wydaje się archaiczna i
koszmarnie rozwlekła. Denerwuje zwłaszcza brak zmiennych automatycznych (na
szczęście już od wersji 10 można pisać
var msg = "Ala ma kota"
. - Czym różni się interfejs
Runnable
odCallable
? –Runnable
jest starszy, prostszy i ma mniej funkcjonalności (nie może zwrócić wartości, nie może delegować specjalnych wyjątków).