Einen Threadpool kann man sich wie einen ganz gewöhnlichen Whirlpool vorstellen, in dem eine beliebige Anzahl an Threads sitzen. Die Threads machen erstmal nichts, aber wenn man vorbeikommt und ihnen eine Aufgabe hinlegt, klettert einer aus dem Pool, stellt seinen Cocktail zur Seite und macht sich an die Arbeit. Wenn man schnell ist, und noch mehr Aufgaben platziert, stehen auch die anderen Threads auf.

Mehrere Threads im Pool Threads an der Arbeit

Bildliche Veranschaulichung: KI generierte Grafiken von OpenAI, August 2025

FixedThreadPool

Wie der Name bereits vermuten lässt, nutzt ein FixedThreadPool eine feste Anzahl an Threads. Wenn man davon ausgeht, dass die Threads nicht blockieren, ergibt es Sinn, hier mindestens die Anzahl der CPU-Kerne zu nehmen.

int cpuCoreCount = Runtime.getRuntime().availableProcessors();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(cpuCoreCount);

Sollten die Aufgaben hingegen eher IO nutzen, ist auch eine größere Zahl angemessen.

Wir können jetzt, weil es ein ganz normaler Executor ist, einfach Aufgaben per execute() in die Warteschlange packen. Der FixedThreadpool wird sie nacheinander verteilen und abhängig von der Anzahl der Threads, gleichzeitig bearbeiten.

Ein guter Anwendungsfall ist bei unserem Problem, dem Mapper, das Hochladen der einzelnen Dateien.

static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);

void uploadAndAttach(List<File> files) {
  // Erstelle einen Threadpool und lass ihn (gleichzeitig) alle Dateien hochladen
  for (var file : files) {
    THREAD_POOL.execute(() -> uploadAndAttachSingle(file));
  }
}

Andere Threadpools

Neben dem “FixedThreadPool” können wir auch verschiedene andere Implementierungen nutzen. Schauen wir uns einfach mal zwei davon an, und diskutieren die Vor- und Nachteile.

ScheduledThreadPool

Der “ScheduledThreadPool” ist ähnlich wie der “SingleThreadScheduledExecutor”, den wir ja bereits kennen. Unterschied ist, wer hätte es gedacht, dass dieser ScheduledExecutorService mehrere darunterliegende Threads verwendet. Wie viele das sind, kann man genau wie beim FixedThreadPool angeben.

Vorteile

  • Wenn eine Aufgabe in einem Aufruf von scheduleAtFixedRate, länger braucht, als das Interval, dann kann ein anderer Thread einspringen. So kann Überlastung verhindert werden.
  • Wenn man ihn mehrfach mit verschiedenen Aufgaben aufruft, können diese von verschiedenen Threads ausgeführt werden und blockieren sich nicht gegenseitig.

Nachteile

  • Wenn die Aufgabe in einem Aufruf von scheduleAtFixedRate immer schneller läuft, als das Interval, dann sind alle weiteren Threads überflüssig und sitzen untätig im Speicher.
  • Wenn man nur scheduleWithFixedDelay aufruft und mehr Threads erzeugt, als man Aufrufe getätigt hat, sind diese ebenfalls immer überflüssig.

CachedThreadPool

Du weißt also nicht, wie viele Threads du in deinen Pool lassen sollst? Diese Implementierung erstellt neue Threads, wenn sie gebraucht werden, nutzt existierende Threads, die gerade nicht arbeiten und fährt ungenutzte Threads wieder herunter, wenn sie für eine Minute nichts getan haben.

Klingt gut? Klingt… skalierbar?

Vorteile

  • Wenn du gerade keine Aufgaben hast, verbraucht der CachedThreadPool nur wenig Ressourcen.
  • Wenn du plötzlich ganz viele Aufgaben hast, passt sich der Pool entsprechend an.

Nachteil
Wenn der CachedThreadPool trotzdem nicht hinterherkommt, weil du ihm schneller Aufgaben gibst, als er sie abarbeiten kann, erstellt er immer mehr Threads. Irgendwann ist jedoch, wie wir bereits etabliert haben, der Idealpunkt überschritten und mehr Threads machen nur mehr zusätzliche Arbeit durch Scheduling. Jetzt wird die Anwendung also auch noch immer langsamer. Schließlich sprengst du mit den Threads deinen Arbeitsspeicher oder die Maximalanzahl vom Betriebssystem.

Dieser Fall darf also nie eintreten.

Aufgaben ablehnen

Es kann bei Executors also immer mal wieder dazu kommen, dass man mehr Aufgaben übergibt, als sie eigentlich bearbeiten können. Dies kann in der Realität zum Beispiel bei einem Webserver geschehen, welcher ganz viele HTTP-Anfragen bekommt. Was machen wir, wenn es zu viele werden?

Konfigurierter Threadpool

Um einen CachedThreadPool nach eigenen Kriterien zu erstellen, kann man einen ThreadPoolExecutor verwenden. Hier kommen direkt ein paar Neuerungen auf uns zu:

ExecutorService threadPool = new ThreadPoolExecutor(
	1, 40, // (corePoolSize & maximumPoolSize)
	1, TimeUnit.MINUTES, // (keepAliveTime & unit)
	new LinkedBlockingQueue<>(), // (workQueue)
	new ThreadPoolExecutor.AbortPolicy() // (handler)
);

corePoolSize & maximumPoolSize

  • Die Mindestanzahl an Threads sollten verständlich sein. Der Pool hält von Grund auf immer mindestens einen Thread vor, falls neue Aufgaben kommen. Dieser Wert sollte so niedrig gesetzt sein, dass alle Threads immer ausgelastet sind.

  • Die Höchstanzahl ist die obere Grenze für Threads. Im gezeigten Beispiel werden nicht mehr als 40 Threads erstellt, dieser Wert hängt jedoch immer vom Zielsystem ab und sollte unterhalb der Grenze gesetzt werden, ab der noch mehr Threads das Programm verlangsamen würden.

Diese Mindest- und Höchstwerte für die eigene Anwendung/Umgebung herauszufinden ist leider äußerst knifflig. Hier hilft meistens nur rigoroses Herumprobieren.

keepAliveTime & unit
Gibt an, nach welcher Zeit ungenutzte Threads terminieren. Die Anzahl der Threads wird jedoch nie unter die corePoolSize fallen, selbst, wenn der Threadpool gar nichts tut.

workQueue
Ist die Warteschlange, in der die Aufgaben abgearbeitet werden. Hier gibt es drei interessante Versionen:

  • new LinkedBlockingQueue<>() Unbegrenzte Warteschlange
  • new ArrayBlockingQueue<>(100) Begrenzte Warteschlange (hier mit Kapazität 100)
  • new SynchronousQueue<>() Warteschlange, bei der jeder insert blockiert, bis das jeweilige Element herausgenommen wurde, und jedes remove blockiert, bis ein Element hinzugefügt wurde.

Für den Anfang empfiehlt es sich jedoch, einfach die unbegrenzte Warteschlange zu nehmen, und eine konservative maximumPoolSize zu wählen.

handler
Der Handler steuert, was passiert, wenn es entweder mehr Arbeit gibt, als die Threads erledigen können oder (bei begrenzter Warteschlange) mehr Aufgaben gepushed werden, als in die workQueue passen.

Handler muss man nicht selbst implementieren, es gibt bereits vier verschiedene Klassen in ThreadPoolExecutor, die wir einfach nutzen können:

Handler Was macht er? Mögliche Folgen
AbortPolicy Er wirft eine RejectedExecutionException. Kann dazu führen, dass der Executor stoppt.
DiscardPolicy Er ignoriert die Aufgabe. Gefährlich, denn das bekommt man nicht mit.
DiscardOldestPolicy Ersetzt die Aufgabe, die an Stelle 1 in der Warteschlange steht. Das bekommt man nicht mit. Ändert die Reihenfolge der Warteschlange.
CallerRunsPolicy Lässt sofort den aufrufenden Thread die Aufgabe durchführen. Hauptthread wird blockiert.

Am Beispiel

Da wir nun ein Gefühl dafür haben, wie wir mit überschüssigen Aufgaben umgehen, können wir das Gelernte wieder an unserem Anwendungsbeispiel, dem Mapper ausprobieren.

static final int CPU_CORES = Runtime.getRuntime().availableProcessors();

static final ExecutorService THREAD_POOL = new ThreadPoolExecutor(
	CPU_CORES, // Nutzer mindestens alle Kerne
	CPU_CORES * 8, // Erstelle pro Kern im Schnitt 8 Threads
	6, TimeUnit.MINUTES, // Behalte die Threads über ein Polling-Interval (5min)
	new LinkedBlockingQueue<>(), // Begrenze nicht die Anzahl der Aufgaben
	new ThreadPoolExecutor.CallerRunsPolicy() // Nutze den Hauptthread bei Überlastung
);

void uploadAndAttach(List<File> files) {
	for (var file : files) {
		THREAD_POOL.execute(() -> uploadAndAttachSingle(file));
	}
}

So viel hat sich gar nicht geändert, oder?

Fazit

In diesem Artikel haben wir Threadpools als wertvolle Werkzeuge zur gleichzeitigen Abarbeitung verschiedener Aufgaben kennengelernt. Wir wissen jetzt, was Threads sind, wie man sie erstellt und herunterfährt, wie man sie auslastet und wie man Überlastung verhindert. Im nächsten Artikel schauen wir uns auch noch an, wie man mit Arbeitsergebnissen umgeht. Den Dateidownload wollen wir ja auch parallelisieren, und dafür müssen wir auch irgendwann mitbekommen, dass eine Aufgabe beendet wurde und was ihr Ergebnis war. Das Konzept nennt sich Futures.