Thread Pool — ist ein Thread-Verwaltungsmechanismus, bei dem ein zuvor erstellter Pool von Threads zur Ausführung von Aufgaben wiederverwendet wird, wodurch der Overhead beim Erstellen und Zerstören von Threads vermieden wird. In der mobilen Entwicklung wird der Thread-Pool für Hintergrundoperationen verwendet: Netzwerkanfragen, Bildverarbeitung, Datenbankoperationen. Laut Google Android Documentation (2025) ist ExecutorService die empfohlene Methode zur Verwaltung von Hintergrund-Threads unter Android. Unter iOS übernehmen OperationQueue und GCD DispatchQueue mit globalen Concurrent-Warteschlangen eine ähnliche Rolle.
Wichtige Punkte
Thread Pool (Thread-Pool) — ist ein Architekturmuster, bei dem eine festgelegte Anzahl von Threads im Voraus erstellt und zur Ausführung mehrerer Aufgaben wiederverwendet wird. Anstatt für jede Operation einen neuen Thread zu erstellen (was teuer ist: etwa 1 MB Stack pro Thread in der JVM), werden Aufgaben in eine Warteschlange gestellt und von verfügbaren Threads aus dem Pool ausgeführt. In der mobilen Entwicklung ist der Thread-Pool entscheidend für die Leistung — Android und iOS begrenzen die Anzahl der Threads pro Anwendung.
Das Erstellen eines Threads ist eine teure Operation: Stack-Zuweisung, Systemregistrierung, Kontextwechsel. Auf mobilen Geräten mit begrenzten Ressourcen führt unkontrollierte Thread-Erstellung zu OOM (OutOfMemoryError) unter Android und Drosselung unter iOS. Thread Pool löst beide Probleme: Er begrenzt die maximale Anzahl gleichzeitig laufender Threads und verwendet bereits erstellte Threads wieder. Google empfiehlt ExecutorService statt raw Thread(), Apple empfiehlt OperationQueue statt Thread.
| Parameter | Ohne Pool (raw Thread) | Mit Thread Pool |
|---|---|---|
| Thread-Erstellung | Für jede Aufgabe | Einmalig bei Pool-Erstellung |
| Maximale Threads | Unbegrenzt (OOM-Risiko) | Begrenzt durch core/max pool size |
| Auslastung | Niedrig (Thread stirbt nach Aufgabe) | Hoch (Thread wird wiederverwendet) |
| Verwaltung | Manuell (join, interrupt) | Automatisch (ExecutorService) |
| Speicherverbrauch | Wächst mit jeder Aufgabe | Fest |
Der Thread-Pool arbeitet nach dem Producer-Consumer-Prinzip: Aufgaben (Runnable/Callable) werden in eine blockierende Warteschlange (BlockingQueue) gestellt. Threads aus dem Pool warten in der Warteschlange auf Aufgaben und nehmen sie zur Ausführung. Algorithmus: Wenn die Anzahl freier Threads kleiner als corePoolSize ist, wird ein neuer Thread erstellt. Wenn corePoolSize erreicht ist, wird die Aufgabe in die Warteschlange gestellt. Wenn die Warteschlange voll ist und die Thread-Anzahl kleiner als maximumPoolSize ist, wird ein zusätzlicher Thread erstellt. Wenn maximumPoolSize überschritten wird, wird die Aufgabe über RejectedExecutionHandler abgelehnt.
Core pool size — die Anzahl der Threads, die auch im Leerlauf im Pool gehalten werden. Maximum pool size — die maximale Anzahl von Threads, die bei Überlauf der Warteschlange erstellt werden kann. Der Unterschied zwischen ihnen sind die zusätzlichen (Overflow-)Threads, die vorübergehend erstellt und nach dem Leerlauf-Timeout beendet werden. Auf mobilen Geräten wird empfohlen, corePoolSize gleich maximumPoolSize zu setzen, um Spitzenlasten durch Thread-Erstellung zu vermeiden.
BlockingQueue speichert Aufgaben, die auf die Ausführung warten. Die gängigsten Implementierungen: LinkedBlockingQueue (unbegrenzt), ArrayBlockingQueue (begrenzt) und SynchronousQueue (ohne Speicherung — Aufgabe wird direkt an einen Thread übergeben). Wenn Warteschlange und Pool voll sind, wird RejectedExecutionHandler ausgelöst. Standardrichtlinien: AbortPolicy (wirft RejectedExecutionException), CallerRunsPolicy (führt im Thread des Aufrufers aus), DiscardPolicy und DiscardOldestPolicy.
// Thread Pool in Android erstellen
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // Minimum 2 Threads
maximumPoolSize = 4, // Maximum 4 Threads
keepAliveTime = 30L, // Lebensdauer des Overflow-Threads
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// Aufgaben einreichen
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// Pool herunterfahren
threadPool.shutdown()
// Auf Abschluss aller Aufgaben warten
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android bietet mehrere Thread-Pool-Implementierungen über java.util.concurrent. Executors — eine Factory mit vorgefertigten Konfigurationen: newFixedThreadPool(n) (fester Pool), newCachedThreadPool() (unbegrenzt, Threads werden bei Bedarf erstellt), newSingleThreadExecutor() (einzelner Thread — sequentielle Ausführung). Für mobile Projekte wird newFixedThreadPool mit einer angemessenen Grenze (2-4 Threads) empfohlen, da ein gecachter Pool zu viele Threads erstellen kann.
ThreadPoolExecutor (TPE) — eine vollständige Implementierung von ExecutorService mit konfigurierbaren Parametern. Unter Android wird TPE innerhalb von AsyncTask, IntentService und JobIntentService verwendet. Die Parameter corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue und RejectedExecutionHandler ermöglichen eine Feinabstimmung des Pool-Verhaltens. Empfehlungen für Android: corePoolSize = Anzahl der CPU-Kerne - 1 (für IO-gebundene Aufgaben) oder Anzahl der Kerne (für CPU-gebundene Aufgaben). Für typische Anwendungen: 2-4 Threads.
// Vorgefertigte Executors-Konfigurationen
// 1. Fester Pool mit 3 Threads
val fixedPool = Executors.newFixedThreadPool(3)
// 2. Gecachter Pool (nicht für Mobilgeräte empfohlen)
val cachedPool = Executors.newCachedThreadPool()
// 3. Einzelner Thread (Serialisierung)
val singlePool = Executors.newSingleThreadExecutor()
// 4. Planer (periodische Aufgaben)
val scheduler = Executors.newScheduledThreadPool(2)
// Verwendung mit Callable und Future
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// Ergebnis abrufen (blockiert den Thread)
val result = future.get(5, TimeUnit.SECONDS)
// Pool herunterfahren
fixedPool.shutdownNow()
Kotlin-Koroutinen bieten CoroutineDispatcher — eine Abstraktion, die einem Thread-Pool ähnelt. Dispatchers.IO verwendet einen Pool von 64 Threads (begrenzt). Dispatchers.Default — ein Pool, der der Anzahl der CPU-Kerne entspricht. CoroutineDispatcher benötigt kein manuelles Herunterfahren und wird automatisch verwaltet. Für die Feinabstimmung erstellen Sie einen benutzerdefinierten ExecutorCoroutineDispatcher über Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Koroutinen ersetzen den Thread-Pool nicht, sondern kapseln ihn.
iOS bietet zwei Hauptmechanismen zur Verwaltung des Thread-Pools: OperationQueue (High-Level-API auf Basis von GCD) und GCD DispatchQueue (Low-Level-C-API). OperationQueue kapselt einen Thread-Pool über die Eigenschaft maxConcurrentOperationCount. Standardmäßig verwendet OperationQueue ein systemdefiniertes Maximum (abhängig von der Systemlast). DispatchQueue.global() stellt eine Concurrent-Warteschlange mit einem System-Thread-Pool bereit.
OperationQueue verwaltet den Thread-Pool über maxConcurrentOperationCount. Der Wert 1 erstellt eine serielle Warteschlange (Analog zu einem Single-Thread-Pool). Ein Wert größer als 1 erstellt einen Concurrent-Pool mit dem angegebenen Limit. Standardmäßig ist maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (System-Optimum, normalerweise 4-8 Threads). Operation unterstützt Abhängigkeiten, Prioritäten und Abbruch. Jede Operation wird auf einem beliebigen verfügbaren Thread aus dem System-Pool ausgeführt.
// OperationQueue mit einem Pool von 3 Threads
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// Operationen erstellen
let operation1 = BlockOperation {
let data = fetchData(from: url1)
DispatchQueue.main.async { updateUI(data) }
}
let operation2 = BlockOperation {
let data = fetchData(from: url2)
DispatchQueue.main.async { updateUI(data) }
}
// Abhängigkeit: operation2 wartet auf operation1
operation2.addDependency(operation1)
// Zur Warteschlange hinzufügen
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// Alle Operationen abbrechen
queue.cancelAllOperations()
DispatchQueue — ist der Thread-Pool von Apple. Eine Concurrent-Warteschlange (qos: .utility) verwendet den System-Thread-Pool, der für die aktuelle Gerätelast optimiert ist. Verschiedene QoS-Stufen (userInteractive, userInitiated, utility, background) werden auf verschiedene Pools mit unterschiedlichen Prioritäten abgebildet. DispatchGroup ermöglicht die Synchronisierung mehrerer Aufgaben. DispatchWorkItem unterstützt Abbruch und qualityOfService. Für eine feinere Steuerung erstellen Sie benutzerdefinierte Concurrent-Warteschlangen über DispatchQueue(label: qos: attributes: .concurrent).
// GCD DispatchQueue als Thread-Pool
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// Aufgaben an den Pool senden
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup zur Synchronisierung
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .utility)
pool.async(group: group) { fetchData() }
pool.async(group: group) { processImage() }
group.notify(queue: .main) {
self.showResult() // Beide Aufgaben abgeschlossen
}
// Parallelität durch Semaphor begrenzen
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
Die Thread-Pool-Konfiguration wirkt sich direkt auf die Anwendungsleistung aus. Falsche Parameter führen zu einer Unterauslastung der CPU (zu wenige Threads) oder einer Systemüberlastung (zu viele). Für mobile Anwendungen unterscheiden sich die optimalen Werte aufgrund der begrenzten Ressourcen und des Stromverbrauchs von serverseitigen Werten. Die wichtigsten Parameter sind: corePoolSize, maxPoolSize, Warteschlangenkapazität und keepAliveTime.
Formel für IO-gebundene Aufgaben: corePoolSize = Anzahl der CPU-Kerne × 2 (Threads warten auf E/A). Für CPU-gebundene Aufgaben: corePoolSize = Anzahl der CPU-Kerne (Threads sind ständig mit Berechnungen beschäftigt). Auf modernen mobilen Geräten (6-8 Kerne) ergibt dies 6-8 Threads für CPU-gebundene und 12-16 für IO-gebundene Aufgaben. Praktische Tests zeigen, dass für eine typische mobile Anwendung 3-4 Threads optimal sind — mehr Threads erhöhen den Stromverbrauch ohne Leistungssteigerung.
Die Größe der Arbeitswarteschlange (Work Queue) bestimmt, wie viele Aufgaben auf die Ausführung warten können. Unbegrenzte Warteschlange (LinkedBlockingQueue ohne Limit) kann bei schnellem Aufgabenaufkommen zu OOM führen. Begrenzte Warteschlange (ArrayBlockingQueue mit fester Größe) lehnt Aufgaben bei Vollsein ab. Für mobile Anwendungen wird ArrayBlockingQueue mit einer Kapazität von 16-32 Aufgaben empfohlen. CallerRunsPolicy ist der beste RejectedExecutionHandler für Mobilgeräte: Er verlangsamt den Aufrufer (Backpressure) anstatt die Aufgabe zu verlieren.
| Parameter | Empfehlung für Mobilgeräte | Begründung |
|---|---|---|
| corePoolSize | 2-4 | Begrenzte Ressourcen mobiler Geräte |
| maxPoolSize | corePoolSize (oder +1-2) | Spitzenlasten durch Thread-Erstellung vermeiden |
| keepAliveTime | 15-30 Sekunden | Schnelle Speicherfreigabe ohne häufige Erstellung |
| Warteschlangenkapazität | 16-32 | Gleichgewicht zwischen Pufferung und OOM-Risiko |
| Handler | CallerRunsPolicy | Backpressure ohne Aufgabenverlust |
Entwickler mobiler Anwendungen machen bei der Verwendung des Thread-Pools häufig Fehler, die zu Abstürzen, Speicherlecks und instabilem Betrieb führen. Die häufigsten: Kein Aufruf von shutdown() für ExecutorService, Erstellen eines neuen Pools für jede Operation, zu großer Pool, Deadlock zwischen Aufgaben, Verwendung von CachedThreadPool unter Android.
Ein Deadlock tritt auf, wenn eine Aufgabe im Pool auf das Ergebnis einer anderen Aufgabe aus demselben Pool wartet, aber alle Threads mit Warten beschäftigt sind. Beispiel: Aufgabe A sendet Aufgabe B an denselben Pool und ruft future.get() auf — wenn der Pool erschöpft ist, wartet Aufgabe A auf Aufgabe B, und Aufgabe B kann nicht ausgeführt werden, da keine freien Threads vorhanden sind. Lösung: Verwenden Sie separate Pools für verschiedene Aufgabenebenen oder asynchrone Callbacks anstelle von blockierendem .get().
// Deadlock im Thread Pool
val pool = Executors.newFixedThreadPool(1)
// Aufgabe A wartet auf Aufgabe B — Deadlock!
val futureA = pool.submit {
// Diese Aufgabe wird nie ausgeführt
val futureB = pool.submit { 42 }
futureB.get() // Blockiert für immer
}
// Lösung: separate Pools
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// Wird in separatem Pool ausgeführt — Deadlock unmöglich
}
}
// Oder verwenden Sie CompletableFuture
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
Ein in einer Activity erstellter ExecutorService muss in onDestroy() heruntergefahren werden. Wenn dies nicht geschieht, bleiben die Threads auch nach der Zerstörung der Activity im Speicher hängen. Lösung: Halten Sie den Pool im Application-Bereich oder ViewModel, nicht in der Activity. Für Koroutinen verwenden Sie viewModelScope oder lifecycleScope. Wenn der Pool innerhalb einer Activity erstellt wird, rufen Sie unbedingt pool.shutdown() in onDestroy() auf. Für Tests verwenden Sie es.shutdownNow() für sofortigen Stopp.
Häufig gestellte Fragen
Thread Pool verwendet bereits erstellte Threads wieder, um mehrere Aufgaben auszuführen. Ein normaler Thread (raw Thread) wird erstellt, führt eine Aufgabe aus und wird zerstört. Das Erstellen eines Threads benötigt etwa 1 MB Speicher und ~1 ms Zeit. Thread Pool reduziert den Overhead, begrenzt die maximale Anzahl von Threads und bietet eine API für die Verwaltung (shutdown, awaitTermination).
Für eine typische mobile Anwendung sind 2-4 Threads optimal. Für CPU-gebundene Aufgaben — die Anzahl der CPU-Kerne. Für IO-gebundene Aufgaben — Anzahl der Kerne × 2. Mehr Threads erhöhen den Stromverbrauch und Kontextwechsel ohne Leistungssteigerung. Unter Android verwenden Sie Process.availableProcessors() zur Bestimmung der Kerne. Unter iOS — ProcessInfo.processInfo.processorCount.
CachedThreadPool erstellt Threads nach Bedarf und verwendet vorhandene wieder. Das Problem: Er begrenzt die maximale Anzahl von Threads nicht. Wenn 100 Aufgaben gleichzeitig eintreffen, werden 100 Threads erstellt. Dies führt unter Android zu OOM (jeder Thread ~1 MB). Verwenden Sie newFixedThreadPool(n) mit einer expliziten Grenze. CachedThreadPool ist nur für kurzlebige Burst-Aufgaben mit garantiert geringem Volumen akzeptabel.
Ja, wenn der Pool nicht zu einem verwalteten Container (wie Koroutinen) gehört. shutdown() stoppt die Annahme neuer Aufgaben und beendet Threads nach Abschluss der aktuellen. Ohne shutdown() bleiben Threads im Speicher hängen und die Anwendung wird nicht beendet. Für Activity rufen Sie es in onDestroy() auf. Für ViewModel verwenden Sie coroutineScope. Das Herunterfahren des Pools ist ein obligatorischer Teil der Ressourcenverwaltung, ähnlich dem Schließen eines Cursors oder InputStreams.
Ja, OperationQueue und DispatchQueue sind von iOS bereitgestellte Thread-Pools. OperationQueue begrenzt die Gleichzeitigkeit über maxConcurrentOperationCount. DispatchQueue.global() verwendet den System-Thread-Pool ohne direkte Kontrolle. Im Gegensatz zu Java ThreadPoolExecutor verwalten Sie nicht corePoolSize oder Warteschlangenkapazität — das System optimiert den Pool automatisch basierend auf der aktuellen Last und dem Stromverbrauch des Geräts.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch