← Kursa Dön
📄 Text · 12 min

Thread Pool ve ExecutorService

Her iş için yeni thread oluşturmak, her yolculuk için yeni araba almak gibi. Pahalı, verimsiz ve yönetilmesi zor. Bunun yerine bir thread havuzu (pool) oluşturup, işleri bu havuzdaki thread'lere dağıtmak çok daha mantıklı.


Neden Thread Pool?

Doğrudan thread oluşturmanın sorunları:

  1. Thread oluşturma maliyetli — OS düzeyinde kaynak tahsisi gerekir

  2. Bellek tüketimi — her thread ~1MB stack bellek kullanır

  3. Sınırsız thread — 10.000 istek = 10.000 thread = OutOfMemoryError

  4. Yönetim zorluğu — her thread'i takip etmek, kapatmak, hata yönetimi

Thread pool bunları çözer:

// KÖTÜ — her iş için yeni thread
for (int i = 0; i < 1000; i++) {
    new Thread(() -> isYap()).start(); // 1000 thread!
}

// İYİ — thread pool
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    pool.submit(() -> isYap()); // 10 thread, 1000 iş
}
pool.shutdown();

10 thread, 1000 işi sırayla üstlenir. Bir thread işini bitirince kuyruktan yeni iş alır.


ExecutorService — Thread Pool Arayüzü

ExecutorService, thread pool'un ana arayüzüdür. Executors fabrika sınıfı ile oluşturulur.

Fixed Thread Pool

Sabit sayıda thread. En yaygın kullanılan.

ExecutorService pool = Executors.newFixedThreadPool(4);

for (int i = 1; i <= 10; i++) {
    final int isNo = i;
    pool.submit(() -> {
        String thread = Thread.currentThread().getName();
        System.out.println(thread + " → İş #" + isNo + " başladı");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(thread + " → İş #" + isNo + " bitti");
    });
}

pool.shutdown(); // Yeni iş kabul etme, mevcut işleri bitir

Çıktı (yaklaşık):

pool-1-thread-1 → İş #1 başladı
pool-1-thread-2 → İş #2 başladı
pool-1-thread-3 → İş #3 başladı
pool-1-thread-4 → İş #4 başladı
(1 saniye sonra)
pool-1-thread-1 → İş #1 bitti
pool-1-thread-1 → İş #5 başladı  ← Thread-1 yeni iş aldı
pool-1-thread-2 → İş #2 bitti
pool-1-thread-2 → İş #6 başladı  ← Thread-2 yeni iş aldı
...

4 thread, 10 işi sırayla halleder.

Cached Thread Pool

İhtiyaç oldukça thread oluşturur, boştakileri 60 saniye sonra kapatır.

ExecutorService pool = Executors.newCachedThreadPool();
// Kısa süreli, çok sayıda iş için uygun
// DİKKAT: Sınırsız thread oluşturabilir!

Single Thread Executor

Tek thread'li pool. İşler sırayla çalışır.

ExecutorService pool = Executors.newSingleThreadExecutor();
// Sıralı iş garantisi — FIFO

Scheduled Thread Pool

Zamanlı ve periyodik işler için.

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

// 3 saniye sonra çalıştır
scheduler.schedule(() -> {
    System.out.println("3 saniye sonra çalıştım!");
}, 3, TimeUnit.SECONDS);

// Her 2 saniyede bir çalıştır (1 saniye gecikme ile başla)
scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Periyodik iş: " + LocalTime.now());
}, 1, 2, TimeUnit.SECONDS);

// 5 saniye sonra scheduler'ı kapat
scheduler.schedule(() -> scheduler.shutdown(), 10, TimeUnit.SECONDS);

Pool Karşılaştırma Tablosu

Pool TipiThread SayısıKuyrukKullanım
newFixedThreadPool(n)Sabit nSınırsızGenel amaçlı
newCachedThreadPool()0 → ∞Yok (SynchronousQueue)Kısa, çok iş
newSingleThreadExecutor()1SınırsızSıralı işler
newScheduledThreadPool(n)Sabit nGecikmeli kuyrukZamanlı işler

submit() vs execute()

ExecutorService pool = Executors.newFixedThreadPool(4);

// execute — void, exception fırlatılırsa yutulur
pool.execute(() -> System.out.println("execute"));

// submit — Future döner, exception yakalanabilir
Future<?> future = pool.submit(() -> System.out.println("submit"));

Genel kural: `submit()` kullan. Hata yönetimi ve sonuç takibi daha kolay.


Future — Sonuç Bekleme

submit() bir Future nesnesi döner. Bu nesne ile:

  • Sonucu al (get())

  • İşin bitip bitmediğini kontrol et (isDone())

  • İşi iptal et (cancel())

ExecutorService pool = Executors.newFixedThreadPool(2);

Future<Integer> future = pool.submit(() -> {
    Thread.sleep(2000); // 2 saniye süren iş
    return 42;
});

System.out.println("İş devam ediyor...");
System.out.println("Bitti mi? " + future.isDone()); // false

// get() — sonuç gelene kadar bekler (blocking)
Integer sonuc = future.get();
System.out.println("Sonuç: " + sonuc); // 42
System.out.println("Bitti mi? " + future.isDone()); // true

pool.shutdown();

Zaman Aşımlı get()

try {
    Integer sonuc = future.get(3, TimeUnit.SECONDS); // Max 3 saniye bekle
} catch (TimeoutException e) {
    System.out.println("Zaman aşımı! İş çok uzun sürüyor.");
    future.cancel(true); // İşi iptal et
}

İş İptal Etme

Future<?> future = pool.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Uzun süren iş
    }
});

// İptal et
boolean iptalEdildi = future.cancel(true); // true = interrupt gönder
System.out.println("İptal: " + iptalEdildi);
System.out.println("İptal edildi mi: " + future.isCancelled());

Callable — Sonuç Dönen Task

Runnable'ın run() metodu void döner ve checked exception fırlatamaz. Callable bu kısıtlamaları aşar:

// Runnable — void, exception yok
Runnable r = () -> System.out.println("Merhaba");

// Callable — değer döner, exception fırlatabilir
Callable<Integer> c = () -> {
    Thread.sleep(1000);
    return 42;
};
ExecutorService pool = Executors.newFixedThreadPool(3);

Callable<String> gorev1 = () -> {
    Thread.sleep(1000);
    return "Görev 1 tamamlandı";
};

Callable<String> gorev2 = () -> {
    Thread.sleep(2000);
    return "Görev 2 tamamlandı";
};

Callable<String> gorev3 = () -> {
    Thread.sleep(1500);
    return "Görev 3 tamamlandı";
};

Future<String> f1 = pool.submit(gorev1);
Future<String> f2 = pool.submit(gorev2);
Future<String> f3 = pool.submit(gorev3);

// Sonuçları topla
System.out.println(f1.get()); // ~1 saniye sonra
System.out.println(f3.get()); // ~1.5 saniye sonra (zaten başlamıştı)
System.out.println(f2.get()); // ~2 saniye sonra (zaten başlamıştı)

pool.shutdown();

invokeAll — Hepsini Çalıştır, Hepsi Bitene Kadar Bekle

List<Callable<String>> gorevler = List.of(
    () -> { Thread.sleep(1000); return "A"; },
    () -> { Thread.sleep(2000); return "B"; },
    () -> { Thread.sleep(500);  return "C"; }
);

// Hepsini başlat, hepsi bitince döner
List<Future<String>> sonuclar = pool.invokeAll(gorevler);

for (Future<String> f : sonuclar) {
    System.out.println(f.get());
}
// Tüm çıktı ~2 saniye sonra gelir (en uzun görev kadar bekler)

invokeAny — İlk Biteni Al

// İlk biten görevin sonucunu döner, diğerlerini iptal eder
String ilkSonuc = pool.invokeAny(gorevler);
System.out.println("İlk biten: " + ilkSonuc); // C (~0.5 saniye)

Shutdown — Pool'u Kapat

Pool'u doğru kapatmak önemli. Yoksa program kapanmaz (user thread'ler çalışmaya devam eder).

ExecutorService pool = Executors.newFixedThreadPool(4);

// İşleri submit et...

// 1. Yeni iş kabul etme, mevcutları bitir
pool.shutdown();

// 2. Bitmesini bekle
boolean bitti = pool.awaitTermination(30, TimeUnit.SECONDS);
if (!bitti) {
    // 3. Zorla kapat
    List<Runnable> bekleyenler = pool.shutdownNow();
    System.out.println(bekleyenler.size() + " iş iptal edildi");
}

Shutdown Pattern — Best Practice

public static void guvenliKapat(ExecutorService pool) {
    pool.shutdown(); // Yeni iş alma
    try {
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
            pool.shutdownNow(); // 60 saniye dolduysa zorla kapat
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                System.err.println("Pool kapanmadı!");
            }
        }
    } catch (InterruptedException e) {
        pool.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

⚠️ `shutdown()` çağırmazsan program kapanmaz! Pool'daki thread'ler user thread olduğundan JVM bekler. Her zaman shutdown() çağır — ya da try-with-resources kullan (Java 19+).


Gerçek Hayat Örneği — Paralel Web Scraper

public class ParalelScraper {
    
    static String sayfaIndir(String url) throws Exception {
        Thread.sleep(1000); // HTTP isteği simülasyonu
        return "İçerik: " + url + " (" + url.length() + " byte)";
    }
    
    public static void main(String[] args) throws Exception {
        List<String> urls = List.of(
            "https://example.com/sayfa1",
            "https://example.com/sayfa2",
            "https://example.com/sayfa3",
            "https://example.com/sayfa4",
            "https://example.com/sayfa5"
        );
        
        ExecutorService pool = Executors.newFixedThreadPool(3);
        long baslangic = System.currentTimeMillis();
        
        // Her URL için Callable oluştur
        List<Callable<String>> gorevler = urls.stream()
            .map(url -> (Callable<String>) () -> sayfaIndir(url))
            .toList();
        
        // Hepsini çalıştır
        List<Future<String>> sonuclar = pool.invokeAll(gorevler);
        
        // Sonuçları yazdır
        for (Future<String> f : sonuclar) {
            System.out.println(f.get());
        }
        
        long sure = System.currentTimeMillis() - baslangic;
        System.out.println("Toplam süre: " + sure + "ms"); // ~2 saniye (5 iş / 3 thread)
        
        pool.shutdown();
    }
}

3 thread ile 5 sayfa: İlk 3 sayfa paralel (1 sn), sonra kalan 2 sayfa paralel (1 sn) = ~2 saniye. Sıralı olsa 5 saniye olurdu.


Thread Pool Boyutu Nasıl Belirlenir?

İş TipiÖnerilen Pool BoyutuNeden
CPU-yoğun (hesaplama)CPU çekirdek sayısıDaha fazla thread = context switch maliyeti
I/O-yoğun (dosya, ağ, DB)CPU × 2 veya daha fazlaThread'ler beklerken CPU boşta
int cpuSayisi = Runtime.getRuntime().availableProcessors();
System.out.println("CPU: " + cpuSayisi);

// CPU-yoğun iş
ExecutorService cpuPool = Executors.newFixedThreadPool(cpuSayisi);

// I/O-yoğun iş
ExecutorService ioPool = Executors.newFixedThreadPool(cpuSayisi * 2);

💡 Genel kural: CPU-yoğun iş için N thread, I/O-yoğun iş için 2N veya daha fazla thread kullan (N = CPU çekirdek sayısı). Ama en doğru cevap: ölç ve ayarla (benchmark).


Hata Yönetimi

Future ile hata yakalama:

ExecutorService pool = Executors.newFixedThreadPool(2);

Future<Integer> future = pool.submit(() -> {
    if (true) throw new RuntimeException("Bir şeyler ters gitti!");
    return 42;
});

try {
    Integer sonuc = future.get();
} catch (ExecutionException e) {
    // Görev içindeki exception burada yakalanır
    System.out.println("Görev hatası: " + e.getCause().getMessage());
    // Çıktı: Görev hatası: Bir şeyler ters gitti!
}

pool.shutdown();

execute() ile submit edilen görevlerde exception sessizce yutulur. submit() + Future.get() ile exception yakalanabilir.


Özet

  • Thread pool, thread'leri yeniden kullanarak maliyet ve karmaşıklığı azaltır

  • `Executors.newFixedThreadPool(n)` en yaygın kullanılan pool — sabit n thread

  • `submit()` ile görev gönder, `Future` ile sonuç al veya iptal et

  • `Callable` değer döner ve exception fırlatabilir — Runnable'dan daha güçlü

  • `shutdown()` her zaman çağır — yoksa program kapanmaz

  • Pool boyutu: CPU-yoğun iş → CPU sayısı, I/O-yoğun → CPU × 2