---
title: 'Eşzamanlılık Yardımcıları'
source: 'https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/eszamanlilik-yardimcilari'
course: 'JVM, Eşzamanlılık ve Başarım'
language: tr
updated: '2026-08-23T07:00:56+00:00'
license: 'CC BY-SA 4.0'
---

# Eşzamanlılık Yardımcıları

Karşılaştır-ve-ata kurgulanmış bir çakışmada üç denemede sonuca ulaşıyor ve iş boyunca çağırandan hiç ayrılmıyor. CountDownLatch ile Semaphore bekleyeni engelliyor, sayacı başka taraf düşürüyor; CopyOnWriteArrayList dört anlık görüntünün her birini değişmeden koruyor, yazan taraf her değişiklikte yeni bir dizi üretiyor.

Önceki ders kilidi elle almak ve bırakmaktan söz etti — `synchronized` bloğu ya da
`lock()`/`unlock()` çifti, her çağrıda yeniden yazıldı ve serbest bırakmayı garanti etmek
`finally`'e düştü. Standart kitaplığın eşzamanlılık paketi bu elle tutma işini kaynak
metinden kaldırıyor: `AtomicInteger` hiç kilit içermeden atomik güncelleme sağlıyor,
`CountDownLatch` ve `Semaphore` bekleme ile serbest bırakmayı ayrı iki tarafa bölüyor,
`CopyOnWriteArrayList` ve `ConcurrentHashMap` gezinme sırasında eşzamanlı değişikliğe karşı
kendi disiplinini taşıyor. Kaynak metinde artık hiçbir `synchronized` anahtar sözcüğü yok —
ama işin kendisi hâlâ birinin üzerine düşüyor. Bu dersin sorusu: yardımcı tip, bu işi hangi
tarafa yüklüyor?

Dört yardımcı, dört farklı yanıt veriyor. `AtomicInteger` işi hiç dağıtmıyor, tamamen
çağıranın kendi döngüsünde tutuyor. `CountDownLatch` ve `Semaphore` işi iki tarafa
bölüyor: biri bekliyor, öteki serbest bırakıyor, ve bu iki rol hiçbir zaman aynı çağrıda
birleşmiyor. `CopyOnWriteArrayList` yükü **yazan** tarafa yüklüyor ve gezinen tarafı tamamen
muaf tutuyor. `ConcurrentHashMap` ise hiçbir tarafı engellemeden ikisinin de aynı anda
çalışmasına izin veriyor. Ortak nokta, kaynak metinde hiçbirinin bir kilit sözcüğü
taşımaması; fark, kilidin yerini alan mekanizmanın işi nereye koyduğu.

## Karşılaştır-ve-Ata: İş Çağıranda Kalıyor

- **EZ12** — Gerçek bir çakışma koşturulmuyor; ilk iki denemede "başka bir taraf araya
  girmiş" gibi sayaç kasıtlı olarak kaydırılıyor. Bu kurgu tek bir iş parçacığı içinde,
  belirlenimci biçimde çalışıyor.

`AtomicInteger` hiç kilit içermiyor, ama tek bir yöntemle atomik güncelleme sağlıyor:
**karşılaştır-ve-ata**. Bu yöntem "değeri şuysa şuna çevir, değilse hiçbir şey yapma ve
başarısız olduğunu söyle" der; çağıran başarısız yanıtı aldığında yeni değeri okuyup tekrar
dener. Döngünün tamamı **çağıranın** kendi kod akışında kalır — hiçbir yeni iş parçacığı
oluşmaz, hiçbir kilit devreye girmez.

```java
// KarsilastirVeAta.java — is caginanda kalir, cakisma kurgulanarak yeniden deneme sayisi belirlenimci olculur
import java.util.concurrent.atomic.AtomicInteger;

public class KarsilastirVeAta {
    public static void main(String[] args) {
        AtomicInteger sayac = new AtomicInteger(0);
        int denemeSayisi = 0;
        int eski, yeni;
        do {
            eski = sayac.get();
            yeni = eski + 1;
            denemeSayisi++;
            // ilk iki denemede kurgulanmis bir cakisma: baska bir taraf araya girmis gibi degeri kaydiriyoruz
            if (denemeSayisi <= 2) sayac.incrementAndGet();
        } while (!sayac.compareAndSet(eski, yeni));
        System.out.println("cagiranin gordugu deneme sayisi: " + denemeSayisi);
        System.out.println("sayacin son degeri              : " + sayac.get());
    }
}
```

```
cagiranin gordugu deneme sayisi: 3
sayacin son degeri              : 3
```

Döngü her turda önce sayacı okuyor (`eski`), hedef değeri hesaplıyor (`yeni`), sonra
`compareAndSet(eski, yeni)` çağırıyor. İlk iki turda araya kasıtlı bir "kayma" giriyor —
gerçek bir programda bu kaymayı başka bir iş parçacığının kendi `compareAndSet`'i yapardı; bu
ders böyle bir iş parçacığı hiç oluşturmuyor, kaymayı doğrudan yazıyor ve **modellediğini**
burada kaydediyor. İlk iki çağrı bu yüzden başarısız oluyor — çağıranın okuduğu `eski` değer,
`compareAndSet` çalıştığı anda artık geçerli değil — üçüncü çağrıda araya kimse girmiyor ve
işlem sonuca ulaşıyor. Üç deneme boyunca iş hep aynı iş parçacığındaydı; ne bir kilit alındı
ne bir başkası çağrıldı, yalnız okuma-hesapla-dene döngüsü tekrarlandı.

Bu döngünün `synchronized` bloğundan farkı, başarısızlığa verdiği yanıtta. Bir kilit isteği
reddedildiğinde çağıran **engellenir** ve kilit boşalana kadar hiçbir şey yapamaz;
`compareAndSet` reddedildiğinde çağıran engellenmez, yalnız bir mantıksal `false` alır ve
dilediği gibi karşılık verebilir — burada olduğu gibi yeniden deneyebilir, isterse vazgeçip
başka bir işe geçebilir. Yeniden deneme sayısının üstte bir sınırı yoktur: çakışma ne kadar
sık olursa yeniden deneme de o kadar sık olur, ama her denemede iş yine çağıranın kendi
akışında kalır. Gerçek bir çok iş parçacıklı yükte bu sayı ortama bağlı olurdu; burada
kurgulanmış olduğu için `3` sabit ve tekrarlanabilir.

## Bekleyen Engellenir, Sayacı Başka Taraf Düşürür

- **EZ13** — Günlüğün sırası tek bir kanıt taşıyor: B'nin izni aldığı satır, A'nın izni
  bıraktığı satırdan **önce hiç görünemiyor** — çünkü `Semaphore` tek izin veriyor ve B, A
  bırakana kadar `acquire()` içinde engelleniyor.

`CountDownLatch` sayaç sıfıra düşene kadar bekleyeni engelliyor; sayacı düşürmek **her zaman
başka bir tarafın** işi — bekleyenin kendisi asla kendi sayacını düşürmüyor. `Semaphore` buna
yakın ama farklı bir sözleşme taşıyor: sabit sayıda **izin** dağıtıyor, `acquire()` izin
kalmayınca engelliyor, `release()` izni geri veriyor. Aşağıdaki kurulum ikisini birlikte
kullanıyor: `girdiA` ve `bittiB` birer `CountDownLatch`, aradaki karşılıklı dışlamayı ise tek
izinlik bir `Semaphore` sağlıyor.

```java
// Nobetci.java — CountDownLatch bekleyeni engeller, Semaphore izni baska taraf birakir
import java.util.*;
import java.util.concurrent.*;

public class Nobetci {
    public static void main(String[] args) throws InterruptedException {
        List<String> gunluk = Collections.synchronizedList(new ArrayList<>());
        Semaphore izin = new Semaphore(1);
        CountDownLatch girdiA = new CountDownLatch(1);
        CountDownLatch birak = new CountDownLatch(1);
        CountDownLatch bittiB = new CountDownLatch(1);

        Thread a = new Thread(() -> {
            try {
                izin.acquire();
                gunluk.add("A izni aldi");
                girdiA.countDown();
                birak.await();
                gunluk.add("A izni birakiyor");
                izin.release();
            } catch (InterruptedException e) { }
        });
        a.start();
        girdiA.await();

        Thread b = new Thread(() -> {
            try {
                izin.acquire(); // A birakana kadar bekler
                gunluk.add("B izni aldi");
                izin.release();
                bittiB.countDown();
            } catch (InterruptedException e) { }
        });
        b.start();

        birak.countDown();
        bittiB.await();
        a.join(); b.join();
        System.out.println(gunluk);
    }
}
```

```
[A izni aldi, A izni birakiyor, B izni aldi]
```

A tek izni aldıktan sonra `girdiA` ile haber veriyor ve kendi `birak` mandalını bekliyor. B
bu sırada başlatılıyor ve `izin.acquire()` çağırıyor — ama izin A'da, bu yüzden B'nin
`"B izni aldi"` satırı günlüğe düşemiyor, tıpkı önceki dersteki ikinci kilit isteyicisi gibi
engelleniyor. Ana iş parçacığı `birak.countDown()` çağırdığında A uyanıyor, izni bırakıyor
(`"A izni birakiyor"`), ancak o andan sonra B izni alabiliyor. Bekleyen taraf (B) kendi
kendini serbest bırakamıyor; sayacı düşüren de izni bırakan da **her zaman öteki taraf**.

Üç ayrı senkronizasyon aracının burada aynı programda üç ayrı iş gördüğüne dikkat etmek
gerekiyor. `girdiA` ve `bittiB` **birer kerelik** haber taşıyor — her biri bir kez düşüyor,
bir daha kullanılmıyor. `izin` ise **tekrar kullanılabilir**: A onu bırakınca B alıyor, B de
bırakınca üçüncü bir çağıran gelseydi o da alabilirdi. `CountDownLatch` bu yüzden "bir kez
olan bir şeyi bekle" sorusuna, `Semaphore` "aynı anda en çok kaç tarafın içeride olacağını
sınırla" sorusuna karşılık veriyor — burada sınır bire ayarlandığı için `Semaphore` bir kilide
çok benziyor, ama izin sayısı birden fazla olsaydı aynı anda birden fazla çağıranın içeri
girmesine izin verirdi; bu, `synchronized`'ın hiçbir zaman veremeyeceği bir esneklik.

## Yazarken Kopyalama: Gezinen Ödemez, Yazan Öder

- **EZ14** — Dört anlık görüntü, listeye eklenen her ögeden **önce** alınıyor; her görüntü
  kendi alındığı andaki boyutu görüyor ve bu boyut daha sonraki eklemelerden hiç etkilenmiyor.

`CopyOnWriteArrayList` bir yazma işleminde (`add`, `remove`, `set`) mevcut diziyi olduğu
gibi bırakıp **yeni bir dizi** üretiyor ve iç referansı o yeni diziye çeviriyor. Bu yüzden
yazmadan önce alınmış bir yineleyici, kendi aldığı dizide donuyor — sonraki yazmalar o
yineleyiciyi hiç etkilemiyor, üstüne bir istisna da düşürmüyor.

```java
// YazarkenKopyalama.java — gezinen taraf odemez, yazan taraf her degisiklikte yeni bir dizi uretir
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class YazarkenKopyalama {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> liste = new CopyOnWriteArrayList<>();
        List<Iterator<String>> anlikGoruntuler = new ArrayList<>();

        anlikGoruntuler.add(liste.iterator());
        liste.add("a");
        anlikGoruntuler.add(liste.iterator());
        liste.add("b");
        anlikGoruntuler.add(liste.iterator());
        liste.add("c");
        anlikGoruntuler.add(liste.iterator());

        int i = 0;
        for (Iterator<String> it : anlikGoruntuler) {
            int sayim = 0;
            while (it.hasNext()) { it.next(); sayim++; }
            System.out.println("anlik goruntu " + i + " gordugu oge sayisi: " + sayim);
            i++;
        }
        System.out.println("son liste boyutu: " + liste.size());
    }
}
```

```
anlik goruntu 0 gordugu oge sayisi: 0
anlik goruntu 1 gordugu oge sayisi: 1
anlik goruntu 2 gordugu oge sayisi: 2
anlik goruntu 3 gordugu oge sayisi: 3
son liste boyutu: 3
```

Dört yineleyici de `liste.add` çağrılmadan **önce** alınıyor, ama hepsi döngüden **sonra**
gezinilerek okunuyor — üçüncü ekleme yapıldığında ilk üç yineleyici çoktan var. Buna rağmen
hiçbiri sonraki eklemeleri görmüyor: sıfırıncı görüntü hep `0`, birinci hep `1` diyor. Dört
farklı boyut gözlemlemiş olmak, dört ayrı dizinin var olduğunun dolaylı kanıtı — `gezinen
taraf hiçbir şey ödemiyor` çünkü kendi aldığı diziyi hiç paylaşmıyor, `yazan taraf öder` çünkü
her `add` çağrısı `liste.size()`'ın son değerine kadar büyüyen bir diziyi **baştan** kopyalıyor.
Üç `add` çağrısı üç yeni dizi üretiyor; ilk boş dizi de sayılırsa ölçüm boyunca gözlemlenen
farklı boyutların sayısı **dört**.

Bedelin nereye düştüğü, bu yapının ne zaman tercih edildiğini de belirliyor. Gezinme çok, yazma
seyrekse (örneğin bir dinleyici listesi neredeyse hiç değişmeden sürekli dolaşılıyorsa)
yazma başına bir kopyalama bedeli kabul edilebilir, çünkü ödeyen taraf zaten seyrek
çalışıyor. Yazma sık, gezinme seyrekse aynı bedel her seferinde tam bir kopyalamaya
dönüşür ve bu artık kabul edilebilir olmayabilir; o durumda `ConcurrentHashMap`'in aşağıda
ölçülen deseni — hiçbir tarafın tam kopya çıkarmadığı desen — daha uygun düşer. Bu ders iki
yapı arasında bir hız karşılaştırması yapmıyor, yalnız hangi tarafın ne ödediğini sayıyor;
seçim, o ödemenin hangi tarafta daha ucuz olduğuna bakan bir tasarım kararıdır.

## Eşzamanlı Koleksiyonlarda Yazan Kim

- **EZ15** — Gezinme sırasında yapılan yazma, ayrı bir iş parçacığında ve `join()` ile
  tamamlanmış olarak gerçekleşiyor; sıra çizelgeleyiciye bırakılmıyor, ne zaman yazılacağı
  döngünün içinde açıkça seçiliyor.

`ConcurrentHashMap`'in erken hata veren yineleyiciden farkı ve anlık kopya üzerinden gezinme
davranışı M08/K03 Standart Kütüphane ve Akışlar kursunda zaten kuruldu; bu ders o sınıfları yeniden
tanıtmıyor, yalnız kendi sorusunu ekliyor: gezinme sırasında bir yazma olduğunda, o yazmayı
**hangi iş parçacığı** yapıyor?

```java
// EszamanliHarita.java — gezinme sirasinda yazma istisna vermiyor, yazan is parcacigi etiketle belli
import java.util.concurrent.ConcurrentHashMap;

public class EszamanliHarita {
    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<String, Integer> harita = new ConcurrentHashMap<>();
        harita.put("a", 1);
        harita.put("b", 2);

        int gorulen = 0;
        for (var giris : harita.entrySet()) {
            gorulen++;
            if (gorulen == 1) {
                Thread yazan = new Thread(() -> {
                    harita.put("c", 3);
                    System.out.println("yazan: olusturulan is parcacigi");
                });
                yazan.start();
                yazan.join();
            }
        }
        System.out.println("gezinme istisna vermeden bitti, ziyaret edilen giris sayisi=" + gorulen);
        System.out.println("gezinme sonrasi boyut: " + harita.size());
    }
}
```

```
yazan: olusturulan is parcacigi
gezinme istisna vermeden bitti, ziyaret edilen giris sayisi=3
gezinme sonrasi boyut: 3
```

Yazma, **oluşturulan** bir iş parçacığında ve gezinme henüz sürerken (`gorulen == 1`'de,
döngü daha ilk girişteyken) çalışıyor; `join()` bu yazmanın gezinme devam etmeden önce
tamamlanmış olmasını garanti ediyor. Gezinme hiçbir istisna vermeden bitiyor — `ArrayList`'in
erken hata veren yineleyicisi aynı kurulumda `ConcurrentModificationException` fırlatırdı, bu
davranış zaten önceki kursta ölçüldü. Bu koşumda eklenen üçüncü giriş gezinmeye de yansıyor,
ama bu, `ConcurrentHashMap`'in **garanti ettiği** bir şey değil — yineleyici yazma anından
**önceki ya da sonraki** bir görüntüyü gösterebilir, ikisi de sözleşmeye uygun; garanti
edilen tek şey istisna atılmamasıdır.

`yazan: olusturulan is parcacigi` satırı bu dersin kendi eklediği soruyu yanıtlıyor. Önceki
kursta ölçülen davranış tek bir iş parçacığı içinde bile geçerliydi — bir yineleyici alındıktan
sonra **aynı** iş parçacığının yapısı değiştirmesi de erken hata verirdi. Burada değiştiren
taraf ayrı bir iş parçacığı, ve etiketten görüldüğü gibi bu **oluşturulan** bir iş parçacığı,
gezinen `main`'in kendisi değil. `ConcurrentHashMap`'in sözleşmesi hangi iş parçacığının
yazdığını hiç sormuyor — kimin yazdığından bağımsız olarak istisna atmıyor; kimliği bu derste
yalnız izlemek için ekleniyor, sözleşmenin bir parçası değil.

## Sınırlayıcı Ölçüm: Tek Değişkenin Atomikliği

Bu bölümde yeni bir kod koşturulmuyor. `AtomicInteger` bir **tek değişkene** atomiklik verir;
o değişkeni okuyup değiştiren her işlem bölünemez bir bütün olarak görünür. Ama bir program
**iki** atomik değişkeni birlikte, aralarındaki bir ilişkiyi koruyarak güncellemek isterse
(örneğin bir toplamı bir sayaca hep eşit tutmak isterse), bu ilişki `AtomicInteger`'ın
verdiği garantinin dışında kalır — her değişken kendi içinde bölünmez, ikisi birlikte
bölünebilir. Bu, M01/K05 İşletim Sistemi Kavramları kursunda kurulan oku-artır-yaz kavramının bir
başka görünümüdür ve burada da **koşturularak değil, modellenerek** anlatılıyor: iki atomik
değişkenin arasına bir iş parçacığının girip yalnızca birini güncellediği bir an her zaman
mümkündür, ve bu anın gerçekten oluşup oluşmadığını koşumdan göstermek ortama bağlı bir sonuç
üretirdi.

Aynı sınır `CopyOnWriteArrayList` ve `ConcurrentHashMap` için de geçerlidir: her ikisi de
**kendi** iç tutarlılıklarını korur — bir `add` çağrısı hiçbir zaman yarım kalmış bir dizi
bırakmaz, bir `put` çağrısı haritayı hiçbir zaman tutarsız bir ara durumda göstermez — ama bu,
aynı koleksiyon üzerinde çalışan **birden fazla** işlemin birlikte oluşturduğu daha büyük bir
kuralı garanti etmez. Bir listeye önce bir öge ekleyip sonra o ögeye bağlı ikinci bir alanı
başka bir haritaya yazan kod, iki ayrı eşzamanlı koleksiyonu birlikte tutarlı tutma
sorumluluğunu kendi üstlenir; yardımcı tipler yalnız kendi sınırları içindeki bütünlüğü
garanti eder.

## Özet

- `AtomicInteger`'ın karşılaştır-ve-ata döngüsü tamamen çağıranın kod akışında kalır; hiçbir
  yeni iş parçacığı ya da kilit devreye girmez, yalnız okuma-hesapla-dene tekrarlanır.
- `CountDownLatch` ve `Semaphore`'da bekleyen taraf kendi kendini serbest bırakamaz; sayacı
  düşüren ya da izni bırakan her zaman **öteki** taraftır.
- `CopyOnWriteArrayList`'te gezinen taraf hiçbir bedel ödemez, çünkü kendi aldığı diziyi
  hiçbir yazmayla paylaşmaz; yazan taraf her değişiklikte yeni bir dizi üretir.
- `ConcurrentHashMap` gezinme sırasında yapılan bir yazmaya istisna atmaz; yazmayı hangi iş
  parçacığının yaptığı programın kendi kurgusundan bilinir, ama yeni girişin gezinmeye
  yansıyıp yansımayacağı garanti edilmez.
- İki atomik değişkeni birlikte tutarlı tutmak `AtomicInteger`'ın garantisinin dışındadır;
  bu sınır koşturularak değil, kural olarak anlatılır.

## Sonraki Adım

Bu dört yardımcı tip de kilidi elle tutma işini üstlendi — `KarsilastirVeAta` hiç kilit
kullanmadı, `Nobetci` izni ve mandalı elle almaktan kurtardı, `YazarkenKopyalama` ile
`EszamanliHarita` bir kilit olmadan tutarlılık sağladı. Ama görevi **kimin koşturacağını**
hâlâ **çağıran** seçiyor: `Nobetci`'deki A ve B'yi de, `EszamanliHarita`'daki yazanı da
`new Thread` ile elle kuran ve `start()` çağıran çağıranın kendisiydi. Sıradaki konu bu
seçimi bir servise devrediyor ve aynı soruyu ona soruyor: bir görev bir havuza verildiğinde,
onu havuzun kendi iş parçacığı mı koşturuyor, yoksa görevi veren taraf mı — ve bu seçim,
havuzun o anki doluluğuna göre nasıl değişiyor?
