Thread Pool — dit is een mechanisme voor threadbeheer waarbij een vooraf gemaakte pool van threads wordt hergebruikt om taken uit te voeren, waardoor de overhead van het maken en vernietigen van threads wordt vermeden. In mobiele ontwikkeling wordt de threadpool gebruikt voor achtergrondbewerkingen: netwerkverzoeken, beeldverwerking, werken met databases. Volgens Google Android Documentation (2025) is ExecutorService de aanbevolen manier om achtergrondthreads in Android te beheren. In iOS vervullen OperationQueue en GCD DispatchQueue met globale gelijktijdige wachtrijen een vergelijkbare rol.
Belangrijkste punten
Thread Pool (threadpool) — is een architectuurpatroon waarbij een vast aantal threads vooraf wordt gemaakt en hergebruikt voor het uitvoeren van meerdere taken. In plaats van een nieuwe thread te maken voor elke bewerking (wat kostbaar is: ongeveer 1 MB stack per thread in de JVM), worden taken in een wachtrij geplaatst en uitgevoerd door vrije threads uit de pool. In mobiele ontwikkeling is de threadpool cruciaal voor prestaties — Android en iOS beperken het aantal threads per app.
Het maken van een thread is een dure bewerking: stacktoewijzing, registratie in het systeem, contextwisseling. Op mobiele apparaten met beperkte resources leidt onbeheerste threadcreatie tot OOM (OutOfMemoryError) op Android en throttling op iOS. Thread Pool lost beide problemen op: het beperkt het maximale aantal gelijktijdig werkende threads en hergebruikt reeds gemaakte threads. Google beveelt ExecutorService aan in plaats van raw Thread(), Apple beveelt OperationQueue aan in plaats van Thread.
| Parameter | Zonder pool (raw Thread) | Met Thread Pool |
|---|---|---|
| Thread maken | Voor elke taak | Eenmalig bij het maken van de pool |
| Maximum threads | Onbeperkt (OOM risico) | Beperkt door core/max pool size |
| Benutting | Laag (thread sterft na taak) | Hoog (thread wordt hergebruikt) |
| Beheer | Handmatig (join, interrupt) | Automatisch (ExecutorService) |
| Geheugengebruik | Neemt toe met elke taak | Vast |
De threadpool werkt volgens het Producer-Consumer-principe: taken (Runnable/Callable) worden in een blokkerende wachtrij (BlockingQueue) geplaatst. Threads uit de pool wachten op taken in de wachtrij en nemen ze op voor uitvoering. Algoritme: als er minder vrije threads zijn dan corePoolSize, wordt een nieuwe thread gemaakt. Als corePoolSize is bereikt, wordt de taak in de wachtrij geplaatst. Als de wachtrij vol is en er minder threads zijn dan maximumPoolSize, wordt een extra thread gemaakt. Bij overschrijding van maximumPoolSize wordt de taak geweigerd via RejectedExecutionHandler.
Core pool size — het aantal threads dat in de pool wordt gehouden, zelfs in rust. Maximum pool size — het maximale aantal threads dat kan worden gemaakt bij overloop van de wachtrij. Het verschil ertussen zijn extra (overflow) threads die tijdelijk worden gemaakt en eindigen na een inactiviteitstime-out. Op mobiele apparaten wordt aanbevolen om corePoolSize gelijk te stellen aan maximumPoolSize om piekbelastingen bij het maken van threads te voorkomen.
BlockingQueue slaat taken op die wachten op uitvoering. De meest populaire implementaties: LinkedBlockingQueue (onbeperkt), ArrayBlockingQueue (beperkt) en SynchronousQueue (zonder opslag — taak wordt direct aan een thread doorgegeven). Bij overloop van de wachtrij en de pool wordt RejectedExecutionHandler geactiveerd. Standaardbeleid: AbortPolicy (gooit RejectedExecutionException), CallerRunsPolicy (voert uit in de thread van de afzender), DiscardPolicy en DiscardOldestPolicy.
// Thread Pool aanmaken in Android
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // Minimaal 2 threads
maximumPoolSize = 4, // Maximaal 4 threads
keepAliveTime = 30L, // Levensduur overflow-thread
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// Taken verzenden
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// Pool afsluiten
threadPool.shutdown()
// Wachten op voltooiing van alle taken
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android biedt verschillende implementaties van threadpools via java.util.concurrent. Executors — een fabriek met kant-en-klare configuraties: newFixedThreadPool(n) (vaste pool), newCachedThreadPool() (onbeperkt, threads worden indien nodig gemaakt), newSingleThreadExecutor() (één thread — sequentiële uitvoering). Voor mobiele projecten wordt newFixedThreadPool met een redelijke limiet (2-4 threads) aanbevolen, omdat een cached pool te veel threads kan maken.
ThreadPoolExecutor (TPE) — de volledige implementatie van ExecutorService met configureerbare parameters. In Android wordt TPE gebruikt binnen AsyncTask, IntentService en JobIntentService. De parameters corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue en RejectedExecutionHandler maken het mogelijk om het gedrag van de pool fijn af te stemmen. Aanbevelingen voor Android: corePoolSize = aantal CPU-kernen - 1 (voor IO-bound taken) of aantal kernen (voor CPU-bound taken). Voor typische apps — 2-4 threads.
// Kant-en-klare Executors-configuraties
// 1. Vaste pool met 3 threads
val fixedPool = Executors.newFixedThreadPool(3)
// 2. Gecachte pool (niet aanbevolen voor mobiel)
val cachedPool = Executors.newCachedThreadPool()
// 3. Enkele thread (serialisatie)
val singlePool = Executors.newSingleThreadExecutor()
// 4. Planner (periodieke taken)
val scheduler = Executors.newScheduledThreadPool(2)
// Gebruik met Callable en Future
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// Resultaat ophalen (blokkeert thread)
val result = future.get(5, TimeUnit.SECONDS)
// Pool afsluiten
fixedPool.shutdownNow()
Kotlin-coroutines bieden CoroutineDispatcher — een abstractie vergelijkbaar met een threadpool. Dispatchers.IO gebruikt een pool van 64 threads (beperkt). Dispatchers.Default — een pool gelijk aan het aantal CPU-kernen. CoroutineDispatcher vereist geen handmatige shutdown en wordt automatisch beheerd. Voor fijnafstemming maakt u uw eigen ExecutorCoroutineDispatcher via Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Coroutines vervangen de threadpool niet, maar omhullen deze.
iOS biedt twee hoofdmechanismen voor het beheren van de threadpool: OperationQueue (high-level API gebaseerd op GCD) en GCD DispatchQueue (low-level C-API). OperationQueue kapselt de threadpool in via de eigenschap maxConcurrentOperationCount. Standaard gebruikt OperationQueue system-defined maximum (afhankelijk van de systeembelasting). DispatchQueue.global() biedt een gelijktijdige wachtrij met de systeem-threadpool.
OperationQueue beheert de threadpool via maxConcurrentOperationCount. Waarde 1 creëert een seriële wachtrij (vergelijkbaar met single thread pool). Waarde groter dan 1 — een gelijktijdige pool met de opgegeven limiet. Standaard is maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (systeemoptimum, meestal 4-8 threads). Operation ondersteunt afhankelijkheden, prioriteiten en annulering. Elke bewerking wordt uitgevoerd op een vrije thread uit de systeempool.
// OperationQueue met pool van 3 threads
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// Operaties aanmaken
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) }
}
// Afhankelijkheid: operation2 wacht op operation1
operation2.addDependency(operation1)
// Toevoegen aan wachtrij
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// Alle operaties annuleren
queue.cancelAllOperations()
DispatchQueue — de threadpool van Apple. Een gelijktijdige wachtrij (qos: .utility) gebruikt de systeem-threadpool, geoptimaliseerd voor de huidige belasting van het apparaat. Verschillende QoS (userInteractive, userInitiated, utility, background) worden toegewezen aan verschillende pools met verschillende prioriteiten. DispatchGroup maakt synchronisatie van meerdere taken mogelijk. DispatchWorkItem ondersteunt annulering en qualityOfService. Voor fijne controle maakt u uw eigen gelijktijdige wachtrijen via DispatchQueue(label: qos: attributes: .concurrent).
// GCD DispatchQueue als threadpool
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// Taken naar de pool sturen
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup voor synchronisatie
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 taken voltooid
}
// Concurrency beperken via semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
De configuratie van de threadpool heeft directe invloed op de prestaties van de app. Onjuiste parameters leiden tot onderbenutting van de CPU (te weinig threads) of overbelasting van het systeem (te veel threads). In mobiele apps verschillen de optimale waarden van serverwaarden vanwege beperkte resources en energieverbruik. Belangrijkste parameters: corePoolSize, maxPoolSize, queue capacity en keepAliveTime.
Formule voor IO-bound taken: corePoolSize = aantal CPU-kernen × 2 (threads wachten op invoer/uitvoer). Voor CPU-bound taken: corePoolSize = aantal CPU-kernen (threads zijn constant bezig met berekeningen). Op moderne mobiele apparaten (6-8 kernen) levert dit 6-8 threads voor CPU-bound en 12-16 voor IO-bound. Praktijktests tonen aan dat voor een typische mobiele app 3-4 threads optimaal zijn — meer threads verhogen het energieverbruik zonder prestatieverbetering.
De grootte van de takenwachtrij (work queue) bepaalt hoeveel taken kunnen wachten op uitvoering. Onbeperkte wachtrij (LinkedBlockingQueue zonder limiet) kan leiden tot OOM bij snelle taaktoestroom. Beperkte wachtrij (ArrayBlockingQueue met vaste grootte) weigert taken bij overloop. Voor mobiele apps wordt ArrayBlockingQueue met een capaciteit van 16-32 taken aanbevolen. CallerRunsPolicy — de beste RejectedExecutionHandler voor mobiele apparaten: vertraagt de afzender (pressure back) in plaats van de taak te verliezen.
| Parameter | Aanbeveling voor mobiel | Motivering |
|---|---|---|
| corePoolSize | 2-4 | Beperkte resources van het mobiele apparaat |
| maxPoolSize | corePoolSize (of +1-2) | Piekbelasting bij threadcreatie vermijden |
| keepAliveTime | 15-30 seconden | Snel geheugen vrijmaken, maar zonder frequent maken |
| Queue capacity | 16-32 | Balans tussen buffering en OOM-risico |
| Handler | CallerRunsPolicy | Backpressure zonder taakverlies |
Mobiele app-ontwikkelaars maken vaak fouten bij het gebruik van de threadpool, wat leidt tot crashes, geheugenlekken en instabiele werking. Meest voorkomend: het niet aanroepen van shutdown() voor ExecutorService, het maken van een nieuwe pool voor elke bewerking, een te grote pool, deadlock tussen taken, gebruik van CachedThreadPool op Android.
Deadlock treedt op wanneer een taak in de pool wacht op het resultaat van een andere taak uit dezelfde pool, maar alle threads bezet zijn met wachten. Voorbeeld: taak A stuurt taak B naar dezelfde pool en roept future.get() aan — als de pool uitgeput is, wacht taak A op taak B en kan taak B niet worden uitgevoerd omdat er geen vrije threads zijn. Oplossing: gebruik aparte pools voor verschillende taakniveaus of async callback in plaats van blokkerende .get().
// Deadlock in Thread Pool
val pool = Executors.newFixedThreadPool(1)
// Taak A wacht op taak B — deadlock!
val futureA = pool.submit {
// Deze taak wordt nooit uitgevoerd
val futureB = pool.submit { 42 }
futureB.get() // Voor altijd geblokkeerd
}
// Oplossing: aparte pools
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// Wordt in aparte pool uitgevoerd — deadlock onmogelijk
}
}
// Of gebruik CompletableFuture
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
ExecutorService gemaakt in Activity moet worden beëindigd in onDestroy(). Als dit niet gebeurt, blijven threads in het geheugen hangen, zelfs na vernietiging van de Activity. Oplossing: bewaar de pool in Application scope of ViewModel, niet in Activity. Gebruik voor coroutines viewModelScope of lifecycleScope. Als de pool binnen Activity is gemaakt, roep dan zeker pool.shutdown() aan in onDestroy(). Gebruik voor testen es.shutdownNow() voor onmiddellijke stopzetting.
Veelgestelde vragen
Thread Pool hergebruikt reeds gemaakte threads voor het uitvoeren van meerdere taken. Een gewone thread (raw Thread) wordt gemaakt, voert één taak uit en wordt vernietigd. Het maken van een thread kost ongeveer 1 MB geheugen en ~1 ms tijd. Thread Pool vermindert overhead, beperkt het maximale aantal threads en biedt een beheer-API (shutdown, awaitTermination).
Voor een typische mobiele app zijn 2-4 threads optimaal. Voor CPU-bound taken — het aantal CPU-kernen. Voor IO-bound taken — het aantal kernen × 2. Meer threads verhogen het energieverbruik en contextwisselingen zonder prestatieverbetering. Gebruik op Android Process.availableProcessors() om kernen te bepalen. Op iOS — ProcessInfo.processInfo.processorCount.
CachedThreadPool maakt threads indien nodig en hergebruikt bestaande threads. Probleem: het beperkt het maximale aantal threads niet. Als 100 taken tegelijk binnenkomen, worden 100 threads gemaakt. Dit leidt tot OOM op Android (elke thread ~1 MB). Gebruik newFixedThreadPool(n) met een expliciete limiet. CachedThreadPool is alleen toegestaan voor kortdurende burst-taken met garantie van een kleine omvang.
Ja, als de pool niet toebehoort aan een beheerde container (zoals coroutines). shutdown() stopt het accepteren van nieuwe taken en beëindigt threads na het voltooien van huidige taken. Zonder shutdown() blijven threads in het geheugen hangen en wordt de app niet afgesloten. Roep aan in onDestroy() voor Activity. Gebruik voor ViewModel coroutineScope. Het beëindigen van de pool is een verplicht onderdeel van resourcebeheer, analoog aan het sluiten van een Cursor of InputStream.
Ja, OperationQueue en DispatchQueue zijn de threadpool die door iOS wordt geleverd. OperationQueue beperkt gelijktijdigheid via maxConcurrentOperationCount. DispatchQueue.global() gebruikt de systeem-threadpool zonder directe controle. In tegenstelling tot Java ThreadPoolExecutor beheert u geen corePoolSize of queue capacity — het systeem optimaliseert de pool automatisch onder de huidige belasting en het energieverbruik van het apparaat.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook