İçeriğe geç

Concurrency Araçları — Executor, Lock, Atomic ve Deadlock

İleri 9 dk Çok sık karşılaşılır

Önce şunu oku: Race Condition ve Görünürlük

30 saniyede özet

Thread açmak yerine işi bir havuza ver, kilidi mutlaka finally'de bırak, tek değişken için Atomic kullan. İki kilit gerekiyorsa hep aynı sırayla al; yoksa iki iş birbirini sonsuza kadar bekler.

Bir transfer servisi gün boyu sorunsuz çalıştı. Akşam bazı istekler hiç dönmüyor, CPU boşta, log’da tek hata yok. Kodda bir yanlışlık yok gibi — sadece iki kilit ters sırayla alınıyor.

  1. Bayt: Transfer servisim bütün gün çalıştı. Akşam bazı istekler hiç dönmedi, log'da tek hata yok.

  2. Sen: İşlemci ne durumda peki?

  3. Bayt: Boşta! Kimse çalışmıyor, ama kimse bitmiyor da.

  4. Bayt: Birileri bir şeyi bekliyor. Asıl soru: kim, kimi bekliyor?

Thread değil, görev ver

Her iş için yeni bir thread açmak, her müşteri için yeni bir garson işe almak gibidir: yoğun günde restoran taşar. Bir thread poolGörevleri sabit ya da sınırlı sayıda, yeniden kullanılan thread'e dağıtan yapı. Her görev için yeni thread açmanın bellek ve zamanlama maliyetini önler.Sözlükte gör → sabit sayıda garsonu tekrar tekrar kullanır ve aynı anda kaç işin yapılacağını sınırlar.

Kapanmayı unutmayan havuz (Java 19+)
try (var pool = Executors.newFixedThreadPool(8)) {
Future<Report> report = pool.submit(() -> build(id));
return report.get(2, SECONDS);
} // close(): yeni görev almaz, çalışanların bitmesini bekler

Bekleme kuyruğu da sınırlı olmalı. newFixedThreadPool’un kuyruğunun sonu yoktur: yük artınca işler reddedilmez, bellekte birikir ve sonunda bellek biter.

Kafam karıştı, daha basit anlat

Her müşteri için yeni garson işe alma. Birkaç garsonun olsun, işleri sırayla onlara ver. Sıra da sonsuz olmasın: kapı doluysa yeni gelene “şimdi olmaz” demek, restoranın çökmesinden iyidir.

Hızlı kontrolBaşlangıç

Gelen her istek için `new Thread(task).start()` yazmak yerine neden ExecutorService kullanılır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

`Executors.newFixedThreadPool(10)` kullanan bir servis yoğun trafikte OutOfMemoryError ile düşüyor. En olası sebep?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

synchronized’dan fazlası: Lock ve Atomic

AraçNe zamanDikkat
synchronizedBasit, kısa kritik bölgeBeklerken vazgeçemezsin
ReentrantLockZaman aşımı, tryLock, adil sıra gerekiyorsaunlock() her zaman finally’de
AtomicInteger, LongAdderTek bir sayaç ya da referansİki değişkeni birlikte koruyamaz
ConcurrentHashMapPaylaşılan mapBileşik işlem için compute, merge

Güvenli parçalar, güvenli bir bütün yapmaz. İki ayrı AtomicInteger kendi başına güvenlidir, ama aralarındaki min <= max kuralını hiçbiri korumaz.

Kafam karıştı, daha basit anlat

Tek tek güvenli iki parça, birlikte bir kuralı korumaz. Kural iki değeri birden ilgilendiriyorsa, ikisini aynı kilidin altında değiştir.

Hızlı kontrolOrta

Bir süre sonra stok güncellemeleri tamamen duruyor ve thread dump'ta herkes aynı kilidi bekliyor. Hangi satırlar sorunun kaynağı?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Hatalı satıra dokun, sonra kontrol et.

Inventory.java
Java 21UTF-8LF

Bir sınıfta `min` ve `max` diye iki AtomicInteger alanı var ve her zaman `min <= max` olmalı. İkisini ayrı ayrı atomik güncellemek yeterli mi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

İki thread iki hesap arasında para aktarıyor. Her transfer iki hesabı da kilitlemek zorunda.

T1 A → B, T2 aynı anda B → A transferi yapıyor. İkisi de önce 'from' hesabını kilitliyor. Ne olur? Cevabı göster

T1 A’yı, T2 B’yi alır. Sonra T1 B’yi, T2 A’yı bekler — sonsuza kadar. Exception yok, sadece asılı kalan iki istek.

İki araba, iki yarım köprü: ikisi de haklı, ikisi de sonsuza kadar bekliyor.
Adım adım oku
  1. T1 transferi başlatır ve ilk kilidi, A hesabını alır.
  2. Aynı anda T2 ters yöndeki transferi başlatır ve B hesabını kilitler.
  3. T1 şimdi B'yi, T2 ise A'yı bekler. İkisi de ötekinin bırakmasını bekliyor.
  4. Hiçbiri bırakmaz: deadlock. İşlemci boşta, hata yok, kimse ilerlemiyor.

Deadlock — iki transfer birbirini bekleyince

Tohum 709976
Transfer.java
1void transfer(Account from, Account to, long amount) {
2 synchronized (from) {
3 synchronized (to) {
4 from.withdraw(amount);
5 to.deposit(amount);
6 }
7 }
8}
Java 21UTF-8LF
  • Hesap Aboş
  • Hesap Bboş
  • T1çalışıyor
  • T2çalışıyor
Hız
Adım 0

Şu an ne oldu?

İki thread aynı anda transfer başlatıyor

T1: A → B. T2: A → B. Her transfer iki hesabı da kilitlemek zorunda.

Görevler0/3

  • İki transferi deadlock'a sokaçık

    İpucu

    Aynı yönde transferler asla kilitlenmez. Yönleri çaprazla.

  • Ters yönlü transferleri tryLock kullanmadan tamamlaaçık

    İpucu

    Döngü, iki thread kilitleri farklı sırayla aldığında oluşur.

  • Deadlock olmadan hiçbir transferin tamamlanmadığı bir durum yarataçık

    İpucu

    tryLock kilitlenmeyi önler. Ama iki thread her seferinde aynı anda vazgeçerse?

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanla oynat. Aynı yönde transferler: biri bekliyor, ikisi de bitiyor.
  2. T2’nin yönünü ters çevir. deadlockİki ya da daha fazla thread'in, her birinin diğerinin tuttuğu kilidi beklediği için sonsuza kadar durması. Exception yok, CPU boşta; istek sadece asılı kalır.Dört şart birlikte gerekir: karşılıklı dışlama, tut-ve-bekle, zorla geri alınamama ve döngüsel bekleme. Birini kırmak deadlock'u imkânsız kılar; pratikte en kolayı döngüsel beklemeyi kilit sırasıyla kırmaktır.Sözlükte gör →: iki kilit, iki bloklanmış thread.
  3. Kilitlemeyi “önce küçük id” yap. İki thread de önce A’yı istiyor; biri bekliyor, döngü yok.
  4. tryLock seç, jitter kapalı. Kimse bloklanmıyor, ama kimse bitiremiyor.
  5. Jitter’ı aç. Farklı bekleme süreleri simetriyi kırıyor.
Hızlı kontrolOrta

Üretimde bazı istekler hiç dönmüyor, CPU kullanımı düşük ve log'da hata yok. Deadlock'tan şüpheleniyorsun. İlk ne yaparsın?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Deadlock nasıl kırılır

Deadlock (kilitlenme) ancak dört şart birlikte olursa yaşanır. Birini kırmak yeter:

  • Döngüsel bekleme — kilitleri her yerde aynı sırayla al. En ucuz ve en sağlam çözüm.
  • Tut-ve-bekle — tryLock(timeout) başarısız olursa elindekini bırak.
  • Karşılıklı dışlama ve geri alınamama — kilidin doğası; pratikte dokunulmaz.
tryLock'un yan etkisi· istersen atla

İki thread aynı anda deneyip aynı anda vazgeçerse, sonsuza kadar birbirine yol verebilirler: buna livelockThread'lerin bloklanmadığı, sürekli çalıştığı ama birbirine yol verirken hiç ilerleyemediği durum. Simetrik yeniden deneme bunun klasik sebebidir.Sözlükte gör → denir. Yeniden denemeden önce rastgele bir süre bekle.

Kafam karıştı, daha basit anlat

İki kişi dar bir kapıda karşılaşıp birbirine “önce sen” diyor ve hiç kımıldamıyor. Herkes kilitleri aynı sırayla alırsa bu karşılaşma hiç yaşanmaz.

Hızlı kontrolİleri

Her yaklaşım deadlock'un hangi şartını kırar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Döngüsel beklemeyi kırar

Kilitler arasında döngü oluşamaz

    Tut-ve-beklemeyi kırar

    Bir kilidi tutarken ötekini sonsuza kadar beklemez

      Deadlock'u önlemez

      Dört şartın hiçbirine dokunmaz

        Tuzaklar

        shutdown() beklemez. Görevlerin bitmesini awaitTermination ya da try-with-resources ile bekle.

        Önce bak, sonra yap. containsKey ile kontrol edip put ile eklemek iki ayrı adımdır; araya başka bir thread girebilir. İkisini tek adımda yapan computeIfAbsent kullan.

        Kilit tutarken dış çağrı. Kilit içinde HTTP ya da veritabanı çağrısı, kilidi o çağrının süresi kadar tutar.

        Birden fazla sunucu. Java’daki kilitler yalnızca kendi sürecini korur. Aynı hesabı iki ayrı sunucu güncelliyorsa kilit veritabanında olmalı.

        Hızlı kontrolOrta

        Program ne yazdırır?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
        Counter.java
        1var hits = new AtomicInteger();
        2var pool = Executors.newFixedThreadPool(4);
        3
        4for (int i = 0; i < 1000; i++) {
        5 pool.submit(hits::incrementAndGet);
        6}
        7pool.shutdown();
        8System.out.println(hits.get());
        Java 21UTF-8LF

        Bu çıktı garanti değildir — program non-deterministiktir, farklı çalıştırmada başka sonuç verebilir.

        Aşağıdaki örnek bir bankanın arka ofisinden ve concurrency araçlarını gerçek işlerde kullanıyor: gelen EFT dosyası için üretici-tüketici kuyruğu, başlangıç kapısı, SMS sağlayıcısı için eşzamanlılık sınırı ve düzgün kapanan executor’lar.

        Derinleş · Bankada concurrency araçları: dört iş, dört araç 4 dosya · ~93 satır · ilk okumada atlayabilirsin
        Proje dosyaları

        src/main/java/bank/tools/ IncomingEftPipeline.java BlockingQueue: gelen EFT dosyasını okuyan ve hesaplara işleyen hızları farklı iki taraf. Sınırlı kuyruk üreticiyi yavaşlatır; zehirli hap bitişi bildirir.

        src/main/java/bank/tools/IncomingEftPipeline.java
        // Incoming EFT messages arrive as a file; each line credits one beneficiary account.
        public class IncomingEftPipeline {
        private static final String POISON = "__END__";
        private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(1_000); // bounded: backpressure
        public int run(Path csv, Consumer<String> sink) throws Exception {
        var imported = new AtomicInteger();
        try (var executor = Executors.newFixedThreadPool(2)) {
        executor.submit(() -> { // producer
        try (var lines = Files.lines(csv)) {
        for (String line : (Iterable<String>) lines::iterator) {
        queue.put(line); // blocks when the consumer falls behind
        }
        } finally {
        queue.put(POISON); // always tell the consumer we are done
        }
        return null;
        });
        executor.submit(() -> { // consumer
        for (String line = queue.take(); !line.equals(POISON); line = queue.take()) {
        sink.accept(line);
        imported.incrementAndGet();
        }
        return null;
        });
        } // close(): no new tasks, then waits for both to finish
        return imported.get();
        }
        }

        src/main/java/bank/tools/ StartupGate.java CountDownLatch: ana bankacılık bağlantısı ve HSM hazır olmadan trafiğe açılma.

        src/main/java/bank/tools/StartupGate.java
        public class StartupGate {
        private final CountDownLatch ready;
        public StartupGate(int dependencies) {
        this.ready = new CountDownLatch(dependencies);
        }
        public void dependencyReady(String name) {
        System.out.println("ready: " + name);
        ready.countDown();
        }
        // Returns false instead of hanging forever if a dependency never shows up.
        public boolean awaitAll(Duration timeout) throws InterruptedException {
        return ready.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
        }
        }

        src/main/java/bank/tools/ BulkSmsSender.java Semaphore: SMS sağlayıcısına aynı anda en fazla N çağrı.

        src/main/java/bank/tools/BulkSmsSender.java
        // "Your card statement is ready" to every customer whose statement was cut today.
        public class BulkSmsSender {
        private final Semaphore inFlight = new Semaphore(10); // the SMS provider allows 10 parallel calls
        private final SmsClient client;
        public BulkSmsSender(SmsClient client) {
        this.client = client;
        }
        public void sendAll(List<Sms> messages) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        for (Sms sms : messages) {
        executor.submit(() -> {
        inFlight.acquire(); // waits cheaply on a virtual thread
        try {
        client.send(sms);
        } finally {
        inFlight.release(); // always, even when send throws
        }
        return null;
        });
        }
        }
        }
        }

        src/main/java/bank/tools/ Main.java Hepsini try-with-resources ile kapanan executor'larla çalıştıran program.

        src/main/java/bank/tools/Main.java
        public class Main {
        public static void main(String[] args) throws Exception {
        var gate = new StartupGate(2);
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        executor.submit(() -> { Thread.sleep(200); gate.dependencyReady("core-banking"); return null; });
        executor.submit(() -> { Thread.sleep(300); gate.dependencyReady("hsm"); return null; }); // PIN/crypto keys
        if (!gate.awaitAll(Duration.ofSeconds(5))) {
        System.out.println("dependencies not ready, refusing traffic");
        return;
        }
        }
        System.out.println("accepting traffic");
        int count = new IncomingEftPipeline().run(Path.of("incoming-eft.csv"), line -> { /* credit the account */ });
        System.out.println("credited " + count + " incoming transfers");
        }
        }

        Kendini sına

        Şimşek turu1/5

        Yoğun yükte her iş için yeni bir thread açmak, havuz kullanmaktan daha güvenlidir.

        Soru 1/2İleri

        Deadlock'u önlemek için tryLock(timeout) kullandın. Şimdi yük altında transferler tamamlanmıyor ama thread'ler de bloklanmıyor. Ne oluyor, nasıl düzeltirsin?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Aklında kalacak üç şey

        1. 1 Thread pahalı bir kaynak, görev ise yapılacak iş. ExecutorService ikisini ayırır; sınırsız kuyruklu bir havuz ise aşırı yükü bellek sorununa çevirir.
        2. 2 Atomic sınıflar tek bir değişkeni korur. İki değişken arasındaki bir kural için kilit ya da tek bir değişmez nesne gerekir.
        3. 3 Deadlock, kilitler farklı sırayla alındığında olur. Her yerde aynı sırayla almak bu kilitlenmeyi imkânsız kılar.
        Sonraki kapı Güvenli olduğu söylenen bir map'te sayaç neden yine artış kaybeder? ConcurrentHashMap — Güvenli Map, Güvensiz Sayaç · 9 dk

        5 kart sonraki derste seni bekliyor

        0/5 kart bu dersten toplandı

        Bu dersin üstüne kurulanlar

        Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.