Concurrency Araçları — Executor, Lock, Atomic ve Deadlock
Ö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.
-
Bayt: Transfer servisim bütün gün çalıştı. Akşam bazı istekler hiç dönmedi, log'da tek hata yok.
-
Sen: İşlemci ne durumda peki?
-
Bayt: Boşta! Kimse çalışmıyor, ama kimse bitmiyor da.
-
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.
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 beklerBekleme 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.
Gelen her istek için `new Thread(task).start()` yazmak yerine neden ExecutorService kullanılır?
`Executors.newFixedThreadPool(10)` kullanan bir servis yoğun trafikte OutOfMemoryError ile düşüyor. En olası sebep?
synchronized’dan fazlası: Lock ve Atomic
| Araç | Ne zaman | Dikkat |
|---|---|---|
synchronized | Basit, kısa kritik bölge | Beklerken vazgeçemezsin |
ReentrantLock | Zaman aşımı, tryLock, adil sıra gerekiyorsa | unlock() her zaman finally’de |
AtomicInteger, LongAdder | Tek bir sayaç ya da referans | İki değişkeni birlikte koruyamaz |
ConcurrentHashMap | Paylaşılan map | Bileş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.
Bir süre sonra stok güncellemeleri tamamen duruyor ve thread dump'ta herkes aynı kilidi bekliyor. Hangi satırlar sorunun kaynağı?
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?
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.
Adım adım oku
- T1 transferi başlatır ve ilk kilidi, A hesabını alır.
- Aynı anda T2 ters yöndeki transferi başlatır ve B hesabını kilitler.
- T1 şimdi B'yi, T2 ise A'yı bekler. İkisi de ötekinin bırakmasını bekliyor.
- Hiçbiri bırakmaz: deadlock. İşlemci boşta, hata yok, kimse ilerlemiyor.
Deadlock — iki transfer birbirini bekleyince
Tohum 709976void transfer(Account from, Account to, long amount) { synchronized (from) { synchronized (to) { from.withdraw(amount); to.deposit(amount); } }}- Hesap Aboş
- Hesap Bboş
- T1çalışıyor
- T2çalışıyor
Ş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.
- Varsayılanla oynat. Aynı yönde transferler: biri bekliyor, ikisi de bitiyor.
- 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.
- Kilitlemeyi “önce küçük id” yap. İki thread de önce A’yı istiyor; biri bekliyor, döngü yok.
- tryLock seç, jitter kapalı. Kimse bloklanmıyor, ama kimse bitiremiyor.
- Jitter’ı aç. Farklı bekleme süreleri simetriyi kırıyor.
Üretimde bazı istekler hiç dönmüyor, CPU kullanımı düşük ve log'da hata yok. Deadlock'tan şüpheleniyorsun. İlk ne yaparsın?
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.
Her yaklaşım deadlock'un hangi şartını kırar?
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ı.
Program ne yazdırır?
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
Kendini sına
Yoğun yükte her iş için yeni bir thread açmak, havuz kullanmaktan daha güvenlidir.
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?
Aklında kalacak üç şey
- 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 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 Deadlock, kilitler farklı sırayla alındığında olur. Her yerde aynı sırayla almak bu kilitlenmeyi imkânsız kılar.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.