İleri Thread Konuları
Thread'leri oluşturduk, pool'da yönettik. Şimdi gerçek zorluğa geliyoruz: birden fazla thread aynı veriye eriştiğinde ne olur? Bu derste synchronized, Lock, volatile, deadlock ve concurrent koleksiyonları öğreneceğiz. Multithreading'in "dikkat et yoksa başın yanar" kısmı burası.
Race Condition — Yarış Durumu
İki thread aynı değişkeni aynı anda güncellemeye çalışırsa ne olur? Önceki dersten hatırla:
public class Sayac {
private int deger = 0;
public void artir() {
deger++; // Thread-safe DEĞİL!
}
public int getDeger() {
return deger;
}
}deger++ basit görünür ama aslında 3 adım:
deger'i oku (örn. 5)1 ekle (6)
Geri yaz (6)
Analoji: İki kişi aynı anda ATM'den bakiye sorguluyor. İkisi de 1000 TL görüyor. Biri 500 TL çekiyor (1000-500=500 yazar), diğeri 300 TL çekiyor (1000-300=700 yazar). Sonuç: 700 TL görünür ama aslında 200 TL kalmalıydı. İşte bu race condition.
synchronized — Kritik Bölgeyi Kilitle
synchronized, bir kod bloğuna aynı anda sadece bir thread'in girmesini garanti eder. Diğer thread'ler kapıda bekler.
Synchronized Method
public class GuvenliSayac {
private int deger = 0;
// Aynı anda sadece 1 thread bu metoda girebilir
public synchronized void artir() {
deger++;
}
public synchronized int getDeger() {
return deger;
}
}GuvenliSayac sayac = new GuvenliSayac();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100_000; i++) sayac.artir();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100_000; i++) sayac.artir();
});
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(sayac.getDeger()); // Her zaman 200000 ✅Synchronized Block
Tüm metodu kilitlemek yerine, sadece kritik bölgeyi kilitleyebilirsin:
public class GuvenliSayac {
private int deger = 0;
private final Object kilit = new Object();
public void artir() {
// Sadece bu blok kilitli
synchronized (kilit) {
deger++;
}
// Burası kilitsiz — diğer thread'ler girebilir
}
}Synchronized block daha iyi çünkü:
Kilitleme süresini minimize eder
Farklı kilit nesneleri ile farklı bölgeleri bağımsız kilitleyebilirsin
public class BankaHesabi {
private double bakiye;
private final List<String> islemler = new ArrayList<>();
private final Object bakiyeKilidi = new Object();
private final Object islemKilidi = new Object();
public void paraYatir(double miktar) {
synchronized (bakiyeKilidi) {
bakiye += miktar;
}
synchronized (islemKilidi) {
islemler.add("Yatırma: " + miktar);
}
}
}İki ayrı kilit: bakiye ve işlem listesi bağımsız kilitleniyor. Birini kilitleyen diğerini bloklamaz.
Synchronized Kuralları
synchronizedmethod →thisnesnesini kilitlerstatic synchronizedmethod → sınıfınClassnesnesini kilitlersynchronized(nesne)→ belirtilen nesneyi kilitler
// Bu ikisi aynı şey:
public synchronized void metod1() { /* ... */ }
public void metod2() {
synchronized (this) { /* ... */ }
}
// Static synchronized — Class nesnesini kilitler
public static synchronized void statikMetod() { /* ... */ }⚠️ synchronized performans maliyeti taşır! Thread'ler sıra bekler, paralellik azalır. Sadece gerçekten paylaşılan veriye erişen kısmı kilitle, tüm metodu değil.
Lock Interface — Daha Esnek Kilitleme
synchronized basit ve etkili ama esnek değil. Lock interface'i daha fazla kontrol sağlar:
Kilidi almayı deneyebilirsin (tryLock)
Zaman aşımı ile bekleyebilirsin
Adil kilitleme yapabilirsin (en uzun bekleyen önce)
synchronizedbloğu dışında kilidi açabilirsin
import java.util.concurrent.locks.ReentrantLock;
public class LockOrnegi {
private final ReentrantLock lock = new ReentrantLock();
private int deger = 0;
public void artir() {
lock.lock(); // Kilitle
try {
deger++;
} finally {
lock.unlock(); // MUTLAKA unlock et — finally'de!
}
}
}tryLock — Kilidi Almayı Dene
ReentrantLock lock = new ReentrantLock();
Thread t = new Thread(() -> {
if (lock.tryLock()) { // Kilidi almayı dene
try {
System.out.println("Kilit alındı, iş yapılıyor");
} finally {
lock.unlock();
}
} else {
System.out.println("Kilit meşgul, başka iş yapıyorum");
}
});tryLock ile Zaman Aşımı
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) { // Max 2 saniye bekle
try {
// İş yap
} finally {
lock.unlock();
}
} else {
System.out.println("2 saniye içinde kilit alınamadı");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}ReadWriteLock — Okuma/Yazma Ayrımı
Çoğu uygulamada okuma, yazmadan çok daha sıktır. ReadWriteLock ile birden fazla thread aynı anda okuyabilir, ama yazma sırasında herkes bekler:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Onbellek<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public V oku(K key) {
lock.readLock().lock(); // Birçok thread aynı anda okuyabilir
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
}
public void yaz(K key, V value) {
lock.writeLock().lock(); // Yazarken herkes bekler
try {
cache.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}💡 synchronized vs Lock: - Basit durumlarda
synchronizedyeterli ve daha az hata riski - tryLock, zaman aşımı, ReadWriteLock gerekiyorsaLockkullan -Lockkullanıyorsan `finally`'de `unlock()` unutma!
volatile — Görünürlük Garantisi
volatile, bir değişkenin her zaman ana bellekten okunmasını ve yazılmasını garanti eder. Thread'ler kendi cache'lerinden eski değer okumaz.
public class DurdurulabilirServis {
private volatile boolean calisiyor = true;
public void calis() {
while (calisiyor) { // Her okumada ana bellekten okur
// İş yap
}
System.out.println("Durdu.");
}
public void durdur() {
calisiyor = false; // Ana belleğe yazar, diğer thread hemen görür
}
}volatile olmazsa ne olur? Thread-1'in CPU cache'inde calisiyor = true kalabilir. Thread-2 false yapsa bile Thread-1 bunu asla görmeyebilir. Sonsuz döngü!
volatile Ne Yapar, Ne Yapmaz?
| volatile Yapar | volatile Yapmaz |
|---|---|
| Görünürlük garantisi | Atomik operasyon garantisi |
| Bir thread yazınca diğeri görür | deger++ thread-safe yapmaz |
| Boolean flag'ler için yeterli | Bileşik operasyonlar için yetersiz |
private volatile int sayac = 0;
// volatile bu durumda YETERSİZ — oku-değiştir-yaz atomik değil
public void artir() {
sayac++; // Hâlâ race condition var!
}
// Çözüm: synchronized veya AtomicIntegerDeadlock — Ölümcül Kilitlenme
İki thread, birbirinin kilitlediği kaynağı beklerse, ikisi de sonsuza kadar bekler. Buna deadlock denir.
Object kilit1 = new Object();
Object kilit2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (kilit1) {
System.out.println("t1: kilit1 aldı");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (kilit2) { // kilit2 bekleniyor — t2 tutuyor!
System.out.println("t1: kilit2 aldı");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (kilit2) {
System.out.println("t2: kilit2 aldı");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (kilit1) { // kilit1 bekleniyor — t1 tutuyor!
System.out.println("t2: kilit1 aldı");
}
}
});
t1.start();
t2.start();
// Program SONSUZA KADAR bekler — deadlock!Deadlock'u Önlemek
1. Kilit sıralaması — Her zaman aynı sırada kilitle
// HER İKİ THREAD de önce kilit1, sonra kilit2 alır
Thread t1 = new Thread(() -> {
synchronized (kilit1) {
synchronized (kilit2) { /* iş yap */ }
}
});
Thread t2 = new Thread(() -> {
synchronized (kilit1) { // Aynı sıra!
synchronized (kilit2) { /* iş yap */ }
}
});2. tryLock ile zaman aşımı
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
try {
// İş yap
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}3. Tek kilit kullan — mümkünse birden fazla kilit yerine tek kilit
⚠️ Deadlock'u debug etmek çok zor! Program donmuş gibi görünür ama hata mesajı yoktur.
jstackveya IDE thread dump ile tespit edilebilir. En iyisi deadlock'a hiç girmemek — kilit sıralaması uygula.
ConcurrentHashMap — Thread-Safe Map
Normal HashMap thread-safe değil. Birden fazla thread aynı anda erişirse veri bozulabilir, hatta sonsuz döngüye girebilir.
// KÖTÜ — HashMap thread-safe değil
Map<String, Integer> map = new HashMap<>(); // Çoklu thread'de tehlikeli!
// ESKİ YOL — tüm map kilitlenir, yavaş
Map<String, Integer> sync = Collections.synchronizedMap(new HashMap<>());
// DOĞRU — segment bazlı kilitleme, hızlı
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();ConcurrentHashMap Kullanımı
ConcurrentHashMap<String, Integer> puanlar = new ConcurrentHashMap<>();
// Temel işlemler — normal Map gibi
puanlar.put("Ali", 85);
puanlar.put("Veli", 90);
int puan = puanlar.get("Ali"); // 85
// Atomik operasyonlar — thread-safe
puanlar.putIfAbsent("Ayşe", 78); // Yoksa ekle
puanlar.replace("Ali", 85, 90); // 85 ise 90 yap
puanlar.compute("Veli", (k, v) -> v + 5); // 90 → 95
puanlar.merge("Ali", 10, Integer::sum); // Ali'ye 10 ekleKelime Frekans Sayma — Thread-Safe
ConcurrentHashMap<String, Integer> frekans = new ConcurrentHashMap<>();
// Birden fazla thread aynı anda güvenle sayabilir
public void kelimeSay(String kelime) {
frekans.merge(kelime, 1, Integer::sum);
}merge atomik bir operasyon — kilitleme gerekmez, ConcurrentHashMap halleder.
Atomic Değişkenler
java.util.concurrent.atomic paketi, lock kullanmadan thread-safe operasyonlar sunar. CAS (Compare-And-Swap) algoritması ile çalışır — synchronized'dan daha hızlı.
AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger sayac = new AtomicInteger(0);
// Atomik artırma
sayac.incrementAndGet(); // ++sayac (artır, yeni değeri döndür)
sayac.getAndIncrement(); // sayac++ (eski değeri döndür, artır)
sayac.addAndGet(5); // sayac += 5
// Atomik güncelleme
sayac.compareAndSet(6, 10); // Eğer 6 ise 10 yap
sayac.updateAndGet(n -> n * 2); // Değeri 2 katına çıkar// Thread-safe sayaç — lock gerekmez
AtomicInteger sayac = new AtomicInteger(0);
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 100_000; i++) {
pool.submit(sayac::incrementAndGet);
}
pool.shutdown();
pool.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(sayac.get()); // Her zaman 100000 ✅Diğer Atomic Sınıflar
AtomicLong atomicLong = new AtomicLong(0);
AtomicBoolean atomicBool = new AtomicBoolean(false);
AtomicReference<String> atomicRef = new AtomicReference<>("Merhaba");
// AtomicReference ile thread-safe güncelleme
atomicRef.updateAndGet(s -> s.toUpperCase()); // "MERHABA"💡 Atomic vs synchronized: - Basit sayaç/flag → Atomic (daha hızlı) - Birden fazla değişkeni birlikte güncelleme → synchronized/Lock (atomik blok gerekli) - Tek değişken üzerinde basit operasyonlar → Atomic her zaman daha iyi
Diğer Concurrent Koleksiyonlar
CopyOnWriteArrayList
Yazma az, okuma çok olan durumlar için. Her yazma işleminde yeni bir kopya oluşturulur.
CopyOnWriteArrayList<String> liste = new CopyOnWriteArrayList<>();
liste.add("Ali");
liste.add("Veli");
// Iterator kullanırken eleman eklense bile ConcurrentModificationException OLMAZ
for (String isim : liste) {
liste.add("Yeni"); // Güvenli — iterator eski kopyayı görür
}BlockingQueue
Üretici-tüketici (producer-consumer) pattern için:
BlockingQueue<String> kuyruk = new LinkedBlockingQueue<>(10); // Max 10 eleman
// Üretici thread
Thread uretici = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
kuyruk.put("Ürün-" + i); // Kuyruk doluysa bekler
System.out.println("Üretildi: Ürün-" + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Tüketici thread
Thread tuketici = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
String urun = kuyruk.take(); // Kuyruk boşsa bekler
System.out.println("Tüketildi: " + urun);
Thread.sleep(200); // Yavaş tüketici
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
uretici.start();
tuketici.start();Araçları Doğru Seç
| Senaryo | Araç |
|---|---|
| Basit sayaç | AtomicInteger |
| Boolean flag | volatile boolean veya AtomicBoolean |
| Thread-safe map | ConcurrentHashMap |
| Az yazma, çok okuma listesi | CopyOnWriteArrayList |
| Üretici-tüketici | BlockingQueue |
| Basit kritik bölge | synchronized |
| tryLock, timeout, ReadWrite | ReentrantLock / ReadWriteLock |
| Birden fazla değişkeni birlikte güncelleme | synchronized blok |
Gerçek Hayat Örneği — Thread-Safe Cache
public class SimpleCache<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
private final AtomicInteger hitCount = new AtomicInteger(0);
private final AtomicInteger missCount = new AtomicInteger(0);
public V get(K key, Function<K, V> loader) {
V value = cache.get(key);
if (value != null) {
hitCount.incrementAndGet();
return value;
}
missCount.incrementAndGet();
// computeIfAbsent atomik — aynı key için tek sefer hesaplar
return cache.computeIfAbsent(key, loader);
}
public void invalidate(K key) {
cache.remove(key);
}
public String istatistik() {
int hit = hitCount.get();
int miss = missCount.get();
int toplam = hit + miss;
double oran = toplam > 0 ? (double) hit / toplam * 100 : 0;
return String.format("Hit: %d, Miss: %d, Oran: %.1f%%", hit, miss, oran);
}
}SimpleCache<String, String> cache = new SimpleCache<>();
// 10 thread aynı anda cache'e erişiyor
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final String key = "key-" + (i % 10);
pool.submit(() -> {
String value = cache.get(key, k -> {
// Yavaş hesaplama simülasyonu
try { Thread.sleep(100); } catch (Exception e) {}
return "value-" + k;
});
});
}
pool.shutdown();
pool.awaitTermination(30, TimeUnit.SECONDS);
System.out.println(cache.istatistik());
// Hit: 90, Miss: 10, Oran: 90.0%Özet
`synchronized` aynı anda tek thread'in kritik bölgeye girmesini sağlar — basit ama etkili
`Lock` (ReentrantLock) daha esnek kilitleme: tryLock, timeout, ReadWriteLock
`volatile` değişkenin tüm thread'lere görünür olmasını garanti eder — flag'ler için yeterli,
++için yetersizDeadlock iki thread birbirini beklediğinde oluşur — kilit sıralaması ile önle
`ConcurrentHashMap` thread-safe map —
synchronizedMap'ten çok daha hızlı`AtomicInteger`/`AtomicLong` lock-free thread-safe sayaçlar — basit operasyonlarda synchronized'dan üstün
AI Asistan
Sorularını yanıtlamaya hazır