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:

text
== 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
AspekPlatform threadVirtual thread
Biaya pembuatanmahal (syscall OS)murah (objek di heap)
Stack~1 MB dipesan di mukatumbuh sesuai kebutuhan
Memblokir I/Omenahan thread OSunmount dari carrier
Cocok untukbeban CPU jangka panjangribuan 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

SkalaConcurrency.java

Unduh berkas
java
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.");
    }
}