Thread Pool — adalah mekanisme manajemen utas di mana kumpulan utas yang dibuat sebelumnya digunakan kembali untuk menjalankan tugas, menghindari biaya overhead pembuatan dan penghancuran utas. Dalam pengembangan mobile, kumpulan utas digunakan untuk operasi latar belakang: permintaan jaringan, pemrosesan gambar, bekerja dengan basis data. Menurut Google Android Documentation (2025), ExecutorService adalah cara yang direkomendasikan untuk mengelola utas latar belakang di Android. Di iOS, peran serupa dilakukan oleh OperationQueue dan GCD DispatchQueue dengan antrean konkuren global.
Poin utama
Thread Pool (kumpulan utas) — adalah pola arsitektur di mana sejumlah tetap utas dibuat sebelumnya dan digunakan kembali untuk menjalankan beberapa tugas. Alih-alih membuat utas baru untuk setiap operasi (yang mahal: sekitar 1 MB memori stack per utas di JVM), tugas ditempatkan dalam antrean dan dijalankan oleh utas kosong dari pool. Dalam pengembangan mobile, kumpulan utas sangat penting untuk kinerja — Android dan iOS membatasi jumlah utas per aplikasi.
Membuat utas adalah operasi yang mahal: alokasi stack, pendaftaran di sistem, peralihan konteks. Pada perangkat mobile dengan sumber daya terbatas, pembuatan utas yang tidak terkendali menyebabkan OOM (OutOfMemoryError) di Android dan throttling di iOS. Thread Pool memecahkan kedua masalah: membatasi jumlah maksimum utas yang berjalan bersamaan dan menggunakan kembali utas yang sudah dibuat. Google merekomendasikan ExecutorService daripada raw Thread(), Apple merekomendasikan OperationQueue daripada Thread.
| Parameter | Tanpa pool (raw Thread) | Dengan Thread Pool |
|---|---|---|
| Pembuatan utas | Untuk setiap tugas | Satu kali saat pembuatan pool |
| Maksimum utas | Tidak terbatas (risiko OOM) | Dibatasi oleh core/max pool size |
| Pemanfaatan | Rendah (utas mati setelah tugas) | Tinggi (utas digunakan kembali) |
| Manajemen | Manual (join, interrupt) | Otomatis (ExecutorService) |
| Konsumsi memori | Meningkat dengan setiap tugas | Tetap |
Kumpulan utas bekerja berdasarkan prinsip Producer-Consumer: tugas (Runnable/Callable) ditempatkan dalam antrean pemblokiran (BlockingQueue). Utas dari pool menunggu tugas dalam antrean dan mengambilnya untuk dieksekusi. Algoritma: jika utas kosong lebih sedikit dari corePoolSize, utas baru dibuat. Jika corePoolSize tercapai, tugas ditempatkan dalam antrean. Jika antrean penuh dan utas lebih sedikit dari maximumPoolSize, utas tambahan dibuat. Saat maximumPoolSize terlampaui, tugas ditolak melalui RejectedExecutionHandler.
Core pool size — jumlah utas yang dipertahankan dalam pool bahkan dalam keadaan diam. Maximum pool size — jumlah maksimum utas yang dapat dibuat saat antrean meluap. Perbedaan di antara keduanya adalah utas tambahan (overflow) yang dibuat sementara dan diakhiri setelah batas waktu idle. Pada perangkat mobile, disarankan untuk menetapkan corePoolSize sama dengan maximumPoolSize untuk menghindari beban puncak pembuatan utas.
BlockingQueue menyimpan tugas yang menunggu eksekusi. Implementasi paling populer: LinkedBlockingQueue (tak terbatas), ArrayBlockingQueue (terbatas) dan SynchronousQueue (tanpa penyimpanan — tugas langsung diteruskan ke utas). Saat antrean dan pool penuh, RejectedExecutionHandler diaktifkan. Kebijakan standar: AbortPolicy (melempar RejectedExecutionException), CallerRunsPolicy (mengeksekusi di utas pengirim), DiscardPolicy dan DiscardOldestPolicy.
// Membuat Thread Pool di Android
val threadPool = ThreadPoolExecutor(
corePoolSize = 2, // Minimal 2 utas
maximumPoolSize = 4, // Maksimal 4 utas
keepAliveTime = 30L, // Masa pakai utas overflow
unit = TimeUnit.SECONDS,
workQueue = LinkedBlockingQueue<Runnable>(16),
threadFactory = Executors.defaultThreadFactory(),
handler = ThreadPoolExecutor.CallerRunsPolicy()
)
// Mengirim tugas
threadPool.execute {
val result = api.fetchData()
runOnUiThread { showData(result) }
}
// Mengakhiri pool
threadPool.shutdown()
// Menunggu penyelesaian semua tugas
threadPool.awaitTermination(10, TimeUnit.SECONDS)
Android menyediakan beberapa implementasi kumpulan utas melalui java.util.concurrent. Executors — pabrik dengan konfigurasi siap pakai: newFixedThreadPool(n) (pool tetap), newCachedThreadPool() (tak terbatas, utas dibuat sesuai kebutuhan), newSingleThreadExecutor() (satu utas — eksekusi berurutan). Untuk proyek mobile, newFixedThreadPool dengan batas wajar (2-4 utas) direkomendasikan, karena cached pool dapat membuat terlalu banyak utas.
ThreadPoolExecutor (TPE) — implementasi lengkap ExecutorService dengan parameter yang dapat dikonfigurasi. Di Android, TPE digunakan di dalam AsyncTask, IntentService dan JobIntentService. Parameter corePoolSize, maximumPoolSize, keepAliveTime, BlockingQueue dan RejectedExecutionHandler memungkinkan penyesuaian perilaku pool secara detail. Rekomendasi untuk Android: corePoolSize = jumlah inti CPU - 1 (untuk tugas IO-bound) atau jumlah inti (untuk tugas CPU-bound). Untuk aplikasi tipikal — 2-4 utas.
// Konfigurasi Executors siap pakai
// 1. Pool tetap untuk 3 utas
val fixedPool = Executors.newFixedThreadPool(3)
// 2. Pool cache (tidak disarankan untuk mobile)
val cachedPool = Executors.newCachedThreadPool()
// 3. Utas tunggal (serialisasi)
val singlePool = Executors.newSingleThreadExecutor()
// 4. Penjadwal (tugas periodik)
val scheduler = Executors.newScheduledThreadPool(2)
// Penggunaan dengan Callable dan Future
val future: Future<String> = fixedPool.submit(Callable {
"Result: ${api.call()}"
})
// Mendapatkan hasil (memblokir utas)
val result = future.get(5, TimeUnit.SECONDS)
// Mengakhiri pool
fixedPool.shutdownNow()
Kotlin coroutine menyediakan CoroutineDispatcher — abstraksi yang mirip dengan kumpulan utas. Dispatchers.IO menggunakan pool 64 utas (terbatas). Dispatchers.Default — pool sama dengan jumlah inti CPU. CoroutineDispatcher tidak memerlukan shutdown manual dan dikelola secara otomatis. Untuk penyesuaian yang detail, buat ExecutorCoroutineDispatcher Anda sendiri melalui Executors.newFixedThreadPool(2).asCoroutineDispatcher(). Coroutine tidak menggantikan kumpulan utas, tetapi membungkusnya.
iOS menyediakan dua mekanisme utama untuk mengelola kumpulan utas: OperationQueue (API tingkat tinggi berbasis GCD) dan GCD DispatchQueue (API tingkat rendah C). OperationQueue merangkum kumpulan utas melalui properti maxConcurrentOperationCount. Secara default, OperationQueue menggunakan system-defined maximum (tergantung pada beban sistem). DispatchQueue.global() menyediakan antrean konkuren dengan kumpulan utas sistem.
OperationQueue mengelola kumpulan utas melalui maxConcurrentOperationCount. Nilai 1 membuat antrean serial (mirip dengan single thread pool). Nilai lebih dari 1 — pool konkuren dengan batas yang ditentukan. Secara default, maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount (optimum sistem, biasanya 4-8 utas). Operation mendukung dependensi, prioritas dan pembatalan. Setiap operasi dijalankan pada utas kosong mana pun dari pool sistem.
// OperationQueue dengan pool 3 utas
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility
// Membuat operasi
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) }
}
// Ketergantungan: operation2 menunggu operation1
operation2.addDependency(operation1)
// Menambahkan ke antrian
queue.addOperations([operation1, operation2], waitUntilFinished: false)
// Membatalkan semua operasi
queue.cancelAllOperations()
DispatchQueue — adalah kumpulan utas dari Apple. Antrean konkuren (qos: .utility) menggunakan kumpulan utas sistem, yang dioptimalkan untuk beban perangkat saat ini. QoS yang berbeda (userInteractive, userInitiated, utility, background) dipetakan ke pool yang berbeda dengan prioritas yang berbeda. DispatchGroup memungkinkan sinkronisasi beberapa tugas. DispatchWorkItem mendukung pembatalan dan qualityOfService. Untuk kontrol yang detail, buat antrean konkuren Anda sendiri melalui DispatchQueue(label: qos: attributes: .concurrent).
// GCD DispatchQueue sebagai kumpulan utas
let customQueue = DispatchQueue(
label: "com.app.background",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .workItem
)
// Mengirim tugas ke pool
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }
// DispatchGroup untuk sinkronisasi
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() // Kedua tugas selesai
}
// Membatasi konkurensi melalui semaphore
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
pool.async {
semaphore.wait()
download(url)
semaphore.signal()
}
}
Konfigurasi kumpulan utas secara langsung memengaruhi kinerja aplikasi. Parameter yang salah menyebabkan kurangnya pemanfaatan CPU (terlalu sedikit utas) atau kelebihan beban sistem (terlalu banyak). Dalam aplikasi mobile, nilai optimal berbeda dari nilai server karena sumber daya yang terbatas dan konsumsi daya. Parameter utama: corePoolSize, maxPoolSize, queue capacity dan keepAliveTime.
Rumus untuk tugas IO-bound: corePoolSize = jumlah inti CPU × 2 (utas menunggu input-output). Untuk tugas CPU-bound: corePoolSize = jumlah inti CPU (utas terus-menerus sibuk dengan perhitungan). Pada perangkat mobile modern (6-8 inti), ini menghasilkan 6-8 utas untuk CPU-bound dan 12-16 untuk IO-bound. Tes praktis menunjukkan bahwa untuk aplikasi mobile tipikal, 3-4 utas adalah optimal — lebih banyak utas meningkatkan konsumsi daya tanpa peningkatan kinerja.
Ukuran antrean tugas (work queue) menentukan berapa banyak tugas yang dapat menunggu eksekusi. Antrean tak terbatas (LinkedBlockingQueue tanpa batas) dapat menyebabkan OOM saat tugas datang dengan cepat. Antrean terbatas (ArrayBlockingQueue dengan ukuran tetap) menolak tugas saat meluap. Untuk aplikasi mobile, ArrayBlockingQueue dengan kapasitas 16-32 tugas direkomendasikan. CallerRunsPolicy — RejectedExecutionHandler terbaik untuk perangkat mobile: memperlambat pengirim (pressure back) daripada kehilangan tugas.
| Parameter | Rekomendasi untuk mobile | Alasan |
|---|---|---|
| corePoolSize | 2-4 | Sumber daya terbatas perangkat mobile |
| maxPoolSize | corePoolSize (atau +1-2) | Menghindari beban puncak pembuatan utas |
| keepAliveTime | 15-30 detik | Pembebasan memori cepat, tanpa sering membuat |
| Queue capacity | 16-32 | Keseimbangan antara buffering dan risiko OOM |
| Handler | CallerRunsPolicy | Tekanan balik tanpa kehilangan tugas |
Pengembang aplikasi mobile sering membuat kesalahan saat menggunakan kumpulan utas, yang menyebabkan crash, kebocoran memori dan operasi yang tidak stabil. Paling umum: tidak memanggil shutdown() untuk ExecutorService, membuat pool baru untuk setiap operasi, pool terlalu besar, deadlock antar tugas, menggunakan CachedThreadPool di Android.
Deadlock terjadi ketika suatu tugas di pool menunggu hasil tugas lain dari pool yang sama, tetapi semua utas sibuk menunggu. Contoh: tugas A mengirim tugas B ke pool yang sama dan memanggil future.get() — jika pool habis, tugas A menunggu tugas B, dan tugas B tidak dapat dijalankan karena tidak ada utas kosong. Solusi: gunakan pool terpisah untuk tingkat tugas yang berbeda atau callback asinkron daripada .get() yang memblokir.
// Deadlock di Thread Pool
val pool = Executors.newFixedThreadPool(1)
// Tugas A menunggu tugas B — deadlock!
val futureA = pool.submit {
// Tugas ini tidak akan pernah dijalankan
val futureB = pool.submit { 42 }
futureB.get() // Diblokir selamanya
}
// Perbaikan: pool terpisah
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()
workerPool.submit {
callbackPool.submit {
// Dijalankan di pool terpisah — deadlock tidak mungkin
}
}
// Atau gunakan CompletableFuture
workerPool.submit {
CompletableFuture
.supplyAsync { 42 }
.thenAccept { result ->
println(result)
}
}
ExecutorService yang dibuat di Activity harus diakhiri di onDestroy(). Jika tidak dilakukan, utas akan tetap berada di memori bahkan setelah Activity dihancurkan. Solusi: simpan pool dalam lingkup Application atau ViewModel, bukan di Activity. Untuk coroutine, gunakan viewModelScope atau lifecycleScope. Jika pool dibuat di dalam Activity, pastikan untuk memanggil pool.shutdown() di onDestroy(). Untuk pengujian, gunakan es.shutdownNow() untuk penghentian segera.
Pertanyaan yang sering diajukan
Thread Pool menggunakan kembali utas yang sudah dibuat untuk menjalankan beberapa tugas. Utas biasa (raw Thread) dibuat, menjalankan satu tugas dan dihancurkan. Membuat utas membutuhkan sekitar 1 MB memori dan ~1 ms waktu. Thread Pool mengurangi biaya overhead, membatasi jumlah maksimum utas dan menyediakan API manajemen (shutdown, awaitTermination).
Untuk aplikasi mobile tipikal, 2-4 utas adalah optimal. Untuk tugas CPU-bound — jumlah inti CPU. Untuk tugas IO-bound — jumlah inti × 2. Lebih banyak utas meningkatkan konsumsi daya dan peralihan konteks tanpa peningkatan kinerja. Di Android, gunakan Process.availableProcessors() untuk menentukan inti. Di iOS — ProcessInfo.processInfo.processorCount.
CachedThreadPool membuat utas sesuai kebutuhan dan menggunakan kembali utas yang ada. Masalah: tidak membatasi jumlah maksimum utas. Jika 100 tugas datang bersamaan, 100 utas akan dibuat. Ini menyebabkan OOM di Android (setiap utas ~1 MB). Gunakan newFixedThreadPool(n) dengan batas eksplisit. CachedThreadPool hanya diizinkan untuk tugas burst jangka pendek dengan jaminan volume kecil.
Ya, jika pool bukan milik wadah terkelola (seperti coroutine). shutdown() menghentikan penerimaan tugas baru dan mengakhiri utas setelah menyelesaikan tugas saat ini. Tanpa shutdown(), utas tetap di memori dan aplikasi tidak berhenti. Untuk Activity, panggil di onDestroy(). Untuk ViewModel, gunakan coroutineScope. Mengakhiri pool adalah bagian wajib dari manajemen sumber daya, mirip dengan menutup Cursor atau InputStream.
Ya, OperationQueue dan DispatchQueue adalah kumpulan utas yang disediakan oleh iOS. OperationQueue membatasi konkuransi melalui maxConcurrentOperationCount. DispatchQueue.global() menggunakan kumpulan utas sistem tanpa kontrol langsung. Tidak seperti Java ThreadPoolExecutor, Anda tidak mengelola corePoolSize atau queue capacity — sistem mengoptimalkan pool secara otomatis di bawah beban saat ini dan konsumsi daya perangkat.
Ringkasan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga