← Kursa Dön
📄 Text · 15 min

İ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:

  1. deger'i oku (örn. 5)

  2. 1 ekle (6)

  3. 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ı

  • synchronized method → this nesnesini kilitler

  • static synchronized method → sınıfın Class nesnesini kilitler

  • synchronized(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)

  • synchronized bloğ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 synchronized yeterli ve daha az hata riski - tryLock, zaman aşımı, ReadWriteLock gerekiyorsa Lock kullan - Lock kullanı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 Yaparvolatile Yapmaz
Görünürlük garantisiAtomik operasyon garantisi
Bir thread yazınca diğeri görürdeger++ thread-safe yapmaz
Boolean flag'ler için yeterliBileş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 AtomicInteger

Deadlock — Ö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. jstack veya 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 ekle

Kelime 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ç

SenaryoAraç
Basit sayaçAtomicInteger
Boolean flagvolatile boolean veya AtomicBoolean
Thread-safe mapConcurrentHashMap
Az yazma, çok okuma listesiCopyOnWriteArrayList
Üretici-tüketiciBlockingQueue
Basit kritik bölgesynchronized
tryLock, timeout, ReadWriteReentrantLock / ReadWriteLock
Birden fazla değişkeni birlikte güncellemesynchronized 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 yetersiz

  • Deadlock 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