Menambah thread tidak sama dengan menambah kecepatan — pada titik tertentu, thread baru hanya menambah antrean di lock, pergantian konteks, dan tekanan memori. Performa concurrency punya tiga langit-langit yang bisa kita hitung: porsi pekerjaan yang memang serial, bagian kritis yang memaksa antrean, dan jejak memori tiap thread. Chapter ini menghitung ketiganya dengan angka tetap, menyetel ukuran thread pool dengan rumus rasio tunggu-hitung, lalu melihat bagaimana virtual threads dari JEP 444 mengubah perhitungan untuk beban I/O.
Batas Kecepatan dari Bagian Serial #
Hukum Amdahl menyatakan speedup maksimum paralelisasi: dengan porsi paralel p dan N prosesor, speedup = 1 / ((1 - p) + p / N). Angka dari program di bawah menunjukkan mengapa optimasi harus menyerang bagian serial: porsi paralel 90% pada 16 core hanya mencapai speedup 6,4 — bukan 16. Memperkecil bagian serial dari 10% ke 1% mengubah speedup 16 core dari 6,40 menjadi 13,91; hampir dua kali lipat hanya dengan menyerang sembilan persen pekerjaan.
Kontensi lock adalah serialisasi yang paling sering tidak disadari. Model sederhana: tugas 100 unit kerja, c unit di antaranya berada di dalam bagian kritis. Dengan 8 worker, waktu total = 8 × c (antrean lock) + (100 - c) (sisa paralel) — persis Amdahl dengan s = c/100, tetapi kali ini penyebabnya bisa kita lihat dan perbaiki: ConcurrentHashMap menggantikan Collections.synchronizedMap, operasi dikeluarkan dari lock, LongAdder menggantikan AtomicLong yang diperebutkan.
Menyetel Ukuran Thread Pool #
Untuk beban campuran tunggu-hitung, rumus klasik thread = core × (1 + W/C) menyeimbangkan core yang menganggur saat thread menunggu. Rasio W/C diperoleh dari profil: waktu tunggu dibagi waktu hitung. Rumus ini bukan angka suci — thread lebih banyak menaikkan antrean dan memori, thread lebih sedikit meninggalkan core kosong — tetapi memberi titik mulai yang bisa diukur. Prinsip-prinsip dasarnya dibahas pada Chapter Thread Pools di section Concurrency dan Multithreading.
Virtual Threads untuk Beban I/O #
JEP 444 membawa virtual threads ke Java final sejak JDK 21: thread ringan yang dikelola JVM, dipetakan ke sedikit carrier thread sistem operasi, dan “turun” (unmount) dari carrier saat memblokir I/O. Model memorinya berubah total: 10.000 platform thread memesan sekitar 10 GB ruang alamat untuk stack, sedangkan 10.000 virtual thread hidup dalam puluhan megabyte di heap. Penjadwalan kerjanya pun berbeda:
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
/**
* Kalkulator sekaligus simulasi performa concurrency berbasis angka tetap:
* hukum Amdahl, model kontensi lock, rumus ukuran thread pool, model memori
* platform vs virtual thread, dan simulasi penjadwalan round-robin
* berpenghitung langkah. Seluruh keluaran deterministik.
*/
public class SkalaConcurrency {
public static void main(String[] args) {
// ---- 1. Hukum Amdahl: speedup maksimum ----
System.out.println("== 1. Hukum Amdahl: speedup maksimum ==");
System.out.printf("%10s %6s %6s %6s %6s %6s%n",
"p paralel", "N=1", "N=2", "N=4", "N=8", "N=16");
for (double p : new double[] {0.50, 0.90, 0.95, 0.99}) {
System.out.printf("%10.2f", p);
for (int n : new int[] {1, 2, 4, 8, 16}) {
double speedup = 1.0 / ((1.0 - p) + p / n);
System.out.printf(" %6.2f", speedup);
}
System.out.println();
}
System.out.println();
// ---- 2. Kontensi lock: bagian kritis memaksa antrean ----
System.out.println("== 2. Model kontensi lock (8 core, tugas 100 unit) ==");
System.out.printf("%12s %12s %12s%n", "unit kritis", "speedup", "efisiensi");
for (int c : new int[] {5, 10, 20}) {
int n = 8;
int w = 100;
double waktu = (double) (n * c + (w - c)); // antrean serial + sisa paralel
double speedup = (double) (n * w) / waktu;
System.out.printf("%12d %12.2f %11.0f%%%n", c, speedup, 100.0 * speedup / n);
}
System.out.println();
// ---- 3. Ukuran thread pool menurut rasio tunggu/hitung ----
System.out.println("== 3. Rumus pool: thread = core x (1 + W/C), 8 core ==");
System.out.printf("%12s %12s%n", "W/C", "thread");
for (double rasio : new double[] {0.5, 1, 2, 8}) {
int thread = (int) Math.round(8 * (1 + rasio));
System.out.printf("%12.1f %12d%n", rasio, thread);
}
System.out.println();
// ---- 4. Jejak memori platform vs virtual (model) ----
System.out.println("== 4. Model memori untuk 10.000 thread ==");
System.out.println("platform : 10.000 x 1 MB stack = 10 GB ruang alamat dipesan");
System.out.println("virtual : 10.000 x ~2 KB = sekitar 20 MB di heap");
System.out.println();
// ---- 5. Simulasi penjadwalan virtual thread round-robin ----
System.out.println("== 5. Simulasi penjadwalan (12 tugas x 8 langkah, 4 carrier) ==");
Deque<Integer> antrean = new ArrayDeque<>(List.of(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
int[] langkahPerCarrier = new int[4];
int totalLangkah = 0;
while (!antrean.isEmpty()) {
for (int carrier = 0; carrier < 4 && !antrean.isEmpty(); carrier++) {
int tugas = antrean.poll();
for (int langkah = 0; langkah < 8; langkah++) {
langkahPerCarrier[carrier]++;
totalLangkah++;
}
}
}
for (int carrier = 0; carrier < 4; carrier++) {
System.out.println("carrier-" + carrier + " : " + langkahPerCarrier[carrier] + " langkah");
}
System.out.println("total " + totalLangkah + " langkah, seimbang, tanpa langkah menganggur");
System.out.println();
System.out.println("Kesimpulan: tambah thread hanya menolong beban I/O;");
System.out.println("bagian serial dan bagian kritis menutup ruang speedup.");
}
}Output yang Dihasilkan:
== 1. Hukum Amdahl: speedup maksimum ==
p paralel N=1 N=2 N=4 N=8 N=16
0.50 1.00 1.33 1.60 1.78 1.88
0.90 1.00 1.82 3.08 4.71 6.40
0.95 1.00 1.90 3.48 5.93 9.14
0.99 1.00 1.98 3.88 7.48 13.91
== 2. Model kontensi lock (8 core, tugas 100 unit) ==
unit kritis speedup efisiensi
5 5.93 74%
10 4.71 59%
20 3.33 42%
== 3. Rumus pool: thread = core x (1 + W/C), 8 core ==
W/C thread
0.5 12
1.0 16
2.0 24
8.0 72
== 4. Model memori untuk 10.000 thread ==
platform : 10.000 x 1 MB stack = 10 GB ruang alamat dipesan
virtual : 10.000 x ~2 KB = sekitar 20 MB di heap
== 5. Simulasi penjadwalan (12 tugas x 8 langkah, 4 carrier) ==
carrier-0 : 24 langkah
carrier-1 : 24 langkah
carrier-2 : 24 langkah
carrier-3 : 24 langkah
total 96 langkah, seimbang, tanpa langkah menganggur
Kesimpulan: tambah thread hanya menolong beban I/O;
bagian serial dan bagian kritis menutup ruang speedup.Mari kita telaah bagian-bagian kuncinya:
- Baris 14-26 (tabel Amdahl): empat porsi paralel dikalikan lima jumlah core. Perhatikan baris
0.90: menambah core dari 8 ke 16 hanya menaikkan speedup dari 4,71 ke 6,40 — penggandaan core dengan imbalan 36% kecepatan. - Baris 28-37 (model kontensi): pada 8 core, menaikkan bagian kritis dari 5 ke 20 unit menurunkan efisiensi dari 74% ke 42%. Lock yang diperebutkan bukan sekadar pelambat — lock tersebut membatalkan paralelisme yang sudah dibayar mahal.
- Baris 49-52 (model memori): perbandingan ruang alamat itulah alasan server dengan puluhan ribu koneksi serentak bermigrasi ke virtual threads — bukan demi throughput per request, melainkan demi kapasitas konkurensi.
- Baris 55-73 (simulasi penjadwalan): dua belas tugas virtual berjalan pada empat carrier secara round-robin; penghitung langkah membuktikan 96 langkah terbagi rata 24-24-24-24 tanpa langkah menganggur. Virtual thread bukan mesin lebih cepat — hanya mesin yang tidak membayar stack besar dan pergantian konteks OS saat menunggu I/O.
| Aspek | Platform thread | Virtual thread |
|---|---|---|
| Biaya pembuatan | mahal (syscall OS) | murah (objek di heap) |
| Stack | ~1 MB dipesan di muka | tumbuh sesuai kebutuhan |
| Memblokir I/O | menahan thread OS | unmount dari carrier |
| Cocok untuk | beban CPU jangka panjang | ribuan task I/O serentak |
Tiga langit-langit tadi — serial, kontensi, memori — menentukan kapasitas mesin kita. Namun kapasitas bukan satu-satunya ukuran yang dirasakan pengguna; dua aplikasi berthroughput sama bisa terasa sangat berbeda. Chapter berikutnya memisahkan dua metrik yang sering dicampuradukkan: latency dan throughput.
Referensi #
- JEP 444: Virtual Threads
- Oracle: Class Thread (Java SE 25)
- Oracle: Interface ExecutorService (Java SE 25)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
SkalaConcurrency.java
Unduh berkasimport java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
/**
* Kalkulator sekaligus simulasi performa concurrency berbasis angka tetap:
* hukum Amdahl, model kontensi lock, rumus ukuran thread pool, model memori
* platform vs virtual thread, dan simulasi penjadwalan round-robin
* berpenghitung langkah. Seluruh keluaran deterministik.
*/
public class SkalaConcurrency {
public static void main(String[] args) {
// ---- 1. Hukum Amdahl: speedup maksimum ----
System.out.println("== 1. Hukum Amdahl: speedup maksimum ==");
System.out.printf("%10s %6s %6s %6s %6s %6s%n",
"p paralel", "N=1", "N=2", "N=4", "N=8", "N=16");
for (double p : new double[] {0.50, 0.90, 0.95, 0.99}) {
System.out.printf("%10.2f", p);
for (int n : new int[] {1, 2, 4, 8, 16}) {
double speedup = 1.0 / ((1.0 - p) + p / n);
System.out.printf(" %6.2f", speedup);
}
System.out.println();
}
System.out.println();
// ---- 2. Kontensi lock: bagian kritis memaksa antrean ----
System.out.println("== 2. Model kontensi lock (8 core, tugas 100 unit) ==");
System.out.printf("%12s %12s %12s%n", "unit kritis", "speedup", "efisiensi");
for (int c : new int[] {5, 10, 20}) {
int n = 8;
int w = 100;
double waktu = (double) (n * c + (w - c)); // antrean serial + sisa paralel
double speedup = (double) (n * w) / waktu;
System.out.printf("%12d %12.2f %11.0f%%%n", c, speedup, 100.0 * speedup / n);
}
System.out.println();
// ---- 3. Ukuran thread pool menurut rasio tunggu/hitung ----
System.out.println("== 3. Rumus pool: thread = core x (1 + W/C), 8 core ==");
System.out.printf("%12s %12s%n", "W/C", "thread");
for (double rasio : new double[] {0.5, 1, 2, 8}) {
int thread = (int) Math.round(8 * (1 + rasio));
System.out.printf("%12.1f %12d%n", rasio, thread);
}
System.out.println();
// ---- 4. Jejak memori platform vs virtual (model) ----
System.out.println("== 4. Model memori untuk 10.000 thread ==");
System.out.println("platform : 10.000 x 1 MB stack = 10 GB ruang alamat dipesan");
System.out.println("virtual : 10.000 x ~2 KB = sekitar 20 MB di heap");
System.out.println();
// ---- 5. Simulasi penjadwalan virtual thread round-robin ----
System.out.println("== 5. Simulasi penjadwalan (12 tugas x 8 langkah, 4 carrier) ==");
Deque<Integer> antrean = new ArrayDeque<>(List.of(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
int[] langkahPerCarrier = new int[4];
int totalLangkah = 0;
while (!antrean.isEmpty()) {
for (int carrier = 0; carrier < 4 && !antrean.isEmpty(); carrier++) {
int tugas = antrean.poll();
for (int langkah = 0; langkah < 8; langkah++) {
langkahPerCarrier[carrier]++;
totalLangkah++;
}
}
}
for (int carrier = 0; carrier < 4; carrier++) {
System.out.println("carrier-" + carrier + " : " + langkahPerCarrier[carrier] + " langkah");
}
System.out.println("total " + totalLangkah + " langkah, seimbang, tanpa langkah menganggur");
System.out.println();
System.out.println("Kesimpulan: tambah thread hanya menolong beban I/O;");
System.out.println("bagian serial dan bagian kritis menutup ruang speedup.");
}
}