---
title: 'JVM, Eşzamanlılık ve Başarım'
source: 'https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik'
language: tr
updated: '2026-08-23T07:00:56+00:00'
license: 'CC BY-SA 4.0'
lessons: 11
outcomes:
    - 'Java bellek modelinin görünürlük ve sıralama garantilerini açıklar'
    - 'Eşzamanlılık araçlarını doğru soyutlama düzeyinde seçer'
    - 'Çöp toplama davranışını ölçerek yorumlar'
    - 'Bir adımı hangi iş parçacığının koşturduğunu koşturarak saptar'
---

# JVM, Eşzamanlılık ve Başarım

Bir Java programının her adımının yanında bir iş parçacığı vardır ve hangisi olduğu çağrı yazımına bakarak görülmez: kurs sınıf yüklemeden gelecek tabanlı zincirlemeye kadar her düzeneği tek bir soruyla ölçer — bu adımı çağıran mı koşturuyor, programın oluşturduğu bir iş parçacığı mı, yoksa hiç oluşturulmamış bir taraf mı? Bellek alanları, çöp toplama, bellek modeli, senkronizasyon, yürütücüler ve sanal iş parçacıkları aynı çerçeveden geçirilir.

## JVM İçyapısı

Sınıfın yükleyici hiyerarşisinden geçerek çalışma zamanına girmesi ve başlatmayı hangi iş parçacığının koşturduğu, bir değerin iş parçacığına özel mi paylaşılan mı olduğunu belirleyen bellek alanları, erişilebilirliğini yitiren nesnenin temizliğini kimin üstlendiği, sınıf dosyasında bulunmayan optimizasyon kararının hangi veriden ve hangi tarafta doğduğu.

### Sınıf Yükleme

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/sinif-yukleme

Yükleyici hiyerarşisi yetki devriyle çalışır: bir yükleyiciye sorulan sınıfı çoğu zaman üstteki yükleyici tanımlar, kimlik yalnız `==` ile okunur. Dört tetikleyicinin üçü sınıf başlatmasını koşturur, biri (derleme zamanı sabiti okumak) hiç koşturmaz; aynı sınıfa aynı anda dokunan iki iş parçacığından yalnızca biri başlatma bloğunu çalıştırır; öbürü bitmesini bekler.

Standart Kütüphane ve Akışlar kursu son dersini bir garantiyle kapattı: bir davranışın
arkasında artık tek bir iş parçacığı durmuyordu, aynı nesneye aynı anda erişen birden çok
iş parçacığı vardı. Bu kurs o garantinin tam olarak ne olduğunu soracak, ama önce ondan
daha küçük ve yine de yalnız koda bakarak yanıtlanamayan bir soru var: bu satırı hangi iş
parçacığı koşturuyor? Soru, yazdığımız ilk satırdan önce başlıyor. `main` çalışmaya
başlamadan önce bile bir sınıfın çalışma zamanı biçimine girmesi gerekir — yüklenmesi ve
başlatılması — ve bu işi de bir iş parçacığı yapar: sınıfa ilk dokunan iş parçacığı.

Bu ders iki soruyu ayırır. Birincisi: bir sınıf istendiğinde onu hangi yükleyici tanımlar?
İkincisi: başlatma bloğunu hangi iş parçacığı koşturur ve aynı sınıfa aynı anda dokunan
ikinci bir iş parçacığına ne olur?

#### Yükleyici Hiyerarşisi ve Yetki Devri

Bir sınıf istendiğinde JVM onu tek bir yükleyiciye değil bir yükleyici zincirine sorar.
Zincirdeki her yükleyicinin bir üstü (parent) vardır; zincirin en tepesinde JVM'in kök
yükleyicisi durur ve bu yükleyici bir Java nesnesi olarak temsil edilmez — `getClassLoader()`
çağrısı onun için `null` döner. Bir sınıfı isteyen yükleyici (initiating loader) kendi işini
önce üstüne devreder: üstüne sorar, üst de kendi üstüne sorar. Zincirde bir yükleyici sınıfı
gerçekten tanımlayabilirse (`defineClass` çağrısını yapabilirse) o sınıfın tanımlayan
yükleyicisi (defining loader) odur. Bu yetki devri (delegation) ilkesidir ve
`ClassLoader.loadClass` yönteminin varsayılan davranışıdır — hiçbir satır yazmadan, yalnız
bir yükleyicinin üstünü belirterek devralınır.

Yetki devrinin sonucu isteyen ile tanımlayanı ayırabilir: bir yükleyiciye bir sınıf sorulur,
ama sınıfı üstteki bulur ve tanımlarsa isteyen ile tanımlayan aynı nesne değildir. Bu fark
çağrı yazımında görünmez — `alt.loadClass("X")` çağrısı her koşulda aynı satırdır; hangi
yükleyicinin tanımladığı yalnız dönen `Class` nesnesinin `getClassLoader()`'ı `==` ile
karşılaştırılarak görülür.

- **JV1** — Yükleyici kimliği yalnız `==` karşılaştırmasıyla okunur; yükleyicinin adı,
  kimlik karması ya da `toString` çıktısı hiçbir yerde basılmaz.
- **JV2** — Derleyici, standart kitaplığın araç sağlayıcı arayüzünden program içinden
  çağrılır; iki küçük sınıf iki ayrı dizine derlenir ve bu iki dizin dışında hiçbir kaynak
  ölçüme katılmaz.
- **JV3** — Üstteki yükleyicinin bu ölçümde kendi üstü yoktur; zincirin ne kadar
  yükseldiği değil, hangi katmanın tanımlayan olduğu ölçülür.

```java
// Delegeli.java — yetki devriyle calisan yukleyici: findClass override edilir, loadClass devralinir
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

class Delegeli extends ClassLoader {
    private final Path dizin;

    Delegeli(Path dizin, ClassLoader ust) {
        super(ust);
        this.dizin = dizin;
    }

    protected Class<?> findClass(String ad) throws ClassNotFoundException {
        try {
            byte[] bayt = Files.readAllBytes(dizin.resolve(ad + ".class"));
            return defineClass(ad, bayt, 0, bayt.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(ad);
        }
    }
}
```

`Delegeli` yalnız `findClass`'ı yeniden tanımlar; `loadClass`'a hiç dokunulmaz. `Paylasilan`
sınıfının bayt kodu yalnızca üstteki yükleyicinin baktığı dizinde durur, `Ozel`in bayt kodu
yalnızca alttakinin baktığı dizinde. Böylece iki dizin, iki ayrı yükleyicinin görebildiği iki
ayrı kaynağı temsil eder.

```java
// Yukleyiciler.java — isteyen yukleyici ile tanimlayan yukleyici kimlik karsilastirmasiyla ayrilir
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;

public class Yukleyiciler {
    static void derle(String kaynak, String sinif, Path dizin) throws Exception {
        Path d = Files.createTempDirectory("kaynak");
        Path k = d.resolve(sinif + ".java");
        Files.writeString(k, kaynak);
        Files.createDirectories(dizin);
        PrintWriter bos = new PrintWriter(Writer.nullWriter());
        ToolProvider.findFirst("javac").orElseThrow()
                .run(bos, bos, "-d", dizin.toString(), k.toString());
    }

    public static void main(String[] args) throws Exception {
        Path ustDizin = Files.createTempDirectory("ust");
        Path altDizin = Files.createTempDirectory("alt");
        derle("class Paylasilan { static int deger() { return 1; } }", "Paylasilan", ustDizin);
        derle("class Ozel { static int deger() { return 2; } }", "Ozel", altDizin);

        ClassLoader ust = new Delegeli(ustDizin, null);
        ClassLoader alt = new Delegeli(altDizin, ust);

        Class<?> c1 = alt.loadClass("Paylasilan");
        Class<?> c2 = alt.loadClass("Ozel");

        System.out.println("Paylasilan sinifini tanimlayan : "
                + (c1.getClassLoader() == ust ? "ustteki" : c1.getClassLoader() == alt ? "kendisi" : "baska"));
        System.out.println("Ozel sinifini tanimlayan       : "
                + (c2.getClassLoader() == alt ? "kendisi" : c2.getClassLoader() == ust ? "ustteki" : "baska"));
        System.out.println("kok yukleyici temsili (null)   : " + (Object.class.getClassLoader() == null));
    }
}
```

```
Paylasilan sinifini tanimlayan : ustteki
Ozel sinifini tanimlayan       : kendisi
kok yukleyici temsili (null)   : true
```

Aynı `alt.loadClass(...)` çağrısı iki kez yapıldı ve iki ayrı yanıt geldi. `Paylasilan`
alt yükleyiciye soruldu, ama bulan üstteki yükleyiciydi — yetki devri üstte durdu ve
tanımlayan `alt` değil `ust` çıktı. `Ozel` üstteki yükleyicinin baktığı dizinde yoktu; devir
başarısız olunca istek alta geri döndü ve tanımlayan bu kez `alt`'ın kendisi oldu. Aradaki
tek fark, ikinci sınıfın bayt kodunun üstte hiç bulunmamasıydı — kodda hiçbir dallanma bunu
söylemiyor. `Object.class.getClassLoader()` çağrısı `null` verdi: zincirin en üstünde,
hiçbir Java nesnesiyle temsil edilmeyen kök yükleyici duruyor.

#### Sınıf Başlatmasını Hangi İş Parçacığı Koşturuyor

Yükleme (bir sınıf dosyasının bulunup çalışma zamanı temsilinin kurulması) ile başlatma
(statik alanlara ilk değerlerin verilmesi ve başlatma bloğunun çalıştırılması) iki ayrı
olaydır ve iki ayrı anda olabilir. JVM bir sınıfı istendiği anda değil, gerçekten
kullanılacağı anda başlatır ve tam olarak bir kez başlatır. İki iş parçacığı aynı sınıfa
aynı anda dokunursa biri başlatma bloğunu koşturur; öbürü bitene kadar bekler — sıra
çizelgeleyiciye bırakılmaz, bu bir dil garantisidir ve JVM içinde örtük bir kilitle sağlanır.

Hangi eylemlerin bir sınıfı gerçekten başlattığı da çağrı yazımına bakarak görünmez: dört
aday eylemden üçü başlatmayı tetikler, biri hiç tetiklemez.

- **JV4** — Bir sınıfın başlatılıp başlatılmadığı kendi kurduğumuz sayaçla izlenir; sayaç
  yalnız başlatma bloğunun gövdesi gerçekten çalıştığında artar, sınıfın yüklenmesiyle
  artmaz.
- **JV5** — Dört tetikleyici dört ayrı sınıf üzerinde denenir, çünkü bir sınıf en çok bir
  kez başlar; aynı sınıfı iki tetikleyiciyle sınamak ikinci denemeyi anlamsız kılar.
- **JV6** — İki iş parçacığının aynı sınıfa aynı anda dokunması iki `CountDownLatch` ile
  kurgulanır: biri ikisinin de hazır olduğunu, öbürü ikisinin de aynı anda serbest
  bırakıldığını garanti eder; aradaki sıra çizelgeleyiciye bırakılmaz.
- **JV7** — Yarışan iki iş parçacığı da aynı etiketle ("oluşturulan") kayıtlıdır; hangisinin
  kazandığı bu ölçümün konusu değildir, kazananın kayıtta olup olmadığı konudur.

```java
// Bas.java — sinif baslatmasini hangi is parcacigi kosturuyor: kendi kaydimizla izlenir
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

class Bas {
    static final AtomicInteger sayac = new AtomicInteger(0);
    static final List<String> gorulen = Collections.synchronizedList(new ArrayList<>());
    static final Map<Thread, String> etiketler = Collections.synchronizedMap(new IdentityHashMap<>());

    static void tanit(Thread t, String etiket) { etiketler.put(t, etiket); }

    static String kim() {
        String e = etiketler.get(Thread.currentThread());
        return e == null ? "bilinmeyen" : e;
    }

    static void gozle(String tetikleyici) {
        sayac.incrementAndGet();
        gorulen.add(tetikleyici);
        System.out.printf("%-30s baslatici kosuyor: %s%n", tetikleyici, kim());
    }
}
```

Dört tetikleyici dört ayrı sınıfa yazılır; her sınıfın başlatma bloğu yalnız `Bas.gozle`'yi
çağırır.

```java
// YeniOrnekTetigi.java — yeni ornek olusturma baslatmayi tetikler mi
class YeniOrnekTetigi { static { Bas.gozle("yeni ornek olusturma"); } }
```

```java
// StatikYontemTetigi.java — statik yontem cagirma baslatmayi tetikler mi
class StatikYontemTetigi { static { Bas.gozle("statik yontem cagirma"); } static void yontem() { } }
```

```java
// StatikAlanTetigi.java — final olmayan statik alan okuma baslatmayi tetikler mi
class StatikAlanTetigi { static { Bas.gozle("statik alan okuma"); } static int deger = 5; }
```

```java
// SabitTetigi.java — derleme zamani sabiti okuma baslatmayi tetikler mi
class SabitTetigi { static { Bas.gozle("derleme zamani sabiti okuma"); } static final int SABIT = 7; }
```

Beşinci sınıf iki ölçümde birden kullanılır: önce yüklenip başlatılmadan durabildiğini, sonra
iki iş parçacığının aynı anda dokunduğunda başlatmanın tam bir kez çalıştığını gösterir.

```java
// GecBaslatilan.java — once yuklenip baslatilmadan durabilen, sonra iki iplik tarafindan es zamanli dokunulan sinif
class GecBaslatilan { static { Bas.gozle("gec baslatilan sinif"); } static int deger = 9; }
```

```java
// Tetikleyiciler.java — dort tetikleyicinin kaci baslatmayi kosturuyor, yukleme baslatmadan ayri mi, es zamanli dokunma ne veriyor
import java.util.concurrent.CountDownLatch;

public class Tetikleyiciler {
    public static void main(String[] args) throws Exception {
        Bas.tanit(Thread.currentThread(), "cagiran");

        System.out.println("-- dort tetikleyici --");
        new YeniOrnekTetigi();
        StatikYontemTetigi.yontem();
        int a = StatikAlanTetigi.deger;
        int s = SabitTetigi.SABIT;
        System.out.println("okunan sabit deger                      : " + s);
        System.out.println("yeni ornek olusturma baslatti mi         : " + Bas.gorulen.contains("yeni ornek olusturma"));
        System.out.println("statik yontem cagirma baslatti mi        : " + Bas.gorulen.contains("statik yontem cagirma"));
        System.out.println("statik alan okuma baslatti mi            : " + Bas.gorulen.contains("statik alan okuma"));
        System.out.println("derleme zamani sabiti okuma baslatti mi  : " + Bas.gorulen.contains("derleme zamani sabiti okuma"));
        System.out.println("dort tetikleyicinin kosturdugu baslatici : " + Bas.sayac.get());

        System.out.println("-- yukle, baslatma; sonra iki iplik es zamanli dokunsun --");
        ClassLoader yukleyici = Tetikleyiciler.class.getClassLoader();
        Class<?> yuklenen = Class.forName("GecBaslatilan", false, yukleyici);
        System.out.println("yalnizca yuklendi, baslatici kosan sayisi: " + Bas.sayac.get());

        CountDownLatch hazir = new CountDownLatch(2);
        CountDownLatch baslasin = new CountDownLatch(1);
        Runnable govde = () -> {
            hazir.countDown();
            try { baslasin.await(); } catch (InterruptedException e) { }
            int x = GecBaslatilan.deger;
        };
        Thread ilk = new Thread(govde);
        Thread ikinci = new Thread(govde);
        Bas.tanit(ilk, "olusturulan");
        Bas.tanit(ikinci, "olusturulan");
        ilk.start();
        ikinci.start();
        hazir.await();
        baslasin.countDown();
        ilk.join();
        ikinci.join();
        System.out.println("iki iplik dokundukten sonra Class ayni mi: " + (yuklenen == GecBaslatilan.class));
        System.out.println("baslatici toplam kac kez kostu           : " + Bas.sayac.get());
    }
}
```

```
-- dort tetikleyici --
yeni ornek olusturma           baslatici kosuyor: cagiran
statik yontem cagirma          baslatici kosuyor: cagiran
statik alan okuma              baslatici kosuyor: cagiran
okunan sabit deger                      : 7
yeni ornek olusturma baslatti mi         : true
statik yontem cagirma baslatti mi        : true
statik alan okuma baslatti mi            : true
derleme zamani sabiti okuma baslatti mi  : false
dort tetikleyicinin kosturdugu baslatici : 3
-- yukle, baslatma; sonra iki iplik es zamanli dokunsun --
yalnizca yuklendi, baslatici kosan sayisi: 3
gec baslatilan sinif           baslatici kosuyor: olusturulan
iki iplik dokundukten sonra Class ayni mi: true
baslatici toplam kac kez kostu           : 4
```

Dört adaydan üçü başlatmayı gerçekten koşturdu: yeni örnek oluşturmak, statik yöntem
çağırmak, final olmayan bir statik alanı okumak. Dördüncüsü — derleme zamanı sabitini
okumak — hiçbir satır bırakmadı; `SabitTetigi` sınıfının başlatma bloğu hiç çalışmadı, buna
karşılık okunan değer (`7`) doğru geldi. Bu satır aşağıda ayrı bir bölümde ele alınıyor.

İkinci blok yükleme ile başlatmayı ayırıyor. `Class.forName("GecBaslatilan", false, ...)`
sınıfı yükledi — bir `Class` nesnesi döndü — ama sayaç `3`'te kaldı: başlatma bloğu
çalışmadı. Yükleme, başlatmadan önce ve ondan bağımsız olarak tamamlanabiliyor.

Üçüncü blok aynı sınıfa iki iş parçacığını aynı anda gönderiyor. `hazir` mandalı ikisinin de
başlama çizgisinde olduğunu, `baslasin` mandalı ikisinin de aynı anda serbest bırakıldığını
garanti ediyor; hangisinin `GecBaslatilan.deger` okumasına önce ulaşacağı ise çizelgeleyiciye
bağlı ve bilinmiyor. Buna karşılık sonuç belirlenimci: başlatıcı **toplamda bir kez** koştu
(sayaç `3`'ten `4`'e çıktı, ikiye değil) ve koşturan taraf kayıtlı iki iş parçacığından
biriydi — `bilinmeyen` değil. İkinci iş parçacığı da `GecBaslatilan.deger`'i okudu, ama
kendi başlatma bloğunu çalıştırmadı; JVM'in örtük kilidine takılıp birincinin bitmesini
bekledi. Hangi iş parçacığının kazandığı koşumdan koşuma değişebilir, ama kazananın kayıtta
olduğu ve başlatmanın tam bir kez çalıştığı değişmiyor — bu yüzden ders iki iş parçacığını
da aynı etiketle kaydetti (JV7).

#### Sınırlayıcı Ölçüm: Derleme Zamanı Sabiti Sınıfı Hiç Başlatmaz

Yukarıdaki tabloda dördüncü satır boştu: `SabitTetigi` sınıfının başlatma bloğu hiç çalışmadı
ve `Bas.gozle` hiç çağrılmadı. Nedeni `SABIT` alanının bildirim biçiminde: `static final int
SABIT = 7;` bir derleme zamanı sabitidir ve derleyici böyle bir alanı okuyan her yeri
sabitin değeriyle değiştirir — Nesneye Dayalı Java kursunun ölçtüğü **değer gömme**. `int s =
SabitTetigi.SABIT;` satırı sınıf dosyasında `SabitTetigi`'ye ait hiçbir iz bırakmaz;
`7` değeri doğrudan çağrı yerine gömülür. Sınıfa hiç dokunulmadığı için başlatılacak bir şey
de yoktur.

Bu, ortak ölçüm çerçevesinin kendi kör noktasının bu derste yeniden kurulmuş hâlidir: boş bir
satır "başlatma olmadı" demek değildir, "tetikleyici hiç oluşmadı" demektir. Bir ölçüm yalnız
beklenen satırın yokluğuna bakarak "düzenek çalışmıyor" sonucuna varamaz; önce o satırı
üretecek eylemin gerçekten gerçekleştiğini doğrulamak zorundadır. Burada eylem (bir alanı
okumak) gerçekleşti, ama okunan şey sınıfa değil, derleyicinin sınıfın yerine koyduğu sayıya
gitti. Gözlemin yokluğu, düzeneğin yokluğu değildir.

`StatikAlanTetigi` ile `SabitTetigi` arasındaki tek fark bir `final` sözcüğüdür; ikisi de
`int` türünde bir alan tutar, ikisi de aynı tetikleyici satırla okunur. Buna karşılık biri
sınıfını başlatırken öbürü hiç dokunmaz — kararı veren şey alanın türü ya da değeri değil,
değerinin derleme zamanında bilinip bilinmediğidir. Bu ayrım, Java Temelleri kursunun ilk
dersinde ölçülen sabit katlamayla aynı kaynaktan gelir: derleyici, sonucu önceden
hesaplayabildiği her yerde çalışma zamanına hiçbir iz bırakmaz.

#### Özet

- Yükleyici hiyerarşisi yetki devriyle çalışır: bir sınıfı isteyen yükleyici önce üstüne
  sorar, sınıfı üstteki bulursa tanımlayan üstteki olur; kimlik yalnız `==` ile okunur, ad
  ve sayı basılmaz.
- Kök yükleyici bir Java nesnesi olarak temsil edilmez; `getClassLoader()` onun için `null`
  döner.
- Yükleme ile başlatma iki ayrı olaydır: bir sınıf, başlatma bloğu hiç çalışmadan
  yüklenebilir.
- Dört tetikleyicinin üçü (yeni örnek, statik yöntem çağrısı, final olmayan statik alan
  okuma) sınıf başlatmasını koşturur; derleme zamanı sabitini okumak koşturmaz, çünkü değer
  çağrı yerine gömülür ve sınıfa hiç dokunulmaz.
- Aynı sınıfa aynı anda dokunan iki iş parçacığından yalnızca biri başlatma bloğunu çalıştırır;
  öbürü JVM'in örtük kilidine takılıp bitmesini bekler. Kazanan taraf koşumdan koşuma
  değişebilir, ama her zaman kayıtlı bir iş parçacığıdır ve başlatma toplamda bir kez koşar.
- Bir ölçümün beklediği satır çıktıda yoksa, önce o satırı üretecek eylemin gerçekten
  tetiklenip tetiklenmediği doğrulanır; boş satır "olmadı" değil "tetiklenmedi" anlamına
  gelebilir.

#### Sonraki Adım

Bu derste sınıfın çalışma zamanına kimin elinde girdiği ölçüldü: hangi yükleyicinin
tanımladığı, hangi iş parçacığının başlattığı. Ama başlatılan sınıfın alanları nerede
durduğu ve bir iş parçacığının yazdığı değeri başka bir iş parçacığının görüp göremediği
hiç sorulmadı — `GecBaslatilan.deger` okundu, ama kimin yazdığı değeri gördüğü hiç
sınanmadı. Sıradaki ders bunu ölçer: yerel değişken, statik alan, nesne alanı ve iş
parçacığına özel bir değer dört ayrı yerde durur ve bunlardan yalnızca bir kısmı yazan iş
parçacığının dışına görünür.

### Bellek Alanları

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/bellek-alanlari

Aynı iş parçacığının yazdığı altı değerden üçü başka bir iş parçacığına hiç görünmez, üçü görünür — ayrımı belirleyen değerin türü değil durduğu yerdir: yerel değişken ve parametre çağrı çerçevesinde, nesne alanı ve dizi ögesi öbekte, statik alan sınıfa ait alanda durur. Paylaşılan olmak görülecek olmak değildir: bir sıralama aracı olmadan aynı okuma tanımsızdır.

Önceki derste sınıfın çalışma zamanına kimin elinde girdiği ölçüldü: hangi yükleyicinin
tanımladığı, hangi iş parçacığının başlattığı. Ama başlatılan sınıfın alanları nerede
durduğu ve bir iş parçacığının yazdığı değeri başka bir iş parçacığının görüp göremediği
hiç sorulmadı. Bu ders o soruyu sorar: bir değeri kaç iş parçacığı görebiliyor?

Yanıt değerin türüne bakılarak verilemez — `int` de olsa aynı `int`, bir yerde yazan iş
parçacığının dışına hiç çıkmaz, başka bir yerde her iş parçacığına açık durur. Ayrımı yapan
şey değerin **durduğu yer**dir.

Bu ayrımın önemi, iki iş parçacığının aynı yöntemi aynı anda çağırdığı sıradan bir durumda
görülür. İki iş parçacığı `parametreYaz(5, ...)` yöntemini tam olarak aynı anda çağırsa
bile, ikisi de kendi `p` parametresini kendi çerçevesinde tutar; biri ötekinin `p`'sini
hiçbir şekilde okuyamaz, çünkü ikisinin çerçevesi ayrı adres uzayında değil ama ayrı
**çerçeve zincirinde** durur. Bu, programcının bir şey yapmasıyla kurulan bir izolasyon
değildir — dilin kendisi tarafından, hiçbir kilide ve hiçbir anahtar sözcüğe gerek
kalmadan sağlanır. Öbek paylaşımı ise tam tersi yönde çalışır: bir nesnenin öbekte
paylaşılması, o nesnenin referansının başka bir iş parçacığına **verilmiş olmasına**
bağlıdır; referans hiç paylaşılmazsa öbekteki bir nesne de yalnız tek bir iş parçacığına
görünür kalabilir. Çağrı çerçevesi izolasyonu yapısaldır, öbek paylaşımı ise bir tasarım
kararıdır.

#### Üç Yer: Çağrı Çerçevesi, Öbek ve Sınıfa Ait Alan

Bu konunun kataloğundaki alt başlık bellek alanlarını "yığın, yığıt ve meta alan" diye anar.
Bu platformda **yığın** bir veri yapısının (öncelik ağacının) adıdır ve bellek alanı için
kullanılmaz; düzyazı burada ve bundan sonra hep **öbek** yazar. Üç alanın üçü de JVM
belirtiminin kendi çalışma zamanı veri alanları arasında adı geçen alanlardır: her iş
parçacığının kendi yığıtı, bütün iş parçacıklarının paylaştığı tek bir öbek ve yine bütün
iş parçacıkları arasında paylaşılan tek bir yöntem alanı. Belirtim bu üç alanın nasıl
gerçekleştirileceğini söylemez — yalnız hangi verinin hangi alana ait olduğunu söyler; bu
ders de yalnız o ayrıma bakar, gerçekleştirime bakmaz.

Çalışma zamanı bir değeri üç ayrı alandan birine koyar. Bir yöntem çağrıldığında, o iş
parçacığının kendi **çağrı çerçevesi** açılır ve yöntemin yerel değişkenleri ile
parametreleri oraya yazılır; her iş parçacığının kendi çerçeve zinciri vardır, biri
ötekininkine erişemez. Çerçeve, çağrı bittiğinde de kapanır: bir yöntem özyinelemeli
çağrılırsa aynı iş parçacığında aynı yöntemin birden çok çerçevesi üst üste durur, ama
hiçbiri bir başka iş parçacığının çerçeve zincirine karışmaz. Bir nesne `new` ile
üretildiğinde alanları ve bir dizi üretildiğinde ögeleri **öbeğe** yazılır; öbek bütün iş
parçacıklarının paylaştığı tek bir alandır ve bir nesneye giden referans hangi iş
parçacığına aktarılırsa o iş parçacığı da aynı alanlara ulaşır. Bir sınıfın kendi statik
alanları ise örneklerden ayrı, sınıfın kendisine ait bir alanda durur — JVM belirtiminin
**yöntem alanı**, güncel adıyla **meta alan**; bu alan da bütün iş parçacıkları arasında
tektir ve paylaşılır. Statik bir alanın var olması için sınıftan tek bir örnek bile
üretilmiş olması gerekmez — meta alandaki yer sınıf hazırlanırken, daha ilklendirme hiç
çalışmadan açılır ve örnek sayısından bağımsızdır.

`ThreadLocal` dördüncü bir örüntüdür: tuttuğu değer fiziksel olarak öbekte durur, ama her iş
parçacığı kendi kaydına erişir — erişim yolu iş parçacığının kendisinden geçer. Bu yüzden
sınıf olarak öbekte olsa bile davranış olarak çağrı çerçevesindeki bir değere benzer. Ama
ömürleri aynı değildir: yerel değişken kendi çerçevesi kapanınca yok olur, bir sonraki
çağrının hiçbir izini taşımaz. `ThreadLocal` kaydı ise iş parçacığı sürdüğü sürece kalır —
aynı iş parçacığı aynı yöntemi ikinci kez çağırdığında kaydı hâlâ oradadır, yeniden
yazılmadıkça bir önceki çağrıdan kalan değeri okur. "Yalnız" sütununa ikisi de girer, çünkü
ikisi de başka bir iş parçacığına görünmez; ama biri çağrıyla doğup çağrıyla ölür; öbürü
iş parçacığıyla birlikte yaşar.

Dizi ögesinin öbekte durması da aynı kuraldan gelir: Java Temelleri kursunun kurduğu gibi
dizi kendisi bir nesnedir, bu yüzden bir dizinin ögeleri de tıpkı bir nesnenin alanları gibi
öbekte durur ve aynı referans hangi iş parçacığına ulaşırsa o iş parçacığı da aynı ögelere
erişir.

- **JV8** — Bir değerin "paylaşılan" sayılması, yazan iş parçacığının bitmesi
  **beklendikten sonra** okunan bir değerle belirlenir (`join` ile); bitmeden okunan bir
  değer bu ölçüme girmez.
- **JV9** — Yerel değişken ve yöntem parametresi için "yalnız" sonucu bir gözlemden değil
  bir imkânsızlıktan gelir: başka bir iş parçacığının bu iki değeri okuyacağı hiçbir adres
  yoktur, dolayısıyla denenecek bir çağrı bile yazılamaz.
- **JV10** — Ölçülen altı değer aynı yazan iş parçacığı tarafından, aynı koşumda, aynı
  sırayla yazılır; hiçbiri ayrı bir koşumdan gelmez.
- **JV11** — Ortama bağlı hiçbir veri okunmaz: yığıt derinliği, öbek boyutu, adres
  yazılmaz; kimlik yalnız değer eşitliğiyle (`== 5`) okunur.

#### Altı Değerin Üçü Yalnız, Üçü Paylaşılan

Ölçüm bir nesne yazar (`alan` ve `dizi` öbekte), bir sınıf statik alanı yazar (meta alanda),
bir `ThreadLocal` yazar ve aynı iş parçacığının yerel bir değişkeni ile bir yöntem
parametresini yazar. Hepsi tek bir yazan iş parçacığında olur; okuma o iş parçacığı
bittikten sonra, ana iş parçacığından yapılır.

```java
// GorunurlukKutusu.java — bir nesne alani ve bir dizi ogesi tasir
class GorunurlukKutusu {
    int alan = 0;
    final int[] dizi = { 0 };
}
```

```java
// GorunenDegerler.java — dort yerdeki alti deger: kaci yazan is parciginin disina goruyor
import java.util.concurrent.CountDownLatch;

public class GorunenDegerler {
    static int statikAlan = 0;
    static final ThreadLocal<Integer> ozel = ThreadLocal.withInitial(() -> 0);
    static int yalniz = 0, paylasilan = 0;

    static void parametreYaz(int p, int[] hedef) { hedef[0] = p; }

    static void goren(String etiket, boolean paylasildi) {
        if (paylasildi) paylasilan++; else yalniz++;
        System.out.printf("%-26s %s%n", etiket, paylasildi ? "paylasilan" : "yalniz");
    }

    public static void main(String[] args) throws InterruptedException {
        GorunurlukKutusu kutu = new GorunurlukKutusu();
        CountDownLatch yazildi = new CountDownLatch(1);
        int[] parametreGorulen = { -1 };

        Thread yazan = new Thread(() -> {
            int yerel = 5;
            ozel.set(5);
            statikAlan = 5;
            kutu.alan = 5;
            kutu.dizi[0] = 5;
            parametreYaz(5, parametreGorulen);
            if (yerel != 5) throw new IllegalStateException();
            yazildi.countDown();
        });
        yazan.start();
        yazildi.await();
        yazan.join();

        // yerel degisken ve parametre: baska bir is parciginin bunlari okuyacagi hicbir
        // adres yok, dolayisiyla denenecek bir cagri bile yazilamaz.
        goren("yerel degisken", false);
        goren("yontem parametresi", false);
        goren("is parcacigina ozel deger", ozel.get() == 5);
        goren("nesne alani", kutu.alan == 5);
        goren("dizi ogesi", kutu.dizi[0] == 5);
        goren("statik alan", statikAlan == 5);
        System.out.printf("gorunurluk: %d yalniz, %d paylasilan%n", yalniz, paylasilan);
    }
}
```

```
yerel degisken             yalniz
yontem parametresi         yalniz
is parcacigina ozel deger  yalniz
nesne alani                paylasilan
dizi ogesi                 paylasilan
statik alan                paylasilan
gorunurluk: 3 yalniz, 3 paylasilan
```

Altı değerin **üçü** yazan iş parçacığının dışına hiç çıkmadı, **üçü** çıktı. İlk iki satır
hiç koşum gerektirmedi: `yerel` ve `parametreYaz`'ın parametresi yazan iş parçacığının kendi
çerçevesinde yaşar ve öldüğünde çerçeveyle birlikte yok olur; ana iş parçacığının bunları
okuyacağı bir değişken adı, bir alan, bir yöntem — hiçbiri yoktur. Koddaki
`if (yerel != 5) throw` satırı bunu doğrular: `yerel`in kendisi yalnız kendi çerçevesi
içinde, bir doğrulama olarak kullanılabilir; çerçeve dışına hiçbir biçimde aktarılmaz.
`parametreYaz` çağrısı ise parametre değerini `parametreGorulen` adlı paylaşılan bir diziye
**kopyalayarak** ana iş parçacığına taşıdı — ama taşınan şey parametrenin kendisi değil,
kopyasıdır; ölçüm bu kopyayı hiç okumadı, çünkü sorulan soru "değer başka bir yere
kopyalanabilir mi" değil "parametrenin durduğu yer başka bir iş parçacığına görünür mü"
sorusudur ve yanıtı hayırdır.

Üçüncü satır koşum gerektirdi ve sonucu da aynı yöndeydi: `ozel.get()` ana iş parçacığında
çağrıldığında kendi `ThreadLocal` kaydını okur, yazan iş parçacığının kaydını değil; iki iş
parçacığının aynı adı taşıyan iki ayrı değeri vardır ve `5 == 0` yanlış çıkar. Son üç satır
ise `kutu.alan`, `kutu.dizi[0]` ve `statikAlan`'ın hepsinde `true` verdi: nesne öbekte tek
bir yerde durur, iki iş parçacığı da aynı referansı taşır; statik alan sınıfın kendi
alanında tek bir yerde durur, iki iş parçacığı da aynı sınıfa bakar.

#### Nesneye Ait Alan ile Sınıfa Ait Alan Arasındaki Fark

Bir önceki ölçüm tek bir `GorunurlukKutusu` nesnesi kullandı; "öbekte tek bir yerde durmak"
ile "sınıfa ait alanda tek bir yerde durmak" aynı şey gibi görünebilir. Aradaki fark iki
nesne üretilince ortaya çıkar.

```java
// IkiOrnek.java — nesneye ait alan her ornekte ayri, sinifa ait alan tek bir yerde
public class IkiOrnek {
    public static void main(String[] args) {
        GorunurlukKutusu birinci = new GorunurlukKutusu();
        GorunurlukKutusu ikinci = new GorunurlukKutusu();
        birinci.alan = 1;
        ikinci.alan = 2;
        GorunenDegerler.statikAlan = 99;

        System.out.println("birinci.alan                : " + birinci.alan);
        System.out.println("ikinci.alan                 : " + ikinci.alan);
        System.out.println("iki nesnenin alani ayni mi  : " + (birinci.alan == ikinci.alan));
        System.out.println("iki nesne var, statikAlan yine de tek: " + GorunenDegerler.statikAlan);
    }
}
```

```
birinci.alan                : 1
ikinci.alan                 : 2
iki nesnenin alani ayni mi  : false
iki nesne var, statikAlan yine de tek: 99
```

İki `GorunurlukKutusu` nesnesi üretildiğinde `alan` alanından da iki ayrı kopya çıktı; biri
`1`, öbürü `2` tuttu ve ikisi eşit değil. `alan`, öbekte nesne başına bir yer kaplar — kaç
nesne üretilirse o kadar kopyası olur. `statikAlan` ise `GorunenDegerler` sınıfının kendi
alanıdır ve nesne sayısından bağımsızdır: sıfır nesne üretilse de var olurdu, iki nesne
üretilince de tek kalır. Önceki ölçümde "paylaşılan" sayılan üç satırın ikisi (nesne alanı,
dizi ögesi) paylaşılmayı **aynı nesneye referansı taşımaktan** alıyordu; üçüncüsü (statik
alan) paylaşılmayı **sınıfın kendisinden** alıyordu. İkisi de öbek ya da meta alanda tek bir
yerde durduğu için paylaşılıyor, ama "tek yer" ikisinde de farklı bir şeye bağlı: birinde
nesnenin kimliğine, öbüründe sınıfın kimliğine.

Bu fark yeni bir sınıf tasarlarken doğrudan bir sonuç doğurur: bir alanı bilerek paylaşılan
yapmak isteyen kod onu `static` yazar — o zaman paylaşım nesne sayısına bakmaksızın
kurulmuş olur; bir alanı yalnız kendi nesnesine bağlı tutmak isteyen kod ise onu örnek
alanı olarak bırakır ve nesneyi hiçbir zaman ikinci bir iş parçacığına aktarmaz. İki durumda
da JVM'in kendisi bir seçim yapmaz; alanın `static` olup olmadığı ve referansın paylaşılıp
paylaşılmadığı, ikisi de kaynak kodda yazılı bir karardır.

#### Sınırlayıcı Ölçüm: Paylaşılan Olmak Görülecek Olmak Değildir

Yukarıdaki üç `paylasilan` satırı bir koşullu okumayla elde edildi: `yazan.join()`
çağrısı, yazma ile okuma arasına bir sıra koydu ve okuma yalnız yazma kesin bitince yapıldı.
Bu sıra olmasaydı — örneğin ana iş parçacığı `yazan.start()`'tan hemen sonra, `join`
çağırmadan `statikAlan`'ı okusaydı — aynı üç satırın ne vereceği **tanımsızdır**: yazan iş
parçacığının henüz yazmadığı eski değer (`0`) de görülebilir, yeni değer (`5`) de görülebilir.
Tek bir `int` yazması Java'da atomiktir, yani okunan değer yarı yazılmış bir çöp olmaz; asıl
sorun atomiklik değil **sıralamadır** — derleyicinin ve işlemcinin, gözlemlenebilir tek iş
parçacıklı davranışı bozmadığı sürece komutları yeniden sıralamasına izin verilir ve bu
yeniden sıralama iki iş parçacığı arasında hangi yazmanın hangi okumadan önce sayılacağını
belirsiz bırakabilir. Bu, ölçülüp raporlanacak bir sayı değil, dilin **tanımsız** bıraktığı
bir durumdur; bu yüzden ders böyle bir okumayı çalıştırmaz, yalnız var olduğunu adlandırır —
bu kursun kuralı bir yarış koşulunu koşturmak değil, modellemektir.

Statik alan "paylaşılan" olarak sayıldı çünkü öbekte tek bir yerde duruyor ve iki iş
parçacığı da aynı adrese bakıyor — ama tek bir yerde durmak, o yerdeki en güncel değerin
**görüleceğinin** garantisi değildir. "Aynı yerde durmak" bir konum olgusudur ve derleme
zamanında bellidir; "görünür olmak" bir sıralama olgusudur ve yalnız çalışma zamanında,
aradaki sıralama aracına bağlı olarak kurulur. Görünürlüğün hangi kurala bağlı olduğu —
hangi yazmanın hangi okumadan önce sayılacağı, `volatile` ve kilit gibi araçların bu sırayı
nasıl garantilediği — Eşzamanlılık konusunun Java Bellek Modeli dersinin konusudur; bu ders
yalnız görünürlüğün **sıfırdan** kurulması gerektiğini, `join` gibi bir sıralama aracı
olmadan sıfır olduğunu gösterir.

Yığıt derinliği ve öbek boyutu bu derste ölçülmez: ikisi de çalıştığı makineye göre değişen
ortama bağlı verilerdir. Ölçülen tek şey bir değerin **kaç iş parçacığına** göründüğüdür —
konum bilgisi (dört yerden hangisi) derleme zamanında bellidir ve koşumdan koşuma değişmez;
görünürlük bilgisi (o konumdaki en güncel değerin okunup okunamayacağı) ise ancak doğru bir
sıralama aracıyla birlikte anlamlıdır ve bu ders onu yalnız aracın **var olduğu** durumda
ölçer.

#### Özet

- Bir değer üç yerden birinde durur: yerel değişken ve parametre çağrı çerçevesinde,
  nesne alanı ve dizi ögesi öbekte, statik alan sınıfa ait alanda (meta alanda);
  `ThreadLocal` öbekte durur ama erişimi iş parçacığının kendisinden geçer.
- Bu depoda **yığın** bir veri yapısının adıdır, bellek alanı için **öbek** kullanılır.
- Altı ölçülen değerin üçü (yerel değişken, parametre, iş parçacığına özel değer) yazan iş
  parçacığının dışına hiç çıkmaz, üçü (nesne alanı, dizi ögesi, statik alan) çıkar.
- Yerel değişken ve parametre için "yalnız" sonucu bir gözlem değil bir imkânsızlıktır:
  başka bir iş parçacığının onları okuyacağı bir adres yoktur.
- Paylaşılan olmak görülecek olmak değildir: `join` gibi bir sıralama aracı olmadan aynı
  okumanın sonucu tanımsızdır; bu yüzden ders sırasız bir okumayı hiç çalıştırmaz.
- Yığıt derinliği ve öbek boyutu ortama bağlıdır ve ölçülmez.

#### Sonraki Adım

Bu derste öbekte duran değerlerin hâlâ **erişilebilir** olduğu varsayıldı — `kutu` değişkeni
hep elde tutuldu, hiçbir referans bırakılmadı, `birinci` ve `ikinci` de `main`in kendi
çerçevesinde son satıra kadar canlı kaldı. Ama bir nesneye giden son referans de
kaybolabilir; program bir nesneyi bırakır, o nesneye artık hiçbir çerçeveden, hiçbir statik
alandan ulaşılamaz hâle gelir ve öbekte artık kimsenin okuyamadığı bir alan kalır. Sıradaki
ders bu durumu ölçer: erişilebilirliğini yitiren bir nesnenin temizliğini hangi iş parçacığı
üstleniyor — ne çağıran, ne programın oluşturduğu bir iş parçacığı, kayıtta hiç olmayan bir
taraf.

### Çöp Toplayıcılar

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/cop-toplayicilar

Erişilebilirliğini yitiren bir nesnenin temizliğini kim üstleniyor: ne çağıran, ne programın oluşturduğu bir iş parçacığı — kayıtta hiç olmayan bir taraf. Güçlü referans dururken temizleme koşmaz, kaldırılınca koşar; zayıf ve hayalet referans birkaç istekte kuyruğa düşer; yumuşak referans bellek baskısı yokken direnir. `System.gc()` bir istektir, buyruk değil.

Önceki derste öbekte duran değerlerin hâlâ erişilebilir olduğu varsayıldı. Ama bir nesneye
giden son referans de kaybolabilir; program bir nesneyi bırakır, ona artık hiçbir çerçeveden
hiçbir statik alandan ulaşılamaz ve öbekte kimsenin okuyamadığı bir alan kalır. Bu ders o
alanın temizliğini kimin üstlendiğini sorar.

İzlemeli çöp toplamanın kendisi — kök kümesi, işaretle ve süpür, nesil temelli toplama —
İşletim Sistemi Kavramları kursunda kavram olarak kuruldu ve burada tekrarlanmaz. Bu ders
kuramı değil, **sonucu** ölçer: erişilebilirliğini yitiren bir nesnenin temizliğini hangi iş
parçacığı koşturuyor ve bu iş bir referansın gücüne göre nasıl değişiyor?

Sorunun kendisi programın koduna bakılarak yanıtlanamaz. Bir nesneyi kim ürettiğimiz,
alanlarına ne yazdığımız, referansını nereye taşıdığımız — hepsi kaynakta görünür. Ama o
nesne artık erişilemez hâle geldiğinde ne olacağı, hangi iş parçacığının onunla ilgileneceği
kaynakta hiç yazılı değildir; program yalnız bir öneride bulunabilir, kararı ne verdiğini ne
de ne zaman verdiğini görebilir.

#### Erişilebilirliğini Yitiren Nesnenin Temizliğini Kim Üstleniyor

"Erişilebilirlik" önceki dersin kurduğu dört yerle doğrudan bağlantılıdır. Bir nesne, ona
giden bir yol varsa erişilebilirdir: bu yol bir çağrı çerçevesindeki bir yerel değişkenden,
bir sınıfın statik alanından, ya da erişilebilir başka bir nesnenin alanından başlayabilir.
Yol bu üç başlangıç noktasının hiçbirinden kurulamıyorsa nesne erişilemez sayılır — programın
kendisi artık ona ulaşamaz, ama nesne öbekte durmaya devam eder, ta ki biri onu temizleyene
kadar. Bir nesneye kayıtlı bir temizleme eylemi bağlanabilir: nesne toplanmaya uygun hâle gelince
bu eylem çalışır. Eylemi çalıştıran iş parçacığı ise ne çağıran iş parçacığıdır, ne de
programın kendi oluşturduğu bir iş parçacığı — kayıtta hiç olmayan bir taraftır. Bu, önceki
derste sınıf başlatması için kurulan iki taraflı ayrımın (çağıran / oluşturulan) yetmediği
üçüncü bir durumdur.

Toplanmaya uygunluğun kendisi de tek bir kurala bağlıdır: bir nesneye giden **güçlü**
referans durduğu sürece nesne toplanamaz — bu, kararı JVM'in verdiği bir sezgi değil, dilin
kendisinin verdiği bir garantidir. Güçlü referans kaldırıldığında nesne toplanmaya uygun
hâle gelir, ama toplanacağı **an** garanti edilmez; program yalnız bir **istek**te bulunabilir.
Bu ikili yapı — erişilebilirlik derleme zamanında yazılmış bir gerçek değil, çalışma
zamanında sürekli değişen bir durumdur ve o durumun sonucuna göre verilecek karar da
programın dışındadır — ölçümün neden bir mandal ve bir döngüye ihtiyaç duyduğunu açıklar:
"temizlendi mi" sorusunun yanıtı tek bir okumada değil, tekrar eden bir yoklamada bulunur.

- **JV12** — Bir nesnenin toplanıp toplanmadığı kendi kurduğumuz temizleme eylemiyle
  izlenir; eylem çalıştığında bir mandalı düşürür, çalışmadığı sürece mandal düşmez.
- **JV13** — `System.gc()` bir istektir, buyruk değil; ölçüm bu isteği kısa aralıklarla
  yineler ve isteğin karşılık bulup bulmadığını mandal üzerinden okur — hiçbir süre ya da
  duraklama ölçülmez.
- **JV14** — Temizleme eylemini koşturan tarafın kimliği yalnız kendi kaydımızla okunur;
  kayıtta olmayan her iş parçacığı "bilinmeyen" sayılır.

```java
// Kim.java — bir eylemi hangi is parcacigi kosturuyor: kendi kaydimizla okunur
import java.util.*;

class Kim {
    static final Map<Thread, String> etiketler = Collections.synchronizedMap(new IdentityHashMap<>());
    static void tanit(String etiket) { etiketler.put(Thread.currentThread(), etiket); }
    static String oku() {
        String e = etiketler.get(Thread.currentThread());
        return e == null ? "bilinmeyen" : e;
    }
}
```

```java
// Temizleme.java — guclu referans dururken temizleme kosmaz; kaldirilinca kosar ve kosturan taraf bilinmeyendir
import java.lang.ref.Cleaner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Temizleme {
    public static void main(String[] args) throws InterruptedException {
        Kim.tanit("cagiran");

        Cleaner temizleyici = Cleaner.create();
        CountDownLatch tamam = new CountDownLatch(1);
        String[] kimKosturdu = { null };

        Object nesne = new Object();
        temizleyici.register(nesne, () -> { kimKosturdu[0] = Kim.oku(); tamam.countDown(); });

        for (int i = 0; i < 3; i++) {
            System.gc();
            boolean kosti = tamam.await(50, TimeUnit.MILLISECONDS);
            System.out.println("guclu referans dururken, istek " + (i + 1) + ": " + (kosti ? "kosti" : "kosmadi"));
        }

        nesne = null;
        while (!tamam.await(50, TimeUnit.MILLISECONDS)) System.gc();
        System.out.println("guclu referans kaldirilinca             : kosti");
        System.out.println("temizleme eylemini kosturan taraf        : " + kimKosturdu[0]);
        System.out.println("kosturan taraf cagiran mi                : " + "cagiran".equals(kimKosturdu[0]));
    }
}
```

```
guclu referans dururken, istek 1: kosmadi
guclu referans dururken, istek 2: kosmadi
guclu referans dururken, istek 3: kosmadi
guclu referans kaldirilinca             : kosti
temizleme eylemini kosturan taraf        : bilinmeyen
kosturan taraf cagiran mi                : false
```

Nesneye giden `nesne` değişkeni elde durduğu sürece üç ayrı istek de karşılıksız kaldı — bu
beklenmedik bir gecikme değil, dilin garantisidir: güçlü referans duruyorsa toplama
**olamaz**, kaç kez istenirse istensin. `nesne = null` satırından sonra döngü yeniden istekte
bulundu ve bu kez karşılık geldi. Temizleme eylemini koşturan tarafın kaydı `Kim.oku()` ile
okundu ve "bilinmeyen" çıktı — eylem ne `main`i koşturan `cagiran` iş parçacığında çalıştı,
ne de programın hiç oluşturmadığı bir taraf olmasının dışında bir açıklaması var. Program bu
iş parçacığını hiç yaratmadı, adlandırmadı, kaydetmedi; buna karşılık kendi temizleme
kodunu o koşturdu.

Önceki iki derste kayıtlı iki taraf yeterliydi: bir sınıfın başlatmasını ya çağıran ya da
programın kendi oluşturduğu bir iş parçacığı koşturuyordu, ikisinin dışına hiç çıkılmadı. Bu
ders üçüncü bir tarafı zorunlu kılıyor, çünkü temizleme eylemini koşturan iş parçacığı bu
programın hiçbir satırında `new Thread(...)` ile üretilmedi. "Bilinmeyen" etiketi bir
sahiplik iddiası değildir — iş parçacığının kendisi JVM'in içinde gerçekten var ve gerçekten
çalışıyor, yalnızca bizim kaydımızda bir karşılığı yok. Bir programın çalışma zamanı yüzeyi,
programcının kendi yazdığı iş parçacıklarından ibaret değildir.

#### Dört Referans Gücü ve Referans Kuyruğu

Bir nesneye erişimin kendisi de tek bir güçte değildir. **Güçlü** referans yukarıda
görüldüğü gibi toplamayı tamamen engeller. **Yumuşak** referans nesneyi tutar, ama JVM'e
bellek baskısı altında bırakabileceğini söyler. **Zayıf** referans nesneyi hiç tutmaz;
başka güçlü bir referans kalmadığı an toplanmaya açıktır. **Hayalet** referans ise en
zayıfıdır: `get()` çağrısı toplanmadan **önce bile**, her zaman `null` döner — hayalet
referans bir nesneyi okumak için değil, toplandığı anı bir kuyruktan öğrenmek için vardır.

Bir referans bir `ReferenceQueue`'ya bağlanabilir; nesne toplanmaya uygun hâle geldiğinde
referansın kendisi bu kuyruğa düşer — kuyruk, "hangi referans artık boşaldı" sorusunun
yanıtını okumanın tek yoludur, çünkü referansı elinde tutan kod nesneyi kaybettiği anı
kendiliğinden öğrenemez. Dört gücün sırası rastgele değildir: her biri bir öncekinden daha
az direnç gösterir. Güçlü referans hiçbir koşulda bırakılmaz; yumuşak referans yalnız
JVM gerçekten sıkışırsa bırakılır; zayıf referans ilk fırsatta bırakılır; hayalet referans
ise zaten hiçbir zaman tutmaz, yalnız "toplandı" haberini taşır. Aşağıdaki ölçüm üç hedef
nesneye üç ayrı güçte referans kurar ve hepsinin son güçlü bağını koparır.

- **JV15** — Üç referans nesnesi de aynı koşumda, üç ayrı hedefe kurulur; hangi referansın
  kuyruğa düştüğü nesne kimliğiyle (`==`) ayrılır, sırayla değil.

```java
// ReferansDeney.java — dort referans gucu: zayif ve hayalet kuyruga duser, yumusak direnir
import java.lang.ref.*;

public class ReferansDeney {
    public static void main(String[] args) throws Exception {
        ReferenceQueue<Object> kuyruk = new ReferenceQueue<>();

        Object hedefZayif = new Object();
        Object hedefYumusak = new Object();
        Object hedefHayalet = new Object();

        WeakReference<Object> zayif = new WeakReference<>(hedefZayif, kuyruk);
        SoftReference<Object> yumusak = new SoftReference<>(hedefYumusak, kuyruk);
        PhantomReference<Object> hayalet = new PhantomReference<>(hedefHayalet, kuyruk);

        System.out.println("hayalet.get() toplanmadan once bile: " + hayalet.get());

        hedefZayif = null;
        hedefYumusak = null;
        hedefHayalet = null;

        boolean zayifDustu = false, hayaletDustu = false;
        for (int i = 0; i < 50 && !(zayifDustu && hayaletDustu); i++) {
            System.gc();
            Reference<?> r;
            while ((r = kuyruk.poll()) != null) {
                if (r == zayif) zayifDustu = true;
                if (r == hayalet) hayaletDustu = true;
            }
            if (!(zayifDustu && hayaletDustu)) Thread.sleep(20);
        }

        System.out.println("zayif referans kuyruga dustu mu   : " + zayifDustu);
        System.out.println("hayalet referans kuyruga dustu mu : " + hayaletDustu);
        System.out.println("zayif.get() (toplandiktan sonra)  : " + zayif.get());
        System.out.println("yumusak kuyruga hic dustu mu      : " + (kuyruk.poll() != null));
        System.out.println("yumusak.get() (bellek baskisi yok): " + (yumusak.get() != null ? "hala erisilebilir" : "null"));
    }
}
```

```
hayalet.get() toplanmadan once bile: null
zayif referans kuyruga dustu mu   : true
hayalet referans kuyruga dustu mu : true
zayif.get() (toplandiktan sonra)  : null
yumusak kuyruga hic dustu mu      : false
yumusak.get() (bellek baskisi yok): hala erisilebilir
```

`hayalet.get()`in ilk satırda bile `null` vermesi bir toplama sonucu değil, sözleşmenin
kendisidir: hayalet referans hiçbir zaman nesneyi geri vermez, yalnız kuyruğa düşerek
"toplandı" der. İstek döngüsü birkaç kez tekrarlandıktan sonra hem zayıf hem hayalet referans
kuyruğa düştü; zayıf referansın `get()`i de o noktadan sonra `null`'a döndü — nesneyi
tutmadığı için, güçlü bağ koptuğu an toplanmaya açık hâle geldi. Yumuşak referans ise aynı
istek turlarında kuyruğa hiç düşmedi ve `get()`i hâlâ nesneyi verdi: JVM, bellek sıkışmadığı
sürece yumuşak referansı bozmayı tercih etmedi. Üç referansın da hedefine giden son güçlü bağ
aynı anda koptu; farkı yaratan tek şey referansın gücüydü.

Bu üç sonuç, üç referans türünün neden farklı işler için kullanıldığını da açıklıyor. Bir
nesneyi başka hiçbir yerde tutmayan, yalnız "hâlâ var mı" diye izleyen bir kayıt zayıf
referans ister — kayıt kendisi nesnenin ömrünü uzatmamalıdır. Yeniden hesaplanabilecek ama
hesaplaması pahalı bir sonucu saklamak isteyen bir kayıt yumuşak referans ister — bellek
boşken sonucu tutsun, sıkışınca bıraksın. "Nesne artık kesin gitti" haberini beklemek isteyen
bir kod ise hayalet referans ister — nesneyi hiç geri istemez, yalnız kuyruktan haberi alır.
Üçü de aynı `ReferenceQueue` düzeneğini kullanır; farkı yaratan yalnız referansın kendi
gücüdür.

Bu düzenek, bir önceki bölümde ölçülen `Cleaner` ile de aynı temelden gelir: standart
kitaplığın kendi belgesi `Cleaner`'ın hayalet erişilebilirlik üzerine kurulduğunu söyler —
her kayıtlı temizleme eylemi, arka planda tam olarak yukarıdaki gibi bir hayalet referans ve
bir kuyruk çiftiyle izlenir. İki ölçüm de aynı soruyu iki ayrı düzeyden sorar: `Temizleme`
hazır bir API üzerinden "kim koşturuyor" sorusunu, `ReferansDeney` ise onun temelindeki
düzeneği elle kurarak "ne zaman ve hangi güçte koşuyor" sorusunu ölçer.

#### Sınırlayıcı Ölçüm: Bir İstek Ne Zaman Koştuğunu Söylemez

Her iki ölçüm de aynı örüntüyü paylaşıyor: `System.gc()` bir kez çağrılmadı, bir döngü
içinde **yinelendi**. Bunun nedeni gecikme değil, çağrının doğasıdır — `System.gc()` bir
istektir, bir buyruk değil; JVM bu isteği yok sayabilir, erteleyebilir ya da farklı bir
biçimde karşılayabilir. Tek bir çağrının karşılıksız kalması "toplama olmadı" anlamına
gelmez, yalnızca "bu istekte olmadı" anlamına gelir. Bu, birinci dersteki kör noktayla aynı
biçimdedir: orada boş bir satır "tetiklenmedi" demekti, burada karşılıksız bir istek
"reddedildi" değil "henüz karşılık bulmadı" demektir. Tek fark, oradaki sınıf başlatması
istendiğinde kesin olarak olan bir şeydi (koşulu doğruysa mutlaka çalışırdı); buradaki
toplama isteği ise koşulu doğru olsa bile (nesne gerçekten toplanmaya uygun olsa bile)
JVM'in kendi takdirine bağlıdır.

Ders bu yüzden isteği bir mandal düşene kadar yineler ve **ne zaman** koştuğunu hiç ölçmez —
yalnız koştuğunda **kimin** koşturduğunu ölçer. Bu ayrım rastgele değildir: "ne zaman"
sorusunun yanıtı çalıştığı makinenin o anki bellek durumuna, başka programların yükünü ne
kadar paylaştığına bağlıdır ve koşumdan koşuma değişir — ölçülüp tabloya yazılacak
belirlenimci bir sayı vermez. "Kim" sorusunun yanıtı ise değişmez: hangi makinede,
hangi yükte koşarsa koşsun, temizleme eylemini koşturan taraf hep kayıtta olmayan bir
taraftır, çünkü bu JVM'in kendi tasarımının bir sonucudur, o anki yük durumunun değil. Bu
ikisinin ayrımı bu kursun genel kuralının bir örneğidir: koşuma bağlı olan (ne zaman) tabloya
girmez, koşumdan koşuma sabit kalan (kim, kaç, hangi taraf) girer.

İki ölçüm de bunun ötesine geçmiyor: hangi toplayıcı algoritmasının çalıştığı, bir
duraklamanın ne kadar sürdüğü, hangi neslin ne kadar büyüdüğü, ne kadar bellek geri
kazanıldığı — bunların hiçbiri bu derste yazılmıyor, çünkü hepsi gerçekleştirime ve
çalıştığı makineye bağlıdır. Kursun adında "Başarım" geçmesi bunu değiştirmez: burada
sayılan şey yalnız koşturan taraf ve kuyruğa düşen referans sayısıdır.

#### Özet

- Erişilebilirliğini yitiren bir nesnenin temizliğini koşturan taraf ne çağıran iş
  parçacığıdır ne de programın oluşturduğu bir iş parçacığı — kayıtta hiç olmayan,
  "bilinmeyen" bir taraftır.
- Güçlü referans durduğu sürece bir nesne toplanamaz; bu bir sezgi değil dilin garantisidir.
  Güçlü referans kaldırılınca nesne toplanmaya açılır, ama toplanacağı an garanti edilmez.
- Zayıf ve hayalet referans, güçlü bağ koptuktan sonra birkaç istekte referans kuyruğuna
  düşer; yumuşak referans bellek baskısı yokken direnir. Hayalet referansın `get()`i
  toplanmadan önce bile her zaman `null` döner.
- `System.gc()` bir istektir, buyruk değil; bu yüzden ölçüm isteği bir döngüyle yineler ve
  yalnız koştuğunda kimin koşturduğunu, ne zaman koştuğunu değil.
- Nesil temelli toplama, kök kümesi ve işaretle-süpür kuramı bu derste tekrarlanmadı; bu ders
  yalnız erişilebilirliğin sonucunu — kimin temizlediğini ve hangi referansın ne zaman
  toplanmaya açıldığını — ölçtü.
- Toplayıcı gerçekleştirimi, duraklatma süresi, nesil boyutu ve geri kazanılan bellek miktarı
  bu derste yazılmadı; hepsi ortama bağlıdır. `Cleaner` de bu ölçümdeki `ReferenceQueue`
  düzeneğinin kendisiyle, hayalet erişilebilirlik üzerine kurulur — iki ölçüm aynı sorunun
  iki ayrı düzeyidir.

#### Sonraki Adım

Bu derste sınıf dosyasında bulunmayan bir karar görüldü: hangi nesnenin ne zaman
toplanacağı, programın kendisinin değil çalışma zamanının verdiği bir karardı ve bu kararı
kimin koşturduğu programın kaydından okunamadı — yalnız kendi kaydımızla ölçülebildi. Sınıf
dosyasında bulunmayan başka bir karar daha var — bu kez temizlikle değil, hızla ilgili.
Bir yöntemin gövdesi sınıf dosyasında hiç değişmeden dururken, aynı yöntem birinci
çağrısında ile bininci çağrısında aynı biçimde koşmayabilir. Sıradaki ders bu farkı ölçer:
kararı veren taraf yine programın kendisi değildir ve yine koda bakarak görünmez, ama bu
kez topladığı şey çöp değil, çalışırken biriktirdiği bir gözlemdir.

### Anında Derleme

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/aninda-derleme

Sınıf dosyasında hiçbir optimizasyon kararı yoktur: aynı yöntem üç yüz bin kez çağrılsa da sınıf dosyasından okunan komut listesi birebir aynı kalır. Kararı veren taraf programın kendisi değildir; kararın dayandığı alıcı tip sayısını ise koşarken çağıran iş parçacığı üretir. Bir çağrı yeri tek alıcılıdan çok alıcılıya geçtiğinde varsayım geçersizleşir — bu ders hız ölçmez, yalnız komut sayısını ve alıcı tip sayısını sayar.

Önceki derste sınıf dosyasında bulunmayan bir karar görüldü: hangi nesnenin ne zaman
toplanacağı. Bu ders sınıf dosyasında bulunmayan başka bir kararı ölçer — bu kez temizlikle
değil, hızla ilgili bir karar. Bir yöntemin gövdesi sınıf dosyasında hiç değişmeden dururken,
aynı yöntem birinci çağrısında ile bininci çağrısında aynı biçimde koşmayabilir. Bilgisayarlar
Nasıl Çalışır kursu **anında derlemeyi** kavram olarak kurdu ve bu ders onu tekrarlamaz; bu
ders yalnız ölçülen davranışa bakar — karar sınıf dosyasında mı duruyor, yoksa başka bir
yerde mi?

#### Sınıf Dosyasında Optimizasyon Kararı Yoktur

Sınıf dosyası derlenip diske yazıldığı andan itibaren sabittir; hiçbir çalışma zamanı olayı
onun içeriğini değiştirmez. Bir yöntem ister hiç çağrılmasın ister milyonlarca kez
çağrılsın, o yöntemin sınıf dosyasındaki komut listesi aynı kalır — çünkü "hangi yöntem sık
çağrılıyor, ne zaman hızlandırılsın" kararı sınıf dosyasının kendisine ait bir bilgi değildir.
Bu, iddia olarak bırakılmaz; aşağıda ölçülür.

Bu, önceki üç derste kurulan örüntünün dördüncü tekrarıdır. Yükleyici hangi sınıfı
tanımladığını sınıf dosyasına yazmaz; sınıf başlatmasını hangi iş parçacığının koşturduğu
sınıf dosyasında görünmez; erişilemez bir nesneyi kimin topladığı da sınıf dosyasının
konusu değildir. Dördüncüsü de aynı biçimdedir: sınıf dosyası bir yöntemin **ne** yaptığını
tam olarak söyler, ama o yöntemin çalışma zamanında **nasıl** koşturulacağı — yorumlanarak
mı, hızlandırılmış bir biçimde mi — hiçbir zaman sınıf dosyasının bir parçası olmaz. Dört
ölçüm de aynı sonuca varıyor: sınıf dosyası kaynağın bir çevirisidir, ama çalışma zamanının
kendisi değildir.

Ölçüm, Java Temelleri kursunun kurduğu `Olcek` aracını kullanır — burada bir ölçü adı olarak
değil, yalnızca sınıf dosyasından bir yöntemin komut listesini okuyan bir araç olarak.

- **JV16** — `Olcek` yalnız bir araçtır; sınıf dosyasından bir yöntemin komut listesini
  okur, hiçbir "eklenen adım" sınıflandırması yapmaz.
- **JV17** — Aynı yöntem, gerçekten yüklenip binlerce kez çağrıldıktan sonra, komut listesi
  aynı sınıf dosyasından yeniden okunur; hiçbir yeniden derleme yapılmaz.
- **JV18** — Çağrı sayısı sayılır (kaç kez çağrıldığı); süre hiç ölçülmez, hiçbir zaman
  damgası okunmaz.

```java
// Olcek.java — sinif dosyasindan bir yontemin komut listesini okuyan arac (Java Temelleri'nden, yalnizca arac olarak)
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Olcek {
    static Path derle(String kaynak, String sinif) throws Exception {
        Path d = Files.createTempDirectory("olcek");
        Path k = d.resolve(sinif + ".java");
        Files.writeString(k, kaynak);
        PrintWriter bos = new PrintWriter(Writer.nullWriter());
        ToolProvider.findFirst("javac").orElseThrow().run(bos, bos, "-d", d.toString(), k.toString());
        return d;
    }

    static List<String> komutlar(Path dizin, String sinif, String yontem) throws Exception {
        ClassModel cm = ClassFile.of().parse(dizin.resolve(sinif + ".class"));
        List<String> a = new ArrayList<>();
        for (MethodModel m : cm.methods())
            if (m.methodName().equalsString(yontem) && m.code().isPresent())
                for (CodeElement e : m.code().get())
                    if (e instanceof Instruction i) a.add(i.opcode().name().toLowerCase(Locale.ROOT));
        return a;
    }
}
```

```java
// SicakYol.java — bir yontem cok kez cagrilsa da sinif dosyasindaki komut listesi degismiyor
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.List;

public class SicakYol {
    public static void main(String[] args) throws Exception {
        String kaynak = """
            public class Hesaplama {
                public static int uygula(int x) { return x * 2 + 1; }
            }
            """;
        Path dizin = Olcek.derle(kaynak, "Hesaplama");
        List<String> once = Olcek.komutlar(dizin, "Hesaplama", "uygula");

        URLClassLoader yukleyici = new URLClassLoader(new URL[] { dizin.toUri().toURL() });
        Class<?> sinif = Class.forName("Hesaplama", true, yukleyici);
        Method yontem = sinif.getMethod("uygula", int.class);

        int cagriSayisi = 300_000;
        int toplam = 0;
        for (int i = 0; i < cagriSayisi; i++) toplam += (int) yontem.invoke(null, i);

        List<String> sonra = Olcek.komutlar(dizin, "Hesaplama", "uygula");

        System.out.println("cagri sayisi                 : " + cagriSayisi);
        System.out.println("komut listesi cagridan once  : " + once);
        System.out.println("komut listesi cagridan sonra : " + sonra);
        System.out.println("iki liste birebir ayni mi    : " + once.equals(sonra));
        System.out.println("toplam sifir degil mi (kosti): " + (toplam != 0));
    }
}
```

```
cagri sayisi                 : 300000
komut listesi cagridan once  : [iload_0, iconst_2, imul, iconst_1, iadd, ireturn]
komut listesi cagridan sonra : [iload_0, iconst_2, imul, iconst_1, iadd, ireturn]
iki liste birebir ayni mi    : true
toplam sifir degil mi (kosti): true
```

`uygula` yöntemi üç yüz bin kez, gerçekten yüklenmiş bir sınıf üzerinden çağrıldı — `toplam`
sıfır çıkmadı, yani çağrılar gerçekten koştu. Buna karşılık aynı sınıf dosyası çağrılardan
önce ve sonra ayrı ayrı okundu ve iki komut listesi **birebir aynı** çıktı. Bu, "değişmedi
çünkü ölçülmedi" değildir — aynı dosya, aynı yöntem, iki ayrı okuma, aynı sonuç. Yöntemin
sık çağrıldığını fark etmek, onu hızlandırmaya karar vermek, o kararı gerçekten uygulamak —
bunların hiçbiri sınıf dosyasına yazılmaz. Karar, programın kendi kaynak metninde de,
derlediği sınıf dosyasında da hiç yoktur; çalışma zamanında, programın kendisinin
oluşturmadığı bir yerde doğar.

Bilgisayarlar Nasıl Çalışır kursunun kurduğu gibi, sanal makine bayt kodunu doğrudan
yorumlayabilir ya da bir bölümünü çalıştığı işlemciye özgü komutlara çevirebilir; hangi
yolun seçildiği, o bölümün ne sıklıkla koşturulduğuna bağlıdır. Ama bu çeviri, üretildiği
yerde — çalışan programın kendi belleğinde — kalır; kaynağa da, sınıf dosyasına da hiç geri
yazılmaz. Aşağıdaki ölçüm bu ayrımı doğrudan sınar: aynı sınıf dosyası, hızlandırılmaya
en uygun duruma getirildikten sonra bile, kendi diskteki hâliyle karşılaştırılır.

Ölçümün `Method.invoke` ile, dolaylı bir çağrı üzerinden yapılması bilinçli bir seçimdir:
`Hesaplama` sınıfı bu dersin diğer sınıflarıyla aynı dizine derlenmedi, ayrı bir yükleyiciyle
yüklendi ve adı yalnız bir metin olarak taşındı. Bunun nedeni, aynı sınıf dosyasını hem
gerçekten çalıştırıp hem de çağrılardan sonra yeniden okuyabilmektir; sınıf normal bir
komşu dosya olarak yazılsaydı, çalışan kopyanın bayt kodunu ayrıca okumak için yine aynı
yoldan geçmek gerekirdi. Okunan şey, yalnız aynı kaynaktan tekrar derlenmiş bir kopya değil,
**gerçekten üç yüz bin kez çağrılmış olan sınıfın kendisidir.**

#### Çağrı Yerinden Geçen Alıcı Tipi: Tek Alıcılıdan Çok Alıcılıya

Bir kararın dayanacağı bir veri olması gerekir ve o veri de bir yerden gelir. Bir arayüz
üzerinden yapılan her çağrı — bir **çağrı yeri (call site)** — zamanla hangi somut tiplerle
karşılaştığının izini biriktirebilir. Bu ders o izi kendi sayacıyla tutar: bir çağrı
yerinden geçen alıcıların kaç **ayrı** tipten geldiğini `getClass()` kimlikleriyle sayar. Bir
çağrı yeri yalnız tek bir tiple karşılaştıysa **tek alıcılı**, birden çok tiple karşılaştıysa
**çok alıcılı** sayılır.

Bu ayrımın önemi şuradan gelir: `Islenebilir i` referansının sınıf dosyasındaki karşılığı
`i.deger()` çağrısıdır ve bu çağrı hangi somut sınıfı taşıdığını **derleme zamanında**
söylemez — `TipA` da olabilir, `TipB` de. Bir çağrı yeri gerçekte hep aynı tek tiple
karşılaşıyorsa, o bilgi bir hızlandırma fırsatıdır: her çağrıda "hangi sınıf bu" sorusunu
yeniden sormak yerine, geçmişte hep aynı yanıtın geldiği varsayılabilir. Ama bu varsayım
sınıf dosyasının bir parçası değildir — çünkü sınıf dosyası derlendiği anda, çağrı yerinin
çalışma zamanında kaç ayrı tiple karşılaşacağı henüz bilinmez. `Islenebilir` arayüzünün kaç
gerçekleştirimi olduğu da bu bilgiyi vermez: bir arayüzün on gerçekleştirimi olabilir, ama
tek bir çağrı yeri koşum boyunca bunlardan yalnız birini görebilir; alıcı sayısı arayüzün
kendi özelliği değil, o çağrı yerinin geçmişte gördüklerinin bir özetidir.

- **JV19** — Çağrı yerinden geçen alıcı tip sayısı kendi kurduğumuz bir kümeyle
  (`getClass()` kimlikleriyle) sayılır; bu, çalışma zamanının kendi profil düzeneğinin
  değil, o düzeneği modelleyen kendi sayacımızın sonucudur.

```java
// Islenebilir.java — cagri yerinden gecen alicilarin ortak arayuzu
interface Islenebilir { int deger(); }
```

```java
// TipA.java — cagri yerindeki ilk alici tip
class TipA implements Islenebilir { public int deger() { return 1; } }
```

```java
// TipB.java — cagri yerine sonradan katilan ikinci alici tip
class TipB implements Islenebilir { public int deger() { return 2; } }
```

```java
// CagriYeri.java — arayuz uzerinden tek bir cagri yeri
class CagriYeri { static int cagir(Islenebilir i) { return i.deger(); } }
```

```java
// AliciTipi.java — cagri yerinden gecen alici tip sayisi kendi sayacimizla sayilir
import java.nio.file.Path;
import java.util.*;

public class AliciTipi {
    public static void main(String[] args) throws Exception {
        Set<Class<?>> gorulenTipler = new HashSet<>();

        TipA a = new TipA();
        int cagriSayisi = 100_000;
        for (int i = 0; i < cagriSayisi; i++) {
            gorulenTipler.add(a.getClass());
            CagriYeri.cagir(a);
        }
        System.out.println("cagri sayisi (yalniz TipA)     : " + cagriSayisi);
        System.out.println("tek alicili cagri yeri          : " + (gorulenTipler.size() == 1));

        TipB b = new TipB();
        gorulenTipler.add(b.getClass());
        CagriYeri.cagir(b);
        System.out.println("ikinci gerceklestirim yuklendi  : " + (gorulenTipler.size() == 2));
        System.out.println("cok alicili cagri yeri (varsayim gecersiz): " + (gorulenTipler.size() > 1));

        String kaynak = """
            interface Islenebilir { int deger(); }
            class CagriYeri { static int cagir(Islenebilir i) { return i.deger(); } }
            """;
        Path dizin = Olcek.derle(kaynak, "CagriYeri");
        System.out.println("cagir yonteminin komut listesi  : " + Olcek.komutlar(dizin, "CagriYeri", "cagir"));
    }
}
```

```
cagri sayisi (yalniz TipA)     : 100000
tek alicili cagri yeri          : true
ikinci gerceklestirim yuklendi  : true
cok alicili cagri yeri (varsayim gecersiz): true
cagir yonteminin komut listesi  : [aload_0, invokeinterface, ireturn]
```

Yüz bin çağrı boyunca `CagriYeri.cagir` yalnız `TipA` örnekleriyle karşılaştı; sayaç tek
kaldı. `TipB`'nin ilk örneği çağrı yerinden geçtiği an sayaç ikiye çıktı ve çağrı yeri çok
alıcılı oldu. Gerçek çalışma zamanı, tek alıcılı bir çağrı yerini bu bilgiye dayanarak
hızlandırabilir — alıcının hep aynı tip olacağını **varsayarak**; ikinci tip ortaya
çıktığında bu varsayım geçersizleşir ve çalışma zamanı **optimizasyondan döner**. Bu ders bu
dönüşü gerçek bir olay olarak yakalamaz (bunu yapmak gerçekleştirime özgü bir araç
gerektirirdi); yalnız varsayımı geçersiz kılan koşulu — ikinci tipin çağrı yerine katılışını
— kendi sayacıyla **modeller**. Alttaki `cagir` yönteminin komut listesi ise iki durumda da
aynıdır: `invokeinterface` komutu hangi somut tipin geleceğini söylemez, yalnız "bir
`Islenebilir` çağır" der; alıcı sayısının bir mi iki mi olduğu karar sınıf dosyasında hiçbir
iz bırakmaz.

Kararın dayandığı bu veriyi kim üretti? Kod, `gorulenTipler` kümesine her çağrıdan önce
kendi ekleme yaptı — bu ekleme **çağıran iş parçacığının kendi kodunun bir parçasıdır**,
ayrı bir taraf tarafından değil. Gerçek çalışma zamanının profil verisi de aynı ilkeyle
üretilir: bir çağrı yerinden geçen her çağrı, kararı verecek tarafa (çalışma zamanının
kendisine) veri bırakır ve bu veriyi biriktiren adımın kendisi **çağıran iş parçacığının
yürüttüğü kodun bir parçasıdır**. Kararı veren taraf çağıran değildir, ama kararın
dayanacağı veriyi üreten çağırandır.

Bu ayrım önceki dersteki "kim koşturuyor" ile "kim görüyor" ayrımına benzer, ama aynısı
değildir. Orada bir değeri yazan ile onu gören iki ayrı iş parçacığıydı; burada veriyi
üreten (çağıran) ile o veriye dayanarak karar veren (çalışma zamanı) iki ayrı **taraftır**,
ikisi de aynı iş parçacığı kavramıyla anılamaz — çünkü kararı veren taraf bu derste hiçbir
zaman bir iş parçacığı kimliğiyle gözlemlenmez, yalnız sınıf dosyasında bulunmayışıyla
tanınır.

#### Sınırlayıcı Ölçüm: Sıcak Yol Bir Süre İddiası Değildir

Bu derste hiçbir yerde süre ölçülmedi ve ölçülemez: üç yüz bin çağrının ne kadar sürdüğü,
tek alıcılı ile çok alıcılı bir çağrı yerinin hangisinin daha hızlı koştuğu — bunların
hiçbiri bu derste yazılmıyor, çünkü ikisi de çalıştığı makineye, o anki yüke ve
gerçekleştirime bağlıdır. Sayılan tek şey **komut sayısı** ve **alıcı tip sayısı**dır; ikisi
de programın kendi ürettiği, koşumdan koşuma sabit kalan verilerdir.

**Sıcak yol** terimi bu yüzden bir hız iddiası değil, **çağrı yoğunluğuyla** tanımlı bir
bölgedir: bir yöntem çok çağrıldığı için sıcaktır, hızlı koştuğu için değil. Yukarıdaki
ölçüm `uygula` yöntemini üç yüz bin kez çağırarak onu yoğun bir çağrı bölgesine soktu, ama
bu derste ne çağrı öncesi ne çağrı sonrası hiçbir süre karşılaştırması yapılmadı — yalnız
çağrı sayısı ve sınıf dosyasının değişmediği sayıldı. Sıcaklığın bir hız kazancına
dönüşüp dönüşmediği bu dersin sorusu değildir.

Bu sınır iki yönde de işler. Bir yandan, üç yüz bin çağrının "yavaş" ya da "hızlı" olduğuna
dair hiçbir iddia yazılmadı — tek iddia, sınıf dosyasının değişmediğiydi. Öte yandan, tek
alıcılı bir çağrı yerinin çok alıcılı olandan "daha hızlı" koştuğuna dair de hiçbir iddia
yazılmadı — tek ölçülen, alıcı sayısının bir mi iki mi olduğuydu. İki ölçüm de "ne
kadar hızlı" sorusuna hiç yaklaşmadan, yalnız "kaç" ve "aynı mı" sorularına yanıt verdi; bu
kursun ölçüm disiplini burada da aynen geçerlidir.

#### Özet

- Sınıf dosyası derlendiği andan sonra sabittir: bir yöntem üç yüz bin kez çağrılsa da
  komut listesi çağrı öncesi ve sonrası birebir aynı kalır.
- Bir yöntemin ne zaman ve nasıl hızlandırılacağı kararı sınıf dosyasında hiç yoktur; karar
  çalışma zamanında, programın kendisinin oluşturmadığı bir yerde doğar.
- Bir çağrı yeri, geçen alıcıların kaç ayrı tipten geldiğine göre tek alıcılı ya da çok
  alıcılı sayılır; ikinci bir gerçekleştirim çağrı yerine katıldığında tek alıcılı varsayım
  geçersizleşir — bu ders bu geçersizleşmeyi (optimizasyondan dönüşü) kendi sayacıyla
  modeller, gerçek bir olay olarak yakalamaz.
- Kararın dayanacağı veriyi (alıcı tip sayısını) üreten taraf çağıran iş parçacığıdır; kararı
  veren taraf ise çağıran değildir.
- Bir çağrı yerinin alıcı sayısı bir mi iki mi olursa olsun, o çağrıyı taşıyan yöntemin komut
  listesi sınıf dosyasında değişmez.
- Sıcak yol çağrı yoğunluğuyla tanımlıdır, süreyle değil; bu ders hiçbir süre ölçmez, yalnız
  komut sayısını ve alıcı tip sayısını sayar.

#### Sonraki Adım

Bu konu boyunca sınıf dosyasında bulunmayan üç karar görüldü: sınıfı hangi yükleyicinin
tanımladığı ve hangi iş parçacığının başlattığı, erişilemez bir nesneyi kimin topladığı ve
bir yöntemi ne zaman hızlandıracağı. Üçünde de kararı veren taraf programın kendisi
değildi — ne kaynak metninde, ne de ondan derlenen sınıf dosyasında hiçbir yerde
bulunamadı. Ama kodu **koşturan** iş parçacıklarını oluşturan yine programın kendisidir:
`new Thread(...)` satırını programın kendisi yazar, `start()` çağrısını programın kendisi
yapar. Sıradaki konu oradan başlar — bir iş parçacığı programın içinden nasıl var ediliyor
ve gövdesini hangi taraf koşturuyor?

## Eşzamanlılık

Bir gövdeyi çağıranda mı başka bir iş parçacığında mı koşturduğunu belirleyen çağrı, bellek modelinin görünürlük ve sıralama sözleri, izleyici kilitlerinin ve yeniden girilebilirliğin ölçülmesi, eşzamanlılık yardımcılarının işi hangi tarafa yüklediği, yürütücü havuzunun dolduğunda görevi kime verdiği, sanal iş parçacıklarının ayrık kimlik sayısı ve gelecek tabanlı zincirlemede adımı kimin koşturduğu.

### İş Parçacıkları ve Yaşam Döngüsü

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/is-parcaciklari-ve-yasam-dongusu

Aynı gövde run() ile çağıranda, start() ile oluşturulan bir iş parçacığında koşuyor; fark tek bir yöntem adı. Üç kesme kurulumundan yalnız biri gövdeyi durduruyor, kalan ikisi kesme çağrısı olsun olmasın aynı adıma ulaşıyor.

Anında Derleme dersi şu sonuçla bitti: sınıf dosyasında bulunmayan bir optimizasyon kararını
veren taraf programın kendisi değildi, ama kodu koşturan iş parçacıklarını oluşturan yine
programdı. Bu ders oradan başlıyor. Sınıf dosyasını kim yüklüyor, kararı kim veriyor, profili
kim üretiyor sorularının hepsi bir gövdenin **zaten koştuğunu** varsayıyordu. Şimdi bir adım
geri gidiliyor: bir iş parçacığı nasıl var ediliyor ve az önce yazılmış olan gövdesini hangi
taraf koşturuyor?

Sorunun yanıtı çağrı yazımında görünmüyor. `Runnable` gövdesi tek bir yerde tanımlanır ve iki
ayrı yöntemle tetiklenebilir; ikisi de aynı satırı çalıştırır, ikisi de derleyici için geçerli
bir çağrıdır. Bu dersin ölçtüğü ilk fark — kursun ortak çekirdeğinin de ilk iki satırı — tam
olarak burada duruyor.

Kursun sorduğu soru iki parçadan oluşuyor: bir adımı hangi iş parçacığı koşturuyor ve o iş
parçacığını kim var etti? Bu ders yalnız ikinci parçaya, var etmenin kendisine bakıyor. Bir
program çalışırken zaten en az bir iş parçacığı vardır — `main` yöntemini koşturan iş
parçacığı — ve program başka hiçbir iş parçacığı istemezse bu tek başına yeterlidir. Yeni bir
iş parçacığı istemek gövdeyi ayrı bir akışa taşır; o akış artık **çağıranın** akışı değildir
ve kendi başına başlar, kendi başına biter. Aşağıdaki iki bölüm bu var etme ve bitirme
sürecini, ortadaki üçüncü bir soruyla birlikte ölçüyor: yarım kalmış bir iş parçacığına
"artık dur" demenin gövdeye ne yaptırdığı.

#### Aynı Gövde, İki Koşum Yolu

- **EZ1** — Kim koşturduğunu söyleyen etiket, iş parçacığının kendisinden değil, ayrı tutulan
  bir kayıttan okunur; bir iş parçacığı `Kosum.tanit` ile kaydolmadıysa `bilinmeyen` sayılır.
- **EZ2** — `Govdeler.main`'i çalıştıran iş parçacığı baştan `"cagiran"` diye kaydedilir; bu
  dersin geri kalanında "çağıran" hep bu iş parçacığıdır.

```java
// Kosum.java — bir govdeyi hangi is parcaciginin kosturdugunu etiketle soyler
import java.util.*;

public class Kosum {
    static final Map<Thread, String> etiketler =
            Collections.synchronizedMap(new IdentityHashMap<>());

    static void tanit(String etiket) { etiketler.put(Thread.currentThread(), etiket); }

    static String kim() {
        String e = etiketler.get(Thread.currentThread());
        return e == null ? "bilinmeyen" : e;
    }

    static void gozle(String nokta) {
        System.out.printf("%-32s %s%n", nokta, kim());
    }
}
```

`Kosum`, iş parçacığının adını, kimlik karmasını ya da önceliğini hiç okumaz — bunların hepsi
ortama bağlı veridir. Tek okuduğu şey, `tanit` ile önceden yazılmış bir etikettir; kimlik
sorusu böylece bizim verdiğimiz ada indirgenir.

```java
// Govdeler.java — ayni govde run() ile cagiranda, start() ile olusturulan is parcacikinda kosuyor
public class Govdeler {
    public static void main(String[] args) throws InterruptedException {
        Kosum.tanit("cagiran");
        Runnable govde = () -> Kosum.gozle("govde");

        // 1. dogrudan cagri: run() adi baska bir is parcacigi baglamaz, cagiran kosturur
        new Thread(govde).run();

        // 2. start(): govdeyi olusturulan is parcacigi kosturur
        Thread t = new Thread(() -> { Kosum.tanit("olusturulan"); govde.run(); });
        t.start();
        t.join();
    }
}
```

```
govde                            cagiran
govde                            olusturulan
```

İki satır da aynı `govde` referansını çalıştırıyor, ikisi de `Runnable.run()` çağırıyor. Fark
kimin `run()` dediğidir: birinci satırda `new Thread(govde)` nesnesinin `run()`'ı doğrudan
çağrılıyor — bu, sıradan bir yöntem çağrısıdır, yeni bir iş parçacığı **oluşturmaz**, sadece
gövdeyi çağıranın kendi yığıtında çalıştırır. İkinci satırda `start()` çağrılıyor; bu,
çalışma zamanına yeni bir iş parçacığı isteği yollar ve gövde o iş parçacığında koşar. `t.join()`
olmasaydı `main`'in gövdenin bitişini beklediğini hiçbir şey garanti etmezdi — bu köprü
aşağıdaki sınırlayıcı ölçümde ayrıca ele alınıyor.

`new Thread(govde)` ifadesinin kendisi, henüz hiçbir şey **koşturmuyor**. İki koşum yolunda da
önce bir `Thread` nesnesi kuruluyor; nesne kurulduğu an yalnızca bir kayıt taşıyor, gövdeyi
kimin çalıştıracağına dair henüz bir taahhüt yok. Birinci satırda o nesne hiç `start()`
edilmeden `run()` ile çağrılıyor — nesne var ama iş parçacığı hiç talep edilmemiş oluyor,
gövde doğrudan çağıranın kod akışına giriyor. İkinci satırda ise `start()` çağrısı bu
taahhüdü veriyor: çalışma zamanından yeni bir iş parçacığı istiyor ve gövde artık o isteğin
karşılığı olan iş parçacığında koşuyor. Aradaki fark, çağrının kendisinin bir iş parçacığı
**var ettiği** ile var etmediği arasındaki farktır — ve bu fark, iki satırın ikisinde de aynı
görünen `run()` çağrısının arkasında saklı kalıyor.

#### Yaşam Döngüsü: Belirlenimci Gözlemler

- **EZ3** — `isAlive()` iş parçacığının kendisinden hiçbir kimlik verisi okumaz, yalnız
  "başlatıldı ve henüz bitmedi mi" sorusuna `true`/`false` döner; bu, ortama bağlı değildir.

Bir `Thread` nesnesi `start()` çağrılmadan önce **yeni**dir, `start()` sonrası ve bitmeden
önce **çalışır durumdadır**, `join()`'in döndüğü an ise kesin olarak **bitmiştir**. Üç durum
da `isAlive()` ile belirlenimci biçimde gözlenir; aradaki geçişler zamanlamaya değil, hangi
yöntemin çağrıldığına bağlıdır. `isAlive()`'ın kendisi de iş parçacığından hiçbir ortama bağlı
veri okumuyor — döndürdüğü tek bilgi bir mantıksal değer ve o değer hangi yöntemlerin daha
önce çağrıldığından belirleniyor, makinenin o an ne kadar meşgul olduğundan değil.

```java
// YasamDongusu.java — durumlar belirlenimci gozlenir: baslamadan once, calisirken, bittikten sonra
import java.util.concurrent.CountDownLatch;

public class YasamDongusu {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch baslasin = new CountDownLatch(1);

        Thread t = new Thread(() -> {
            try { baslasin.await(); } catch (InterruptedException e) { }
        });

        System.out.println("baslamadan once  isAlive: " + t.isAlive());
        t.start();
        System.out.println("start sonrasi    isAlive: " + t.isAlive());
        baslasin.countDown();
        t.join();
        System.out.println("join sonrasi     isAlive: " + t.isAlive());

        try {
            t.start();
            System.out.println("ikinci start     : basarili");
        } catch (IllegalThreadStateException e) {
            System.out.println("ikinci start     : " + e.getClass().getSimpleName());
        }
    }
}
```

```
baslamadan once  isAlive: false
start sonrasi    isAlive: true
join sonrasi     isAlive: false
ikinci start     : IllegalThreadStateException
```

`baslasin` gövdeyi bir `CountDownLatch` üzerinde bekletiyor; böylece `start()` döndükten sonra
gövdenin henüz bitmediği, `isAlive()` ile **koşuma bırakmadan** garanti ediliyor. `join()`
çağrısı iki işi birden yapıyor: hem çağıranı gövde bitene kadar durduruyor hem de dönüşünden
sonra `isAlive()`'ın `false` olacağını kesinleştiriyor — `join`, bu dersin sıralama aracıdır.
Son satır ayrı bir kuralı gösteriyor: bir `Thread` nesnesi yalnız **bir kez** başlatılabilir;
ikinci `start()` çağrısı, nesnenin durumundan bağımsız olarak her zaman
`IllegalThreadStateException` fırlatır.

Bu kısıtlamanın nedeni yaşam döngüsünün kendisidir. `start()`, bir `Thread` nesnesini
**yeni** durumdan çıkarıp çalışma zamanına bir kez teslim eder; teslimden sonra o nesne bir
daha hiç **yeni** duruma dönmez, bitince kalıcı olarak sona ermiş sayılır. Bir nesneyi ikinci
kez başlatmaya izin vermek, aynı iş parçacığı kimliğinin iki ayrı koşum boyunca yeniden
kullanılması demek olurdu; çerçeve bunun yerine her koşum için taze bir `Thread` nesnesi
kurulmasını zorunlu kılar. Bitmiş bir gövdeyi yeniden çalıştırmak isteyen kod, aynı `Runnable`
gövdesini **yeni** bir `Thread` nesnesine sararak yapar — nesne değişir, gövde değişmez.

#### Kesme: Bir İstek, Cevap Gövdede

- **EZ4** — Kesme bir buyruk değildir; `Thread.interrupt()` yalnız bir bayrağı işaretler ya da
  engelleyen bir çağrıda istisna düşürür. Gövdenin bu isteğe **ne yapacağı**, gövdenin kendi
  kodunda yazılıdır — çerçevenin belirlediği bir şey değildir.

Üç kurulum aynı deseni paylaşıyor: bir iş parçacığı başlar, kendini `CountDownLatch` ile
kaydettirir, sonra kesme isteği (isteğe bağlı olarak) gönderilir, en son `join()` ile
tamamlanması beklenir. Değişen tek şey gövdenin kesmeye **nasıl davrandığıdır**.

Birinci gövdenin döngü koşulu `Thread.currentThread().isInterrupted()` çağırıyor; bu, bir
örnek yöntemidir ve bayrağı okuduktan sonra **temizlemez**. Aynı soruyu soran ama bayrağı okur
okumaz sıfırlayan statik bir karşılığı da var; o karşılık burada kasıtlı olarak kullanılmıyor,
çünkü döngüden çıktıktan sonraki `sonuc.append` satırı hâlâ "bu iş parçacığı kesme istedi"
bilgisine ihtiyaç duyabilirdi — sıfırlayan biçim bayrağı ilk okumada silip bu bilgiyi
döngünün dışına taşımazdı. Hangi biçimin kullanılacağı da gövdenin kararıdır — çerçeve ikisini
de sunar, sonrasında bayrağın gerekip gerekmediğine göre seçim programcıya kalır.

```java
// Kesme.java — kesme bir istektir: govde onu gorebilir ama durup durmamaya kendi kodu karar verir
import java.util.concurrent.CountDownLatch;

public class Kesme {
    static String bayragiDenetleyerekDur(boolean kesEt) throws InterruptedException {
        CountDownLatch basladi = new CountDownLatch(1);
        StringBuilder sonuc = new StringBuilder();
        Thread t = new Thread(() -> {
            basladi.countDown();
            while (!Thread.currentThread().isInterrupted()) { }
            sonuc.append("bayrak gorulunce dongudan cikti");
        });
        t.start();
        basladi.await();
        if (kesEt) t.interrupt();
        t.join();
        return sonuc.toString();
    }

    static String uykudaYakalar(boolean kesEt) throws InterruptedException {
        CountDownLatch basladi = new CountDownLatch(1);
        StringBuilder sonuc = new StringBuilder();
        Thread t = new Thread(() -> {
            basladi.countDown();
            try { Thread.sleep(80); sonuc.append("uyku tamamlandi, "); }
            catch (InterruptedException e) { sonuc.append("istisna yakalandi, "); }
            sonuc.append("ayni sonraki adima gecti");
        });
        t.start();
        basladi.await();
        if (kesEt) t.interrupt();
        t.join();
        return sonuc.toString();
    }

    static String bayragiHicSormaz(boolean kesEt) throws InterruptedException {
        CountDownLatch basladi = new CountDownLatch(1);
        StringBuilder sonuc = new StringBuilder();
        Thread t = new Thread(() -> {
            basladi.countDown();
            long toplam = 0;
            for (long i = 0; i < 20_000_000L; i++) toplam += i;
            sonuc.append("dongu tamamlandi (toplam sifirdan buyuk: " + (toplam > 0) + ")");
        });
        t.start();
        basladi.await();
        if (kesEt) t.interrupt();
        t.join();
        return sonuc.toString();
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1. bayragi denetleyen govde, kesme cagrisiyla:");
        System.out.println("   " + bayragiDenetleyerekDur(true));
        System.out.println();

        System.out.println("2. uykuda yakalayan govde, kesme cagrisiyla:");
        System.out.println("   " + uykudaYakalar(true));
        System.out.println("2. ayni govde, kesme cagrisi OLMADAN:");
        System.out.println("   " + uykudaYakalar(false));
        System.out.println();

        System.out.println("3. bayragi hic sormayan govde, kesme cagrisiyla:");
        System.out.println("   " + bayragiHicSormaz(true));
        System.out.println("3. ayni govde, kesme cagrisi OLMADAN:");
        System.out.println("   " + bayragiHicSormaz(false));
    }
}
```

```
1. bayragi denetleyen govde, kesme cagrisiyla:
   bayrak gorulunce dongudan cikti

2. uykuda yakalayan govde, kesme cagrisiyla:
   istisna yakalandi, ayni sonraki adima gecti
2. ayni govde, kesme cagrisi OLMADAN:
   uyku tamamlandi, ayni sonraki adima gecti

3. bayragi hic sormayan govde, kesme cagrisiyla:
   dongu tamamlandi (toplam sifirdan buyuk: true)
3. ayni govde, kesme cagrisi OLMADAN:
   dongu tamamlandi (toplam sifirdan buyuk: true)
```

Birinci kurulumun döngüsü **yalnız** kesme bayrağına bakıyor; kesme çağrısı gelmeseydi bu
döngünün başka hiçbir çıkış yolu yoktu — kodun kendisi bunu gösteriyor, bu yüzden o kurulum
kesmesiz çalıştırılmıyor. İkinci ve üçüncü kurulumlar ise **iki kez**, biri kesme çağrısıyla
biri çağrısız, koşturuldu ve ikisinde de aynı sonuca ulaştı: `Thread.sleep` içindeki yakalama
bloğu istisnayı yutup bir sonraki satıra geçiyor, sayaç döngüsü ise bayrağı hiç okumadığı için
kesmenin varlığından habersiz tamamlanıyor. Üç kurulumun **yalnız biri**, kesme isteği
gelmeden gövdesini hiç bitiremiyor; kalan ikisi kesme isteğini görsün ya da görmesin aynı
adıma ulaşıyor. Kesmenin kendisi bir davranış dayatmıyor — davranışı gövdenin yazarı seçiyor.

Aradaki ayrım tek tek bu üç örneğe özgü değil, genelleşen bir kuraldır. Standart kitaplığın
**engelleyen** çağrıları — `Thread.sleep`, nesne kilidinde beklerken çağrılan bekleme yöntemi,
`join`, eşzamanlılık paketindeki bekleyen çağrılar — kesme bayrağını kendileri denetler ve
bayrak işaretliyken (ya da işaretlendiği an) `InterruptedException` fırlatıp bayrağı
**temizler**. Bunun dışındaki her kod — bir hesaplama döngüsü, bir dosyaya yazma, bir ağ
soketi okuma — bayrağı kendiliğinden denetlemez; denetlemek isteyen gövde bunu `birinci
kurulum`daki gibi kendi elleriyle yazmak zorundadır. İkinci kurulum bu ayrımın ortasında
duruyor: `sleep` bayrağı görüyor ve istisna fırlatıyor, ama gövde o istisnayı yakalayıp hiçbir
şey yapmadan yoluna devam ediyor. İstisnanın fırlatılması bir garantidir; ona **cevap vermek**
gövdenin sorumluluğudur ve bu sorumluluk çoğu zaman boş bir `catch` bloğunda kayboluyor.

#### Sınırlayıcı Ölçüm: start()'ın Dönmesi Sıra Vermez

`Govdeler.main`'deki `t.start()` çağrısı döndüğünde, gövdenin ilk satırının çalışmış olduğunu
hiçbir şey garanti etmez. `start()`'ın görevi yeni bir iş parçacığı **istemektir**; o isteğin
ne zaman yerine getirileceği çağıranın bilgisi dışındadır. Bu yüzden `Govdeler.main` içinde
`t.join()` çağrılmadan önce gövdenin çıktısını okumaya çalışan bir satır eklenseydi, o satırın
gövdenin çıktısından önce mi sonra mı çalışacağı **kurguya bağlanmamış bir gözlem** olurdu ve
ölçüm sayılmazdı — tıpkı bu dersin ortak tanımının kör noktasında yazılana benzer biçimde,
"olmadı" ile "sırası kurulmadı" birbirine karıştırılmış olurdu. `YasamDongusu` ve `Kesme`
derslerindeki her kurulum bu yüzden bir `CountDownLatch` ile açılıyor: gövde önce kendini
kaydettiriyor, çağıran ancak o kayıttan sonra bir sonraki adıma geçiyor. Sıra çizelgeleyiciye
bırakılmıyor; `await`, `join` ve kayıt noktalarıyla elle kuruluyor.

Bu sınır, ölçümün neden hep aynı iskeleti taşıdığını da açıklıyor. `Kesme.bayragiDenetleyerekDur`
gövdesi kendini `basladi.countDown()` ile kaydettirmeden önce ana iş parçacığı `basladi.await()`
üzerinde bekliyor; kesme isteği ancak bu kayıttan sonra gönderiliyor. Kayıt satırı atlanıp
`t.interrupt()` doğrudan `t.start()`'ın hemen ardından çağrılsaydı, isteğin gövde henüz
`while` döngüsüne girmeden mi girdikten sonra mı geldiği belirsiz kalırdı — sonuç değişmezdi
(döngü er ya da geç bayrağı görüp çıkardı), ama **hangi turda** çıktığı ölçülemezdi. Bu
derste öyle bir tur sayısı hiç iddia edilmiyor; iddia yalnız **çıktı mı çıkmadı mı**
sorusuyla sınırlı tutuluyor, çünkü sınırın kendisi tam olarak budur: ölçülebilecek olan,
kurgunun deterministik kıldığı kadarıdır.

#### Özet

- Aynı `Runnable` gövdesi `run()` ile çağrılırsa çağıranda, `start()` ile çağrılırsa
  oluşturulan bir iş parçacığında koşar; fark çağrı yazımında tek bir yöntem adıdır.
- `isAlive()` bir iş parçacığının başlamadan önce, çalışırken ve `join()` sonrası durumunu
  belirlenimci biçimde gözler; ikinci `start()` çağrısı nesnenin durumundan bağımsız olarak
  her zaman `IllegalThreadStateException` fırlatır.
- Kesme bir istektir, bir buyruk değildir: üç kurulumun yalnız biri (bayrağı döngü koşulunda
  denetleyen) kesme isteği olmadan gövdesini hiç bitiremiyor; kalan ikisi isteği görsün ya da
  görmesin aynı sonraki adıma ulaşıyor.
- `start()`'ın dönmesi, gövdenin ilk satırının koştuğu anlamına gelmez; iki olay arasında
  sıra kurulmamıştır ve bu, kurgusuz gözlenemez — sıra `CountDownLatch` ve `join` ile elle
  kurulur.

#### Sonraki Adım

Bu derste "kim koşturuyor" sorusu yanıtlandı ve kesme isteğine verilecek yanıtın gövdenin
kendi kodunda yazıldığı görüldü. Ama `Kesme` derslerinde her kurulum, gövdenin yazdığı bir
`StringBuilder`'ı ve bir sayacı okurken hiçbir eşitleme kullanmadı, çünkü değeri hep aynı iş
parçacığı hem yazdı hem okudu — `basladi.await()` ile `t.join()` yalnız **sırayı** kurdu,
görünürlüğü sağlayan gerçek mekanizma her ikisinin de kendi içinde zaten taşıdığı bir
garantiydi ve bu derste hiç adı konmadı. Sıradaki ders bu rahatlığı kaldırıyor: bir değeri
yazan iş parçacığı ile okuyan iş parçacığı farklı olduğunda ne garanti ediliyor ve bu
garantiyi hangi taraf — yalnız yazan mı, yalnız okuyan mı, ikisi de mi — kurmak zorunda?

### Java Bellek Modeli

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/java-bellek-modeli

Sekiz yayımlama düzeneğinden dördü hem yazanın hem okuyanın harekete geçmesini istiyor, biri yükü tek başına yazana (final alan), biri tek başına okuyana (join) yüklüyor, ikisi ise görünürlük sorusunu hiç sormuyor. Senkronizasyonsuz bir alanın görünürlük kusuru koşturularak gösterilmiyor; kural olarak modelleniyor.

Bir önceki ders bir değeri hep aynı iş parçacığının hem yazıp hem okuduğu kurulumlarla
çalıştı; `basladi.await()` ile `t.join()` yalnız **sırayı** kurdu, görünürlüğü sağlayan
mekanizma hiç adı konmadan zaten oradaydı. Bu ders o mekanizmanın adını koyuyor. Bir değeri
bir iş parçacığı yazdığında ve başka bir iş parçacığı okuduğunda, okuyanın yazılan değeri
görmesi **otomatik değildir** — görülmesini sağlayan şey, iki eylem arasında dilin tanıdığı bir
sıra kurulmuş olmasıdır. Bu sıraya **önce-olur bağıntısı** deniyor ve kursun bu dersteki
sorusu şu: sekiz farklı yayımlama düzeneğinin her birinde, bu bağıntıyı kurmak için **hangi
taraf** ne yapmak zorunda — yalnız yazan mı, yalnız okuyan mı, ikisi de mi, yoksa hiçbiri mi?

Sorunun "otomatik değildir" kısmı önceki dersteki örneklerle çelişiyor gibi görünebilir; orada
hiçbir ek çaba harcanmadan her değer doğru okundu. Çelişki yüzeyseldir: önceki dersteki her
kurulum zaten `CountDownLatch` ya da `join` kullanıyordu ve bu iki araç — aşağıda görüleceği
gibi — kendi başlarına birer önce-olur kaynağıdır. Görünürlük orada da otomatik değildi;
yalnız gerekli bağıntı zaten kuruluydu ve bu ders o kuruluşun neden yeterli olduğunu
adlandırıyor.

#### Önce-Olur Bağıntısı: Bir Kural Olarak

- **EZ5** — Önce-olur bağıntısı bir zamanlama iddiası değildir; iki eylemin **hangi sırayla
  gerçekleştiğini** söylemez, yalnız biri gerçekleştikten sonra ötekinin sonuçlarının **görünür
  olacağını** garanti eder. Bu derste bağıntı hiçbir yerde ölçülmüyor, yalnız kural olarak
  kullanılıyor.

Dilin tanıdığı önce-olur kuralları arasında bu derste kullanılanlar şunlar: bir iş
parçacığındaki eylemler program sırasına göre birbirinden önce-olur; bir izleyici kilidinin
serbest bırakılması, aynı kilidi sonra alan her eylemden önce-olur; uçucu (`volatile`) bir
alana yazmak, aynı alanı sonra okuyan her eylemden önce-olur; bir iş parçacığının
başlatılması (`start()`), o iş parçacığındaki bütün eylemlerden önce-olur; bir iş
parçacığındaki bütün eylemler, başka bir iş parçacığının o iş parçacığında `join()`'den
başarıyla dönmesinden önce-olur; ve bir `final` alanın kurucudaki değeri, kurucu tamamlandıktan
sonra o nesneye **güvenli yollarla** ulaşan her iş parçacığından önce-olur. Bu liste, aşağıdaki
sekiz düzeneği kategorize etmek için yeterli.

Bu kuralların hepsi **geçişlidir**: A eylemi B'den önce-olur ve B eylemi C'den önce-olursa, A
da C'den önce-olur — ara halka farklı bir mekanizma kullansa bile. Bu yüzden bir değeri
`main`'de yazıp bir iş parçacığını `start()` eden, o iş parçacığın içinde değeri bir
`CountDownLatch` ile ikinci bir iş parçacığına ileten bir zincirde, en baştaki yazı en
sondaki okumaya **tek bir zıplamayla değil, halka halka** ulaşır; her halka kendi kuralını
uygular, zincirin bütünü ayrıca kanıtlanmaz. Aşağıdaki ölçümlerin her biri bu zincirin **tek
bir halkasını** — yazan ile okuyan arasındaki doğrudan bağı — izole ediyor.

#### Yazanın Bedeli: final Alan ve Güvenli Yayımlama

- **EZ6** — Ölçüm okuyan tarafın kod akışına hiçbir ek adım koymuyor; okuyan yalnızca alanı
  okuyor. Bedel tamamen kurucunun tamamlanma anına yükleniyor.

```java
// GuvenliYayimlama.java — final alanin bedeli kurulum aninda odenir, okuyan hicbir sey yapmaz
public class GuvenliYayimlama {
    static final class Paket {
        final int deger;
        Paket(int deger) { this.deger = deger; }
    }

    public static void main(String[] args) throws InterruptedException {
        Paket kutu = new Paket(5);
        Thread okuyan = new Thread(() -> {
            System.out.println("okuyanin hicbir eklemesi yok, sadece Paket.deger okuyor: " + kutu.deger);
        });
        okuyan.start();
        okuyan.join();
    }
}
```

```
okuyanin hicbir eklemesi yok, sadece Paket.deger okuyor: 5
```

`Paket` kurucu tamamlanana kadar `deger` alanı yazılmamış sayılır; kurucu bittiği an bu alan
**dondurulur**. `okuyan` iş parçacığı `kutu.deger`'i okurken ne bir kilit alıyor ne bir uçucu
alanı denetliyor — hiçbir ek disiplin uygulamıyor, çünkü uygulaması gereken disiplin zaten
**yazan tarafta**, kurucunun içinde tüketildi. Bu derste `kutu` referansı `okuyan`'a
`start()` üzerinden, yani zaten önce-olur bağıntısı kuran bir kanaldan ulaşıyor; `final`
alanın kendine özgü güvencesi bundan da geniştir — kurucu `this` referansını erken kaçırmadığı
sürece, referans hangi kanaldan ulaşırsa ulaşsın alanın doğru değeri görülür. Bu genişletilmiş
güvence burada ayrıca koşturulmuyor: kanıtlamak için referansı **kasıtlı olarak eşleşimsiz**
yayımlamak gerekirdi ve bu, tam olarak aşağıdaki sınırlayıcı ölçümün kaçındığı türde bir
kurgudur.

Güvencenin bir de koşulu var: kurucu `this` referansını **erken kaçırmamalıdır**. Bir kurucu,
kendi tamamlanması bitmeden `this`'i bir statik alana yazar ya da bir dinleyiciye kaydederse,
başka bir iş parçacığı o nesneyi kurucu hâlâ çalışırken görebilir — ve o anda `final` alan
henüz atanmamış ya da yarım atanmış olabilir. Bu ders böyle bir kaçışı kurmuyor; yalnız
`final` alanın güvencesinin **koşulsuz** olmadığını, kurucunun disiplinine bağlı olduğunu
kaydediyor. `GuvenliYayimlama` bu koşulu sağlıyor: `Paket` kurucusu `this`'i hiçbir yere
yazmıyor, `deger` atandıktan hemen sonra kurucu bitiyor.

#### Sekiz Düzenek, Dört Kategori

- **EZ7** — Aşağıdaki altı düzenek `bekle` adlı sınırlı bir döngüyle ölçülüyor: döngü koşulu
  gerçekleşene kadar en çok 200 milyon kez sınanıyor, süresi hiçbir yerde yazdırılmıyor.
  Ölçülen tek şey, koşulun **gerçekleşip gerçekleşmediği** ve gerçekleştiğinde okunan değerdir.

```java
// Yayimlama.java — alti duzenekte yazanin ve okuyanin ayni mekanizmayi kullanmasi yeterli mi
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;

public class Yayimlama {
    static volatile int ucucuDeger = 0;
    static volatile boolean ucucuHazir = false;

    static final Object kilit = new Object();
    static int kilitliDeger = 0;
    static boolean kilitliHazir = false;

    static final AtomicInteger atomikDeger = new AtomicInteger(-1);

    static int joinDeger = 0;
    static int mandalDeger = 0;

    static final ThreadLocal<Integer> ozel = ThreadLocal.withInitial(() -> 0);

    static boolean bekle(BooleanSupplier kosul) {
        for (long i = 0; i < 200_000_000L; i++) if (kosul.getAsBoolean()) return true;
        return false;
    }

    public static void main(String[] args) throws InterruptedException {
        // 1. volatile: yazan ucucu alana yazar, okuyan ayni ucucu alani okur
        Thread t1 = new Thread(() -> { ucucuDeger = 5; ucucuHazir = true; });
        t1.start();
        boolean g1 = bekle(() -> ucucuHazir);
        System.out.println("volatile       : gorundu=" + g1 + " deger=" + ucucuDeger);
        t1.join();

        // 2. synchronized: yazan ve okuyan ayni kilidi kullanir
        Thread t2 = new Thread(() -> { synchronized (kilit) { kilitliDeger = 5; kilitliHazir = true; } });
        t2.start();
        boolean g2 = bekle(() -> { synchronized (kilit) { return kilitliHazir; } });
        int d2; synchronized (kilit) { d2 = kilitliDeger; }
        System.out.println("synchronized   : gorundu=" + g2 + " deger=" + d2);
        t2.join();

        // 3. AtomicInteger: yazan set eder, okuyan get eder
        Thread t3 = new Thread(() -> atomikDeger.set(5));
        t3.start();
        boolean g3 = bekle(() -> atomikDeger.get() != -1);
        System.out.println("AtomicInteger  : gorundu=" + g3 + " deger=" + atomikDeger.get());
        t3.join();

        // 4. join: govde biter, okuyan ancak join'den sonra okur — duz alan, baska hicbir sey yok
        Thread t4 = new Thread(() -> joinDeger = 5);
        t4.start();
        t4.join();
        System.out.println("join           : gorundu=true deger=" + joinDeger);

        // 5. CountDownLatch: yazan countDown eder, okuyan await eder — duz alan
        CountDownLatch mandal = new CountDownLatch(1);
        Thread t5 = new Thread(() -> { mandalDeger = 5; mandal.countDown(); });
        t5.start();
        mandal.await();
        System.out.println("CountDownLatch : gorundu=true deger=" + mandalDeger);
        t5.join();

        // 6. ThreadLocal: her is parcacigi kendi kopyasini gorur, baskasininkini hic gormez
        Thread t6 = new Thread(() -> ozel.set(5));
        t6.start();
        t6.join();
        System.out.println("ThreadLocal    : cagiranin kendi kopyasi=" + ozel.get()
                + " (olusturulanin yazdigi 5 bu kopyada hic yok)");
    }
}
```

```
volatile       : gorundu=true deger=5
synchronized   : gorundu=true deger=5
AtomicInteger  : gorundu=true deger=5
join           : gorundu=true deger=5
CountDownLatch : gorundu=true deger=5
ThreadLocal    : cagiranin kendi kopyasi=0 (olusturulanin yazdigi 5 bu kopyada hic yok)
```

Altı satır da aynı biçimde okunuyor ama gerekçeleri farklı. **`volatile`** ve **`synchronized`**
için hem yazanın hem okuyanın aynı alana ya da aynı kilide dokunması gerekiyor — okuyan başka
bir alanı ya da başka bir kilidi denetleseydi hiçbir garanti kalmazdı; bu ikisi **ikisinin de**
sütununa yazılıyor. **`AtomicInteger`** aynı gerekçeyle üçüncü örnek: yazan `set`, okuyan
`get` çağırıyor, ikisi de aynı nesne üzerinden. **`CountDownLatch`** da öyle — yazan
`countDown`, okuyan `await` çağırmak zorunda; biri eksik olsa bağıntı kurulmaz. **`join`**
farklı bir yükü taşıyor: yazan tarafın yapması gereken **hiçbir ekstra şey yok**, sadece normal
biçimde bitmesi yetiyor; bütün yük okuyanın `join()` çağırmasına biniyor — bu yüzden **yalnız
okuyan** sütununda. `final` alan ise tam tersi: yükün tamamı kurucunun tamamlanmasında, yani
**yalnız yazan** tarafta. **`ThreadLocal`** görünürlük sorusunu hiç sormuyor — her iş
parçacığının kendi kopyası olduğu için ne yazanın ne okuyanın birbirine görünür olması diye
bir beklenti **yok**; bu yüzden **hiçbiri** sütununda. Ötekilerin yedisi bir değeri **paylaşarak**
güvenli hâle getirmenin yollarıydı, `ThreadLocal` ise paylaşmamayı bizzat araç hâline getiriyor
— aynı adı taşıyan bir değişkenin iki iş parçacığında iki ayrı yaşam sürmesi, görünürlük
sorununu çözmüyor, sorudan tamamen çıkarıyor.

| Kategori | Düzenekler | Sayı |
|---|---|---|
| yalnız yazan | `final` alan | 1 |
| yalnız okuyan | `join` | 1 |
| ikisinin de | `volatile`, `synchronized`, `AtomicInteger`, `CountDownLatch` | 4 |
| hiçbiri | senkronizasyonsuz alan, `ThreadLocal` | 2 |

Sekiz düzeneğin **dördü** her iki tarafın da harekete geçmesini istiyor, **biri** yükü tek
başına yazana, **biri** tek başına okuyana yüklüyor, **ikisi** ise görünürlük sorusunun
dışında kalıyor — biri çünkü hiçbir kural onu kurmuyor, öteki çünkü kural kurmaya gerek
duymayacak kadar izole.

`join` ile `CountDownLatch` yan yana konunca ilk bakışta aynı işi yapıyor gibi görünüyorlar,
ama tablodaki yerleri farklı — biri **yalnız okuyan**, öteki **ikisinin de** sütununda. Ayrım
şuradan geliyor: `join`'in kurduğu önce-olur bağıntısı iş parçacığının **kendi doğal sonuna**
bağlıdır, yazan tarafın ayrıca çağırdığı bir yöntem yoktur, gövde nasıl biterse bitsin bağıntı
kurulur. `CountDownLatch` ise böyle bir doğal sona bağlı değildir — sayaç, gövde bitmeden de
bitikten sonra da, yazan tarafın **açıkça** `countDown` çağırmasıyla düşer. Bu fark
`CountDownLatch`'i `join`'den daha esnek kılıyor (bir iş parçacığı bitmeden de ilerleme
sinyali verebiliyor) ama esnekliğin bedeli, yazan tarafa bir çağrı daha yüklemek.

#### Sınırlayıcı Ölçüm: Modellenen İki Sınır

- **EZ8** — Bu bölümde hiçbir kod koşturulmuyor. İki iddia da kural referansıyla ve önceki
  kurslara atıfla destekleniyor, ölçülerek değil — çünkü ölçmenin kendisi ya yanıltıcı ya da
  tanım gereği imkânsız.

Tablodaki `hiçbiri` satırının ilk kalemi, senkronizasyonsuz sıradan bir alan, kasıtlı olarak
koşturulmuyor. Bunun nedeni bir tembellik değil, bir sınırdır: senkronizasyonsuz bir alanı
yazan bir iş parçacığı ile onu bir döngüde bekleyerek okuyan bir başka iş parçacığı, aynı
program aynı makinede bile bazen görünürlüğü hemen gösterir, bazen göstermez — sonuç
derleyicinin o an verdiği optimizasyon kararına bağlıdır ve bu ders böyle bir kararı ölçmüyor,
çünkü M08/K04'ün kendi ölçü ekseni bu değildir. Böyle bir döngüyü burada çalıştırıp
"görünmedi" ya da "göründü" yazmak, bir kerelik bir gözlemi genel bir kural gibi sunmak olurdu;
oysa kural zaten yukarıda kuruldu ve bu alan için **hiçbir madde onu kapsamıyor**. Doğru
okuma şudur: bir alan bu listedeki yedi düzenekten birine girmiyorsa, o alanın görünürlüğü
konusunda hiçbir garanti yoktur — ne "görünür" ne "görünmez" garanti edilir, yalnız
**garanti edilmez.**

Bu, kısa süre çalışan bir denemenin **her zaman** görünürlüğü gösterip programcıyı
yanıltabileceği anlamına geliyor. `Yayimlama`'daki `bekle` döngüsü gerçek bir sınama olarak
kalabilmesinin nedeni, denetlediği alanların (`ucucuHazir`, `kilitliHazir`, `atomikDeger`)
üçünün de bir kural tarafından kapsanmasıdır; aynı döngü senkronizasyonsuz bir `boolean` alanı
denetleseydi, muhtemelen yine `gorundu=true` yazdırırdı — çünkü kısa ömürlü bir programda
derleyicinin döngüyü yeniden düzenlemesi için henüz fırsat doğmamış olabilir. Bu görünür
başarı bir kanıt değildir; yalnızca o çalıştırmada kuralın çiğnenmesinin bir bedeli
ödenmemiştir. Anında Derleme dersinin ölçtüğü karar burada da geçerlidir: aynı kaynak, hiç
değişmeden, çalışma zamanındaki bir kararla farklı davranabilir — fark şu ki o ders kararın
komut sayısını ölçtü, bu ders kararın **gözlemlenebilirliği** bozabileceğini yalnızca
belirtiyor.

İkinci sınır `volatile` alanı ilgilendiriyor ve ölçülen görünürlük garantisiyle karıştırılmaya
en açık olanıdır. `volatile`'ın verdiği şey **görünürlük ve sıralamadır**, **atomikliğin**
kendisi değildir. `oku-artır-yaz` üçlemesi kavram olarak M01/K05 İşletim Sistemi Kavramları kursunda
kuruldu; aynı üçleme bir `volatile` alanda da bölünebilir — iki iş parçacığı aynı `volatile`
sayaca `sayac++` yazdığında, her iki taraf da işlemi görüyor olsa bile, biri ötekinin
artırdığı değeri henüz okumadan kendi artırmasını yapabilir ve bir artırma kaybolur. Bu ders
bu iki taraf arasındaki gerçek bir çakışmayı **koşturmuyor**, çünkü sonucu ortama bağlı olurdu
ve kayıp artırmanın kaç kez gerçekleştiği bu dersin ölçü ekseninde hiçbir yere bağlanmıyor.
İddia bir kural olarak kalıyor: `volatile`, tek bir okuma ya da tek bir yazma için görünürlük
sağlar; okuma ve yazmayı birleştiren bir işlem için hiçbir şey sağlamaz. Bunun asıl aracı,
sonraki derste ölçülen `AtomicInteger`'ın `karşılaştır-ve-ata` yöntemidir.

#### Özet

- Görünürlük otomatik değildir; bir yazının bir okumaya görünmesi, ikisi arasında dilin tanıdığı
  bir önce-olur bağıntısının kurulmuş olmasına bağlıdır.
- Sekiz yayımlama düzeneğinden **dördü** (`volatile`, `synchronized`, `AtomicInteger`,
  `CountDownLatch`) hem yazanın hem okuyanın harekete geçmesini istiyor, **biri** (`final`
  alan) yükü yalnız yazana, **biri** (`join`) yükü yalnız okuyana yüklüyor, **ikisi**
  (senkronizasyonsuz alan, `ThreadLocal`) görünürlük sorusunun dışında kalıyor.
- `final` alanın bedeli kurulum anında ödenir: kurucu tamamlanınca okuyan taraf hiçbir ek
  disiplin uygulamadan doğru değeri görür.
- Senkronizasyonsuz bir alanın görünürlük kusuru koşturularak gösterilmez; bu ders bunu
  kasıtlı olarak modelliyor, çünkü sonucu ortama bağlı olurdu.
- `volatile` görünürlük ve sıralama verir, atomiklik vermez; oku-artır-yaz üçlemesi bir
  `volatile` alanda da bölünebilir ve bu ders bunu koşturarak değil kural olarak anlatıyor.

#### Sonraki Adım

Bu derste `synchronized` bloğu tabloya yalnız bir satır olarak girdi ve okuyanla yazanın
**aynı** kilidi kullanması gerektiği söylendi, ama "aynı kilit" ifadesinin kendisi hiç
sorgulanmadı — `kilit` bu derste tek bir statik alandı ve iki iş parçacığının aynı nesneyi
kullandığı baştan verilmişti. Sıradaki ders bu varsayımı kaldırıyor ve izleyici kilidinin
kime ait olduğunu ölçüyor: aynı senkronize yöntem iki ayrı örnekte çağrıldığında kaç ayrı
kilit devreye giriyor ve bir iş parçacığı kendi tuttuğu kilide yeniden girdiğinde bu kaç kez
sayılıyor?

### Senkronizasyon

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/senkronizasyon

İzleyici kilidine yeniden girmek tutma sayısını 0'dan 2'ye çıkarıp geri indiriyor. Aynı senkronize yöntem aynı örnekte çağrıldığında tek kilit, iki ayrı örnekte ya da bir örnek yöntemiyle bir sınıf yönteminde çağrıldığında iki ayrı kilit devreye giriyor; üç kurulumun ikisinde kilit sayısı ikiye çıkıyor.

Önceki ders `synchronized` bloğunu tabloya tek bir satır olarak koydu ve okuyanla yazanın
**aynı** kilidi kullanması gerektiğini söyledi — ama o derste `kilit` tek bir statik alandı,
iki iş parçacığının aynı nesneyi paylaştığı baştan verilmişti. Bu ders o varsayımı kaldırıyor.
`synchronized` anahtar sözcüğü kaynakta hangi kilidin kullanılacağını açıkça yazmaz; bir
örnek yöntemi çağrıldığında kullanılan kilit **o çağrıdaki nesneye** bağlıdır ve aynı kaynak
satırı, hangi nesne üzerinden çağrıldığına göre iki ayrı kilide karşılık gelebilir.

Bu belirsizlik iki ayrı soruyu birbirinden ayırmayı gerektiriyor. Birincisi: bir iş parçacığı
şu an bir kilidi tutuyor mu ve tutuyorsa kaç kez? Bu, tek bir iş parçacığının kendi
durumuyla ilgili bir sorudur ve bu dersin ilk bölümü bunu ölçüyor. İkincisi: iki ayrı çağrı
**aynı** kilidi mi kullanıyor? Bu, kaynak metne bakarak değil, iki çağrının hangi nesne
üzerinden yapıldığına bakarak yanıtlanabilir bir sorudur ve dersin ikinci bölümü bunu üç
kurulumda sayıyor.

#### İzleyici Kilidini Kim Tutuyor

- **EZ9** — `Thread.holdsLock(nesne)` yalnız çağıran iş parçacığının o nesnenin kilidini o an
  tutup tutmadığını sorar; başka bir iş parçacığının tutup tutmadığını sormaz, bu bilgi API
  ile hiç sunulmaz. Bir kilidin **kaç kez** tutulduğu ise `ReentrantLock.getHoldCount()` ile
  ölçülür — izleyici kilidinin kendisi bu sayıyı dışarı vermez, bu yüzden ölçüm açık kilitle
  yapılıyor.

```java
// KilitTutma.java — izleyici kilidini kim tutuyor holdsLock ile, yeniden girmenin tutma sayisi ReentrantLock ile olculur
import java.util.concurrent.locks.ReentrantLock;

public class KilitTutma {
    static final Object izleyici = new Object();
    static final ReentrantLock acikKilit = new ReentrantLock();

    public static void main(String[] args) {
        System.out.println("kilit tutuluyor mu (baslamadan once) : " + Thread.holdsLock(izleyici));
        synchronized (izleyici) {
            System.out.println("kilit tutuluyor mu (blok icinde)      : " + Thread.holdsLock(izleyici));
        }
        System.out.println("kilit tutuluyor mu (bloktan sonra)    : " + Thread.holdsLock(izleyici));

        System.out.println();
        System.out.println("baslamadan once     tutmaSayisi=" + acikKilit.getHoldCount());
        acikKilit.lock();
        try {
            System.out.println("1. giristen sonra   tutmaSayisi=" + acikKilit.getHoldCount());
            acikKilit.lock(); // ayni is parcacigi ayni kilide yeniden giriyor
            try {
                System.out.println("2. giristen sonra   tutmaSayisi=" + acikKilit.getHoldCount());
            } finally {
                acikKilit.unlock();
                System.out.println("ic cikistan sonra   tutmaSayisi=" + acikKilit.getHoldCount());
            }
        } finally {
            acikKilit.unlock();
            System.out.println("dis cikistan sonra  tutmaSayisi=" + acikKilit.getHoldCount());
        }
    }
}
```

```
kilit tutuluyor mu (baslamadan once) : false
kilit tutuluyor mu (blok icinde)      : true
kilit tutuluyor mu (bloktan sonra)    : false

baslamadan once     tutmaSayisi=0
1. giristen sonra   tutmaSayisi=1
2. giristen sonra   tutmaSayisi=2
ic cikistan sonra   tutmaSayisi=1
dis cikistan sonra  tutmaSayisi=0
```

İlk üç satır kilidin **kimliğini** ölçüyor: `synchronized (izleyici)` bloğuna girildiği an
`holdsLock` `true`'ya dönüyor, bloktan çıkıldığı an `false`'a geri düşüyor — kilit tutmanın
kendisi blok sınırına bağlı, ayrıca bir bırakma çağrısı gerektirmiyor. Alttaki beş satır ise
**yeniden girilebilirliği** ölçüyor. `ReentrantLock`, adını da buradan alıyor: aynı iş
parçacığı zaten tuttuğu kilide ikinci kez girdiğinde engellenmiyor, tutma sayısı `1`'den
`2`'ye çıkıyor. İç bloktan çıkıldığında kilit **hâlâ tutuluyor** — sayı `1`'e düşüyor, sıfıra
değil — ve ancak dış bloktan da çıkıldığında sayı `0`'a iniyor. Bu, izleyici kilidinin de
(built-in `synchronized`) taşıdığı ama dışarı sayı olarak vermediği bir özelliktir: bir
yöntem kendi çağırdığı başka bir senkronize yöntemde aynı kilide yeniden girdiğinde
kilitlenmiyor, çünkü kilit **iş parçacığına** aittir, çağrıya değil — aynı iş parçacığı kendi
tuttuğu kilidi tekrar isteyebiliyor.

Yeniden girilebilirlik olmasaydı, senkronize bir yöntemin kendi sınıfındaki başka bir
senkronize yöntemi çağırması **kendi kendini kilitlemek** anlamına gelirdi — bir iş
parçacığı, zaten sahip olduğu bir kilidi ikinci kez istediği için sonsuza kadar beklerdi. Bu,
tek bir iş parçacığı içinde oluşabilecek en küçük kilitlenme biçimidir ve tutma sayısının
varlığı tam olarak bunu önlüyor: çerçeve her `lock()` çağrısında "bu kilit zaten benim mi"
diye soruyor, yanıt evetse engellemiyor, sayıyı artırıp geçiyor. `holdsLock`'ın kendisi bu
sayıyı dışarı vermiyor, çünkü sorduğu soru farklı — yalnız "tutuyor muyum" diye soruyor,
"kaç kez tutuyorum" diye sormuyor; iki soru da bu derste ayrı araçlarla yanıtlanıyor.

#### Kilit Nesneye Aittir, Koda Değil

- **EZ10** — Üç kurulumun üçü de aynı iki satırlık günlük desenini kullanıyor: bir iş
  parçacığı girdiğinde `"X girdi"` yazıyor, çıkarken `"X cikti"` yazıyor. Günlüğün **sırası**,
  hangi kilidin paylaşıldığını kanıtlıyor — süre hiçbir yerde ölçülmüyor.

```java
// Kutu.java — kilit nesneye aittir, koda degil: senkronize yontem ornek kilidini, static yontem sinif kilidini kullanir
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class Kutu {
    synchronized void gir(List<String> gunluk, String etiket, CountDownLatch girdi, CountDownLatch devamEt)
            throws InterruptedException {
        gunluk.add(etiket + " girdi");
        if (girdi != null) girdi.countDown();
        if (devamEt != null) devamEt.await();
        gunluk.add(etiket + " cikti");
    }

    static synchronized void sinifGir(List<String> gunluk, String etiket) {
        gunluk.add(etiket + " girdi (sinif kilidi)");
        gunluk.add(etiket + " cikti (sinif kilidi)");
    }
}
```

`gir`, kendini çağıran iş parçacığını isteğe bağlı olarak durdurabiliyor: `girdi` verilmişse
girer girmez haber veriyor, `devamEt` verilmişse serbest bırakılana kadar kilidi elinde
tutuyor. Üç kurulum bu iki kolu farklı biçimde kullanıyor.

```java
// KacAyriKilit.java — uc kurulumda kac ayri kilit devreye giriyor
import java.util.*;
import java.util.concurrent.CountDownLatch;

public class KacAyriKilit {
    static List<String> kurulum1AyniOrnek() throws InterruptedException {
        Kutu kutu = new Kutu();
        List<String> gunluk = Collections.synchronizedList(new ArrayList<>());
        CountDownLatch girdiA = new CountDownLatch(1);
        CountDownLatch bekleA = new CountDownLatch(1);

        Thread a = new Thread(() -> { try { kutu.gir(gunluk, "A", girdiA, bekleA); } catch (InterruptedException e) { } });
        a.start();
        girdiA.await(); // A kilidi aldi ve tutuyor

        Thread b = new Thread(() -> { try { kutu.gir(gunluk, "B", null, null); } catch (InterruptedException e) { } });
        b.start(); // B ayni kilidi istiyor, A birakmadikca giremez

        bekleA.countDown(); // A'yi serbest birak
        a.join();
        b.join();
        return gunluk;
    }

    static List<String> kurulum2FarkliOrnek() throws InterruptedException {
        Kutu kutu1 = new Kutu();
        Kutu kutu2 = new Kutu();
        List<String> gunluk = Collections.synchronizedList(new ArrayList<>());
        CountDownLatch girdiA = new CountDownLatch(1);
        CountDownLatch bekleA = new CountDownLatch(1);
        CountDownLatch bittiB = new CountDownLatch(1);

        Thread a = new Thread(() -> { try { kutu1.gir(gunluk, "A", girdiA, bekleA); } catch (InterruptedException e) { } });
        a.start();
        girdiA.await(); // A, kutu1'in kilidini tutuyor

        Thread b = new Thread(() -> {
            try { kutu2.gir(gunluk, "B", null, null); bittiB.countDown(); }
            catch (InterruptedException e) { }
        });
        b.start();
        bittiB.await(); // B, A hala beklerken tamamlandi -- ayri bir kilit kullaniyor demektir

        bekleA.countDown();
        a.join();
        b.join();
        return gunluk;
    }

    static List<String> kurulum3OrnekVeSinif() throws InterruptedException {
        Kutu kutu = new Kutu();
        List<String> gunluk = Collections.synchronizedList(new ArrayList<>());
        CountDownLatch girdiA = new CountDownLatch(1);
        CountDownLatch bekleA = new CountDownLatch(1);
        CountDownLatch bittiB = new CountDownLatch(1);

        Thread a = new Thread(() -> { try { kutu.gir(gunluk, "A", girdiA, bekleA); } catch (InterruptedException e) { } });
        a.start();
        girdiA.await(); // A, ornek kilidini tutuyor

        Thread b = new Thread(() -> { Kutu.sinifGir(gunluk, "B"); bittiB.countDown(); });
        b.start();
        bittiB.await(); // B, A hala beklerken tamamlandi -- sinif kilidi ayri

        bekleA.countDown();
        a.join();
        b.join();
        return gunluk;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1. ayni ornek uzerinde iki cagri:");
        System.out.println("   " + kurulum1AyniOrnek());
        System.out.println("2. iki ayri ornek uzerinde iki cagri:");
        System.out.println("   " + kurulum2FarkliOrnek());
        System.out.println("3. ornek yontemi ve sinif yontemi:");
        System.out.println("   " + kurulum3OrnekVeSinif());
    }
}
```

```
1. ayni ornek uzerinde iki cagri:
   [A girdi, A cikti, B girdi, B cikti]
2. iki ayri ornek uzerinde iki cagri:
   [A girdi, B girdi, B cikti, A cikti]
3. ornek yontemi ve sinif yontemi:
   [A girdi, B girdi (sinif kilidi), B cikti (sinif kilidi), A cikti]
```

Her üç kurulumda da A, `bekleA.countDown()` çağrılana kadar kilidi bırakmıyor. Birinci
kurulumda B'nin günlüğü **A tamamen çıktıktan sonra** başlıyor — B, `gir` yönteminin ilk
satırına bile ulaşamıyor, çünkü senkronize yöntemin gövdesine girmek için önce aynı kilidi
alması gerekiyor ve o kilit A'da. İkinci ve üçüncü kurulumlarda ise B'nin **hem girişi hem
çıkışı**, A henüz kilidi bırakmadan günlüğe düşüyor — B'nin tamamlanması A'nın bırakmasını hiç
beklemiyor. Bu, iki ayrı kilidin devrede olduğunun doğrudan kanıtı: eğer aynı kilit
paylaşılıyor olsaydı, B de birinci kurulumdaki gibi A'nın çıkışını beklemek zorunda kalırdı.

Kanıtın gücü tam olarak burada yatıyor: hiçbir satır "iki ayrı kilit var" diye iddia
etmiyor, yalnız bir sıralama gözleniyor ve o sıralama başka türlü açıklanamıyor. B'nin
`bittiB.countDown()` çağırabilmiş olması, `gir` ya da `sinifGir` yönteminin gövdesini
**tamamen** çalıştırmış olduğu anlamına geliyor; bu da o gövdeye giren senkronize bloğun
kilidini almış ve bırakmış olduğu anlamına geliyor. A ise aynı anda kendi kilidini hâlâ
tutuyor — `bekleA` henüz düşmedi. İki iş parçacığı da bir kilit tutarken üçüncü kurulumda
kilitlenme oluşmuyor, çünkü iki kilit ayrı nesnelere ait; birinci kurulumda ise B'nin
`girdi` parametresi `null` olduğu için B'nin ne zaman kilide girdiğine dair bağımsız bir
sinyal yok — ama günlükteki sıra zaten tek olasılığı bırakıyor.

| Kurulum | Çağrılar | Ayrı kilit sayısı |
|---|---|---|
| 1. aynı örnek | `kutu.gir` × 2 | 1 |
| 2. iki ayrı örnek | `kutu1.gir`, `kutu2.gir` | 2 |
| 3. örnek + sınıf yöntemi | `kutu.gir`, `Kutu.sinifGir` | 2 |

Üç kurulumun **ikisinde** ayrı kilit sayısı ikiye çıkıyor. Örnek kilidi çağrıldığı nesneye
bağlıyken, sınıf kilidi tek bir `Class` nesnesine bağlıdır ve bütün örnekler arasında
paylaşılır — ama örnek kilidiyle **aynı** kilit değildir. Bir sınıfın hem örnek hem statik
senkronize yöntemi varsa, aynı anda bir iş parçacığı örnek yöntemindeyken başka bir iş
parçacığı statik yöntemi çağırabilir; ikisi birbirini hiç engellemez.

Bu ayrım, `synchronized` kullanırken hangi nesnenin kilit olarak seçildiğinin **tasarım
kararı** olduğunu gösteriyor. `Kutu.gir` örtük olarak `this` üzerinde kilitleniyor — bu, bir
sınıfın senkronize yöntemleri için varsayılan davranış. Aynı kilit, sınıfın dışından erişilen
bir referanssa (`this` genel bir API'nin parçasıysa), sınıfın hiç bilmediği bir çağıran da
`synchronized (kutu) { ... }` yazarak aynı kilide talip olabilir ve içeriden beklenmeyen bir
çekişme yaratabilir. `Kutu.gir` bu dersin ölçümü için `this`'i kullanıyor, ama üçüncü kurulum
zaten bunun tersini de gösteriyor: örnek kilidi ile sınıf kilidi birbirinden bağımsız olduğu
gibi, bir sınıf kendi özel bir kilit nesnesini de kullanabilir — o zaman dışarıdan hiçbir
çağıran o kilide `synchronized` ile ulaşamaz, çünkü referansın kendisi hiç dışarı çıkmaz.

#### Sınırlayıcı Ölçüm: Kilit Sıra Vermez

- **EZ11** — Bu bölümde bekleyen iş parçacıklarının **hangi sırayla** kilide gireceği
  koşturularak ölçülmüyor; ölçülseydi sonucu çizelgeleyiciye bağlı olurdu ve bu, kursun
  kurallarına aykırı bir yarış koşulu koşturmak olurdu.

Yukarıdaki üç kurulumun hepsinde yalnız iki iş parçacığı vardı ve hangisinin bekleyeceği
baştan biliniyordu. Aynı kilidi isteyen **üçüncü, dördüncü** bir iş parçacığı eklendiğinde,
kilit serbest kalınca bekleyenlerden **hangisinin** gireceği `synchronized`'ın kendisi
tarafından belirtilmez — karşılıklı dışlamayı garanti eder, sırayı garanti etmez.
`ReentrantLock`'ın kurucusuna `true` verilerek talep edilen **adil kilit** seçeneği bekleme
sırasına yaklaşmayı hedefler, ama bu ders o sırayı bir koşumdan ölçmüyor; seçeneğin varlığı
ve amacı burada yalnız **modelleniyor**. Aynı kilidi isteyen çok sayıda iş parçacığının
birbirini süresiz bekletmesi ayrı bir kusurdur ve M01/K05 İşletim Sistemi Kavramları kursunda kavram
olarak zaten kuruldu; bu ders onu yeniden kurmuyor, yalnız kilidin sıra **vermediğini** ölçüm
dışı bırakarak hatırlatıyor.

Adil kilidin neden varsayılan olmadığı da modellenen bir gerekçedir, koşulan bir sayı değil:
sırayı zorlamak, her `lock()` çağrısında bekleme kuyruğunun başını kontrol etmeyi
gerektiriyor ve bu kontrol yalnız bekleyen varsa değil **her seferinde** işliyor. `synchronized`
ve `ReentrantLock`'ın adil olmayan biçimi, boşta bir kilide yeni gelen bir çağrının kuyruğu
hiç yoklamadan doğrudan girmesine izin veriyor. Bu ders bu iki seçenek arasındaki maliyet
farkını ölçmüyor, çünkü ölçmek demek gerçek bir sürüm karşılaştırması yapmak demek olurdu;
tek belirttiği, sıranın **isteğe bağlı** bir garanti olduğu ve varsayılanın bunu vermediğidir.
Bir kilidi bekleyen iş parçacıklarının sayısı arttıkça hangi tasarımın seçileceği bu dersin
sorusu değil — bu ders yalnız, seçim yapılmadığı sürece hangi garantinin **olmadığını**
sabitliyor.

Üçüncü kurulumdaki sınıf kilidi de kendi içinde tekildir: `Kutu` sınıfının **bütün** statik
senkronize yöntemleri aynı `Class` nesnesinin kilidini paylaşır, tıpkı bir örneğin bütün
örnek senkronize yöntemlerinin o örneğin kilidini paylaşması gibi. Bir sınıfın iki ayrı statik
senkronize yöntemi aynı anda iki ayrı iş parçacığından çağrılsaydı, bu kurulum onları da
birinci kurulumdaki gibi sıraya sokardı — orada ölçülmedi, çünkü bu dersin sorusu zaten örnek
kilidiyle sınıf kilidinin **birbirinden ayrı** olup olmadığıydı, sınıf kilidinin kendi içinde
tekil olup olmadığı değil.

#### Özet

- `Thread.holdsLock` bir kilidin çağıran tarafından o an tutulup tutulmadığını belirlenimci
  biçimde söyler; `ReentrantLock.getHoldCount()` yeniden girme derinliğini sayar ve iç içe iki
  girişte `0`'dan `2`'ye çıkıp geri `0`'a iner.
- Bir kilit iş parçacığına aittir, çağrıya değil: aynı iş parçacığı zaten tuttuğu kilide
  yeniden girdiğinde engellenmez.
- Örnek kilidi çağrıldığı **nesneye** bağlıdır: aynı senkronize yöntem iki ayrı örnekte
  çağrıldığında iki ayrı kilit kullanılır; sınıf kilidi bunlardan **ayrı üçüncü** bir kilittir.
- Üç kurulumun yalnız birinde (aynı örnek üzerindeki iki çağrı) tek kilit paylaşılıyor;
  kalan ikisinde ayrı kilit sayısı ikiye çıkıyor.
- Kilit karşılıklı dışlama ve görünürlük sağlar, bekleyenler arasında sıra vermez; sıra
  isteniyorsa adil kilit seçeneği modellenir, koşumdan ölçülmez.

#### Sonraki Adım

Bu üç kurulumda da kilidi elle almak ve bırakmak çağıranın işiydi — `synchronized` bloğu ya
da `acikKilit.lock()`/`unlock()` çifti her seferinde elle yazıldı ve her yazımda kilidin
serbest bırakılmasını garanti etmek `finally` bloğuna düştü. `KacAyriKilit`'teki her üç
kurulum da bu deseni tekrarladı; gerçek bir programda bu tekrar, unutulan bir `unlock()`
çağrısının kilidi süresiz tuttuğu bir kusura dönüşebilir. Sıradaki ders bu elle tutma işini
standart kitaplığın hazır tiplerine devrediyor: bir sayaç kendi kilidini kendi yönetip
yönetmediğini, bir mandal bekleyeni ne zaman serbest bıraktığını, bir liste yazarken kimin
ödediğini ölçüyor — kilit artık kaynak metinde görünmüyor, ama iş hâlâ birinin üzerine
düşüyor; sıradaki soru bu yükün hangi tarafa yüklendiği.

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

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/eszamanlilik-yardimcilari

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ü ekleme dizisinin arasına serpiştirilerek alınıyor: ilk üçü
  kendinden sonraki eklemeden **önce**, dördüncüsü üç eklemenin de **ardından**. 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
```

İlk üç yineleyici kendinden sonraki `liste.add` çağrısından **önce** alınıyor, dördüncüsü üç
eklemenin ardından; 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 zayıf tutarlı yineleyicisi ile `CopyOnWriteArrayList`'in anlık kopya
üzerinden gezinmesi 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?

### Yürütücü Servisleri

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/yurutucu-servisleri

Aynı execute çağrısının havuz boşken kendi iş parçacığında, doluyken görevi veren iş parçacığında koşturduğu; ayrık iş parçacığı kimliği sayısının sabit havuzda çekirdek sayısıyla sınırlı kaldığı ve reddetme yordamının çağıranın seçtiği bir parametre olduğu; execute ile atılan bir istisnanın yakalanmamış istisna işleyicisine, submit ile atılanın yalnızca get çağrılınca çağırana taşındığı ölçülür.

Önceki ders eşzamanlılık yardımcılarının işi çağırana ne kadar yükleyip ne kadar başka bir
tarafa devrettiğini ölçtü: `AtomicInteger`'da yeniden deneme çağıranda kalıyordu,
`CountDownLatch` ve `Semaphore`'da bekleyen engelleniyor, sayacı başka bir taraf
düşürüyordu — ama görevin **hangi iş parçacığında** koşacağına hâlâ çağıranın kendisi karar
veriyordu, `run()` ile mi `start()` ile mi çağıracağını seçerek. Bu ders o seçimi bir
servise devrediyor. Yürütücü servisi bir görev alır ve onu kimin koşturacağına kendisi karar
verir; soru aynıdır — bu adımı hangi iş parçacığı koşturuyor — ama yanıt artık çağrı
satırında değil, yürütücünün o anki durumundadır.

Kursun en sert örneği burada duruyor, çünkü iki durumun kodu **birebir aynıdır**. Havuz
boşken ve doluyken aynı `execute` çağrısı yazılır; farkı yapan tek şey çağrı yerinde hiç
görünmeyen bir şey — havuzun o anki doluluğu.

Bir yürütücü servisi üç parçadan kurulur: sınırlı sayıda iş parçacığı, bekleyen görevleri
tutan bir kuyruk ve kuyruk da dolduğunda devreye giren bir reddetme yordamı. Önceki derste
`run()` ile `start()` arasındaki fark tek bir yöntem adıydı; burada fark daha da küçülüyor —
aynı yöntem adı, aynı imza, aynı çağrı satırı, ama iki ayrı sonuç. Bu üç bölüm sırayla o üç
parçayı ölçüyor: kaç ayrı iş parçacığı gerçekten koşuyor, doluyken görev kime düşüyor ve
doluluk anını hangi taraf yönetiyor.

#### Görevi Kim Koşturuyor: Ayrık Kimlik Sayısı

`ExecutorService`, bir görev listesi alan ve her görevi hangi iş parçacığının koşturacağına
kendisi karar veren bir soyutlamadır. Çağıran görevi bir `Callable` ya da `Runnable` olarak
yazar; kimin koşturacağını yürütücünün kendi kuralı belirler. Bu kuralı sayıyla görmek için
ayrık iş parçacığı kimliği ölçülüyor: her görev koştuğu iş parçacığını paylaşılan bir kümeye
ekliyor.

**EZ16.** Ayrık iş parçacığı kimliği bir kümeyle sayılır; kümeye eklenen her öge yalnızca
nesne kimliğiyle (`==` eşdeğeri) ayırt edilir, ad ya da numarası hiç basılmaz.
**EZ17.** Sabit boyutlu bir yürütücü, çekirdek iş parçacığı sayısına ulaşana kadar var olan
boşta bir iş parçacığını kullanmak yerine yeni bir iş parçacığı oluşturur; bu kural
çizelgeleyiciye değil yürütücünün kendisine aittir.
**EZ18.** Çekirdek ile en büyük iş parçacığı sayısı eşit verildiğinde havuz bu sayıyı
hiçbir zaman aşmaz; kuyruğa giren görevler var olan iş parçacıklarınca sırayla işlenir.

```java
// Kimlikler.java — ayrik is parcacigi kimligini == esdegeriyle sayan kume (yardimci kaynak, main yok)
class Kimlikler {
    static java.util.Set<Thread> yeniKume() {
        return java.util.Collections.synchronizedSet(
                java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()));
    }
}
```

```java
// AyrikKimlikSayisi.java — sabit boyutlu bir havuzda gorev sayisi buyuse de ayrik kimlik sayisi sabit kalir
import java.util.*;
import java.util.concurrent.*;

public class AyrikKimlikSayisi {
    static int ayrikKimlik(ExecutorService yurutucu, int gorevSayisi) throws Exception {
        Set<Thread> gorulen = Kimlikler.yeniKume();
        List<Callable<Void>> gorevler = new ArrayList<>();
        for (int i = 0; i < gorevSayisi; i++) {
            gorevler.add(() -> { gorulen.add(Thread.currentThread()); return null; });
        }
        yurutucu.invokeAll(gorevler);
        return gorulen.size();
    }

    public static void main(String[] args) throws Exception {
        try (ExecutorService dortluHavuz1 = Executors.newFixedThreadPool(4)) {
            System.out.println("4 gorev, sabit havuz(4)  -> ayrik kimlik: " + ayrikKimlik(dortluHavuz1, 4));
        }
        try (ExecutorService dortluHavuz2 = Executors.newFixedThreadPool(4)) {
            System.out.println("8 gorev, sabit havuz(4)  -> ayrik kimlik: " + ayrikKimlik(dortluHavuz2, 8));
        }
        try (ExecutorService birliHavuz = Executors.newFixedThreadPool(1)) {
            System.out.println("4 gorev, sabit havuz(1)  -> ayrik kimlik: " + ayrikKimlik(birliHavuz, 4));
        }
    }
}
```

```
4 gorev, sabit havuz(4)  -> ayrik kimlik: 4
8 gorev, sabit havuz(4)  -> ayrik kimlik: 4
4 gorev, sabit havuz(1)  -> ayrik kimlik: 1
```

Ölçüm `invokeAll` yöntemini kullanıyor, tek tek `submit` çağırıp elde kalan gelecekleri
beklemek yerine — çünkü `invokeAll` verilen bütün görevler bitmeden dönmüyor. Bu, sırayı
çizelgeleyiciye bırakmadan üç kurulumu da güvenle karşılaştırmanın yolu: kümenin son boyutu
okunduğunda, o kurulumdaki bütün görevlerin gerçekten koştuğu ve kendi kimliğini kümeye
eklediği kesin.

`try` bloğunun parantezindeki `ExecutorService`, Standart Kütüphane ve Akışlar kursunda
kurulan kaynakla deneme kalıbına `AutoCloseable` gerçekleştirimiyle katılır; blok bitince
kapanış çağrısı burada da örtük ve otomatiktir. Üç satır aynı soruyu üç ayrı kurulumda
soruyor: dört görev dört büyüklüğündeki havuzda dört ayrı kimlik üretiyor — her görev kendi
iş parçacığını buluyor (EZ17). Sekiz görev aynı büyüklükteki havuzda **yine** dört ayrı
kimlik üretiyor; kalan dört görev yeni bir iş parçacığı doğurmuyor, var olan dördünden birine
düşüyor (EZ18). Tek iş parçacıklı havuzda dört görev tek bir kimliğe düşüyor — havuz
büyüklüğü kaç görev geldiğinden bağımsız bir üst sınırdır. Sayı sabit kalıyor çünkü kural
çizelgeleyiciye değil yürütücünün kendi kurucusuna aittir: `newFixedThreadPool(4)` çekirdek
ile en büyük iş parçacığı sayısını eşit ve `4` vererek bu sınırı koyuyor.

#### Havuz Doluyken Görevi Veren Taraf Koşturur

Aynı soruyu havuz doluyken sormak için sınırlı bir kuyruk ve tek iş parçacıklı bir havuz
kuruluyor. İlk görev bir `CountDownLatch` üzerinde bilinçli olarak bekletiliyor; bu, havuzun
tek iş parçacığını meşgul tutmanın ve kuyruğun doluluğunu **koşturarak** kurgulamanın yolu —
sıra çizelgeleyiciye bırakılmıyor.

```java
// HavuzDolulugu.java — ayni execute cagrisi, farki havuzun o anki doluluğu belirliyor
import java.util.concurrent.*;

public class HavuzDolulugu {
    static String kim(Thread cagiran) {
        return Thread.currentThread() == cagiran ? "cagiran" : "olusturulan";
    }

    public static void main(String[] args) throws Exception {
        Thread cagiran = Thread.currentThread();
        CountDownLatch tut = new CountDownLatch(1);
        ThreadPoolExecutor havuz = new ThreadPoolExecutor(
                1, 1, 0, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(1),
                gorev -> new Thread(gorev),
                new ThreadPoolExecutor.CallerRunsPolicy());

        havuz.execute(() -> { try { tut.await(); } catch (InterruptedException e) { } });
        havuz.execute(() -> System.out.println("havuza verilen gorev      -> " + kim(cagiran)));
        havuz.execute(() -> System.out.println("dolu havuzda reddedilen   -> " + kim(cagiran)));

        tut.countDown();
        havuz.shutdown();
        havuz.awaitTermination(10, TimeUnit.SECONDS);
    }
}
```

```
dolu havuzda reddedilen   -> cagiran
havuza verilen gorev      -> olusturulan
```

Üç `execute` çağrısı sözdiziminde birbirinin aynısı. Birinci çağrı havuzun tek iş
parçacığını `tut.await()` ile meşgul ediyor. İkinci çağrı geldiğinde iş parçacığı hâlâ
meşgul ama kuyrukta bir yer boş — görev kuyruğa giriyor ve daha sonra, `tut.countDown()`
çağrıldıktan sonra havuzun kendi iş parçacığında koşuyor: **oluşturulan**. Üçüncü çağrı
geldiğinde hem iş parçacığı meşgul hem kuyruk dolu; yürütücünün genişleyecek yeri yok ve
görev reddetme yordamına düşüyor. Yordam `CallerRunsPolicy` olduğu için reddedilen görev
`execute` çağrısını yazan iş parçacığında, **hemen orada**, senkron olarak koşuyor —
çıktıda bu satır önce görünüyor, çünkü üçüncü görev `tut.countDown()` çağrılmadan, ikinci
görev henüz kuyruktan çıkmadan koşturuluyor. Kodda hiçbir dallanma yok; farkı yaratan tek
şey çağrı anındaki doluluk.

#### Reddetme Yordamı Çağıranın Seçtiği Bir Parametredir

`CallerRunsPolicy` reddedilen görevi sessizce çağırana yüklüyor; bu, dört hazır seçenekten
yalnızca biri. Standart kütüphane üç yordamı daha tanımlıyor: biri reddedilen görevi hiç
koşturmadan sessizce düşürüyor, biri kuyruktaki **en eski** bekleyen görevi atıp yeni geleni
kuyruğa alıyor, biri de burada gösterilen gibi bir istisna fırlatıyor. Dördü de aynı anı —
iş parçacığı meşgul, kuyruk dolu — farklı bir kararla yanıtlıyor; hangisinin seçileceği
görevin türünden değil, yürütücüyü kuran tarafın o görevi kaybetmeyi göze alıp
alamayacağından geliyor.

**EZ19.** Sınırlı kuyruk dolduğunda ve havuz üst sınırındayken gelen görev reddetme
yordamına düşer; `CountDownLatch` ile ilk görev bilinçli olarak bekletilerek kuyruğun ve
havuzun doluluğu koşturarak kurgulanır.
**EZ20.** Reddetme yordamı yürütücüyü kuran tarafın seçtiği bir parametredir, görev
tarafından belirlenmez; aynı doluluk anında iki ayrı yordam iki ayrı sonuç verir.

```java
// ReddetmeYordami.java — ayni dolu havuz aninda iki reddetme yordami iki ayri sonuc veriyor
import java.util.concurrent.*;

public class ReddetmeYordami {
    static String kim(Thread cagiran) {
        return Thread.currentThread() == cagiran ? "cagiran" : "olusturulan";
    }

    static ThreadPoolExecutor doluHavuz(RejectedExecutionHandler yordam, CountDownLatch tut) {
        ThreadPoolExecutor havuz = new ThreadPoolExecutor(
                1, 1, 0, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(1),
                gorev -> new Thread(gorev),
                yordam);
        havuz.execute(() -> { try { tut.await(); } catch (InterruptedException e) { } });
        havuz.execute(() -> { });
        return havuz;
    }

    public static void main(String[] args) throws Exception {
        Thread cagiran = Thread.currentThread();

        CountDownLatch tut1 = new CountDownLatch(1);
        ThreadPoolExecutor caginirsaCalisir = doluHavuz(new ThreadPoolExecutor.CallerRunsPolicy(), tut1);
        try {
            caginirsaCalisir.execute(() -> System.out.println("CallerRunsPolicy  -> " + kim(cagiran)));
        } finally {
            tut1.countDown();
            caginirsaCalisir.shutdown();
            caginirsaCalisir.awaitTermination(10, TimeUnit.SECONDS);
        }

        CountDownLatch tut2 = new CountDownLatch(1);
        ThreadPoolExecutor keserAtar = doluHavuz(new ThreadPoolExecutor.AbortPolicy(), tut2);
        try {
            keserAtar.execute(() -> System.out.println("bu satir hic basilmiyor"));
            System.out.println("AbortPolicy       -> dusmedi");
        } catch (RejectedExecutionException e) {
            System.out.println("AbortPolicy       -> " + e.getClass().getSimpleName());
        } finally {
            tut2.countDown();
            keserAtar.shutdown();
            keserAtar.awaitTermination(10, TimeUnit.SECONDS);
        }
    }
}
```

```
CallerRunsPolicy  -> cagiran
AbortPolicy       -> RejectedExecutionException
```

Aynı doluluk kurgusu — bir görev iş parçacığını meşgul ediyor, kuyrukta bir görev bekliyor —
iki yürütücüde de birebir aynı. Değişen tek şey kurucuya verilen dördüncü parametre.
`CallerRunsPolicy` reddedilen görevi sessizce çağırana devrediyor; `AbortPolicy` aynı anı
bir istisnaya çeviriyor ve reddedilen görev **hiç koşmuyor** — üçüncü satır hiçbir zaman
basılmıyor, `catch` bloğu bunu doğruluyor. İkisi de "dolu" durumuna bir yanıt, ama yanıtı
seçen görev değil, yürütücüyü kuran koddur.

Kuyruk ve havuz büyüklüğü neden sınırsız bırakılmıyor da bu kararla uğraşılıyor? Çünkü her
iş parçacığı bir bedel taşıyor — kendi çağrı çerçevesi, kendi yığıtı, çizelgeleyicinin
izlediği bir kayıt. Sınırsız bir kuyruk bu sorunu yalnızca ertelemiş olur: görev üretimi
tüketimden hızlıysa kuyruk sonsuza kadar büyür ve bellek tükenir; sınırsız bir havuz ise aynı
sorunu iş parçacığı sayısına taşır. Sınırlı kuyruk ve sınırlı havuz, dolma anını
**görünür** kılıp bir karara zorluyor — reddetme yordamı bu kararın adı.

#### İstisnanın Yolu: execute mi, submit mi

Bir görev çalışırken istisna fırlatırsa bu istisna nereye düşer? Yanıt görevi hangi
yöntemle verdiğine bağlı — ve iki yöntemden yalnızca biri istisnayı kendiliğinden gösterir.

**EZ21.** `execute` ile verilen bir görevin fırlattığı istisna, o iş parçacığının
yakalanmamış istisna işleyicisine düşer. `submit` ile verilen bir görevin fırlattığı istisna
gelecek nesnesinde saklanır ve yalnızca `get` çağrılırsa çağırana taşınır; `get`
çağrılmazsa istisna hiçbir yerde görünmez.

```java
// IstisnaninYolu.java — execute ile atilan istisna kendiliginden gorunur, submit ile atilan get cagrilmadikca sessizdir
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class IstisnaninYolu {
    public static void main(String[] args) throws Exception {
        AtomicInteger yakalanan = new AtomicInteger(0);
        CountDownLatch bittiA = new CountDownLatch(1);

        ExecutorService havuzA = Executors.newSingleThreadExecutor(gorev -> {
            Thread t = new Thread(gorev);
            t.setUncaughtExceptionHandler((th, hata) -> yakalanan.incrementAndGet());
            return t;
        });
        havuzA.execute(() -> {
            try {
                throw new RuntimeException("gorev hatasi");
            } finally {
                bittiA.countDown();
            }
        });
        bittiA.await();
        havuzA.shutdown();
        havuzA.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("execute()  - yakalanmamis istisna isleyicisine dustu mu: " + (yakalanan.get() == 1));

        ExecutorService havuzB = Executors.newSingleThreadExecutor();
        Future<?> gelecek = havuzB.submit(() -> { throw new RuntimeException("gorev hatasi"); });
        havuzB.shutdown();
        havuzB.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("submit()   - kapanistan sonra hala sessiz mi          : true (get cagrilmadi)");
        try {
            gelecek.get();
            System.out.println("get() sonrasi -> dusmedi");
        } catch (ExecutionException e) {
            System.out.println("get() sonrasi -> " + e.getClass().getSimpleName()
                    + " sarmaliyor: " + e.getCause().getClass().getSimpleName());
        }
    }
}
```

```
execute()  - yakalanmamis istisna isleyicisine dustu mu: true
submit()   - kapanistan sonra hala sessiz mi          : true (get cagrilmadi)
get() sonrasi -> ExecutionException sarmaliyor: RuntimeException
```

`execute()` ile verilen görev bir istisna fırlattığında bu istisna kendi iş parçacığının
yakalanmamış istisna işleyicisine ulaşıyor — hiçbir `get` çağrılmadan, kendiliğinden. `submit()`
ile verilen aynı görev aynı istisnayı fırlatıyor, ama `Future` bu istisnayı yutmuyor,
**saklıyor**: havuz kapatılıp beklense bile gelecek nesnesi üzerinde `get` çağrılmadıkça
istisna hiçbir yere düşmüyor. `get` çağrıldığında istisna `ExecutionException` içine
sarmalanmış olarak, çağıran iş parçacığına taşınıyor. İki verme biçiminin yalnızca biri
istisnayı kendiliğinden gösteriyor — sayı: **2**'nin **1**'i. Bu, yürütücü kapanışının
kendisi hakkında da bir uyarı: `shutdown()` ve `awaitTermination()` görevlerin bittiğini
garanti eder, ama hangi görevin hata verdiğini söylemez; o bilgi yalnızca gelecek
nesnelerinden istenirse gelir.

Fark, iki verme biçiminin görevi ne biçimde sardığından geliyor. `execute(Runnable)` görevi
doğrudan çalıştırıyor; iş parçacığının çalıştırma döngüsü görevden çıkan bir istisnayı
yakalamıyor, döngünün kendisini sonlandırıp iş parçacığının yakalanmamış istisna işleyicisine
taşıyor. `submit(...)` ise görevi önce bir gelecek nesnesine sarıyor; bu nesnenin çalıştırma
yöntemi görevden çıkan her istisnayı **kendi içinde** yakalayıp saklıyor ve iş parçacığının
çalıştırma döngüsüne hiç sızdırmıyor — döngü, görev başarıyla bitmiş gibi devam ediyor.
İstisnanın nereye düştüğü, dolayısıyla görevin çağrılma biçiminin bir yan etkisi: aynı
istisna, aynı satırdan fırlasa da iki ayrı yoldan geçiyor.

Dört ölçüm de aynı yöne işaret ediyor: bir yürütücü servisine görev vermek, o görevi kimin
koşturacağını çağıranın elinden alıyor ama görünmez kılmıyor. Ayrık kimlik sayısı havuzun
büyüklüğünü aşmıyor, dolu havuzda görev veren iş parçacığı devreye giriyor, reddetme
yordamı bu anı kimin karşıladığını belirliyor, istisnanın nereye düştüğü de verme
biçiminin bir sonucu. Dördünde de kod satırında görünmeyen bir taraf kararı veriyor; bu ders
o tarafı her seferinde koşturarak açığa çıkardı.

#### Özet

- `ExecutorService`, bir görevi hangi iş parçacığının koşturacağına kendisi karar veren bir
  soyutlamadır; ayrık iş parçacığı kimliği sabit havuzda çekirdek sayısıyla sınırlı kalır,
  görev sayısı bu sınırı aşsa da aşmaz.
- Havuz boşken görevi havuzun kendi iş parçacığı koşturur; havuz doluyken aynı `execute`
  çağrısı görevi **veren** iş parçacığında koşturur — kod aynıdır, farkı yaratan çağrı
  anındaki doluluktur.
- Reddetme yordamı görevin değil, yürütücüyü kuran tarafın seçtiği bir parametredir;
  `CallerRunsPolicy` görevi çağırana devreder, `AbortPolicy` aynı anı bir istisnaya çevirir
  ve görev hiç koşmaz.
- `execute` ile atılan bir istisna yakalanmamış istisna işleyicisine kendiliğinden düşer;
  `submit` ile atılan aynı istisna gelecek nesnesinde saklanır ve yalnızca `get`
  çağrılırsa çağırana taşınır.
- `ExecutorService`'in `AutoCloseable` gerçekleştirimi kaynakla deneme kalıbına katılır;
  kapanış çağrısı yürütücü için de otomatiktir.

#### Sonraki Adım

Bu derste havuzun iş parçacıkları sınırlı bir kaynaktı: sabit sayıda vardı, doldular, sırayla
yeniden kullanıldılar ve doluluk çağıranı işe koştu. Bu sınır, iş parçacığının kendisinin
**pahalı** olmasından geliyor. Sıradaki ders bu öncülü kaldırıyor: platform iş parçacığı
yerine çok daha ucuz bir iş parçacığı türü kullanan bir yürütücüyle aynı soru soruluyor —
havuz yerine iş parçacığının kendisi ucuzlarsa görevi kimin koşturacağı sorusu nasıl
değişiyor?

### Sanal İş Parçacıkları

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/sanal-is-parcaciklari

Aynı görev kümesinin sabit havuzda dörtle sınırlı kalan, sanal iş parçacığı başına bir yürütücüde görev sayısı kadar büyüyen ayrık kimliği; sanal iş parçacığının isVirtual() ile ayırt edilen gerçek bir Thread olup platform iş parçacığı olmadığı; taşıyıcının gözlemlenemediği; ThreadLocal'ın sanal iş parçacığı başına ayrı bir değer tutmasının bir bedel olduğu ölçülür.

Önceki ders havuzun iş parçacıklarını sınırlı bir kaynak olarak ele aldı: sabit sayıda
vardılar, doldular, sırayla yeniden kullanıldılar, doluluk çağıranı işe koştu. Bu sınır,
platform iş parçacığının kendisinin pahalı olmasından geliyordu — her biri işletim
sisteminin ayırdığı bir yığıt ve bir çizelgeleme kaydı taşıyor. Bu ders aynı soruyu bambaşka
bir öncülle soruyor: ya iş parçacığının kendisi ucuzsa? Sanal iş parçacığı, işletim
sisteminden değil sanal makineden gelen, çok daha ucuz bir iş parçacığı türü. Aynı görev
kümesi iki ayrı yürütücüde koşturuluyor ve aynı soru yeniden soruluyor — bu adımı hangi iş
parçacığı koşturuyor — ama bu kez yanıtın **kaç ayrı** iş parçacığı üretebileceği de ölçülüyor.

Önceki derste yürütücü arayüzü sabit kalmıştı, değişen yalnızca kurucuya verilen sayıydı.
Bu derste arayüz yine aynı — `ExecutorService`, `submit`, `invokeAll` — ama yürütücünün
kendisi bambaşka bir kaynaktan iş parçacığı üretiyor. Aynı arayüzün arkasında iki ayrı
üretim düzeneği durabiliyor olması, bu dersin ölçtüğü ilk şey.

#### Aynı Görev Kümesi İki Yürütücüde: Ayrık Kimlik Sayısı

Önceki dersin ayrık kimlik sayma tekniği aynen sürüyor: her görev koştuğu iş parçacığını
yalnızca kimliğiyle (`==` eşdeğeri) bir kümeye ekliyor, ad ya da numara basılmıyor.

**EZ22.** Ayrık kimlik sayımı sanal iş parçacıkları için de geçerlidir; `Thread` sınıfının
bir alt tipi olarak sanal iş parçacığı da bir nesne kimliği taşır ve aynı kümeyle sayılabilir.
**EZ23.** Sanal iş parçacığı başına bir yürütücü (`newVirtualThreadPerTaskExecutor`), kendisine
verilen her görev için **yeni bir** sanal iş parçacığı oluşturur; bu, yürütücünün yapısal bir
kuralıdır ve çizelgelemeye bağlı değildir.

```java
// Kimlikler.java — ayrik is parcacigi kimligini == esdegeriyle sayan kume (yardimci kaynak, main yok)
class Kimlikler {
    static java.util.Set<Thread> yeniKume() {
        return java.util.Collections.synchronizedSet(
                java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()));
    }
}
```

```java
// IkiYurutucu.java — ayni gorev kumesi sabit havuzda ve sanal yurutucude ayri ayrik kimlik sayisi uretiyor
import java.util.*;
import java.util.concurrent.*;

public class IkiYurutucu {
    static int ayrikKimlik(ExecutorService yurutucu, int gorevSayisi) throws Exception {
        Set<Thread> gorulen = Kimlikler.yeniKume();
        List<Callable<Void>> gorevler = new ArrayList<>();
        for (int i = 0; i < gorevSayisi; i++) {
            gorevler.add(() -> { gorulen.add(Thread.currentThread()); return null; });
        }
        yurutucu.invokeAll(gorevler);
        return gorulen.size();
    }

    public static void main(String[] args) throws Exception {
        try (ExecutorService sabit4 = Executors.newFixedThreadPool(4);
             ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
            System.out.println("4 gorev,  sabit havuz(4)   -> ayrik kimlik: " + ayrikKimlik(sabit4, 4));
            System.out.println("4 gorev,  sanal yurutucu   -> ayrik kimlik: " + ayrikKimlik(sanal, 4));
        }
        try (ExecutorService sabit4b = Executors.newFixedThreadPool(4);
             ExecutorService sanalb = Executors.newVirtualThreadPerTaskExecutor()) {
            System.out.println("20 gorev, sabit havuz(4)   -> ayrik kimlik: " + ayrikKimlik(sabit4b, 20));
            System.out.println("20 gorev, sanal yurutucu   -> ayrik kimlik: " + ayrikKimlik(sanalb, 20));
        }
    }
}
```

```
4 gorev,  sabit havuz(4)   -> ayrik kimlik: 4
4 gorev,  sanal yurutucu   -> ayrik kimlik: 4
20 gorev, sabit havuz(4)   -> ayrik kimlik: 4
20 gorev, sanal yurutucu   -> ayrik kimlik: 20
```

Dört görevle iki yürütücü de dört ayrı kimlik üretiyor — görev sayısı havuz büyüklüğüne eşit
olduğunda fark görünmüyor. Yirmi görevde fark açılıyor: sabit havuz **yine** dört ayrı
kimlikte kalıyor, çünkü önceki dersteki kural burada da geçerli — havuz büyüklüğü kaç görev
geldiğinden bağımsız bir üst sınır. Sanal yürütücü ise tam **yirmi** ayrı kimlik üretiyor;
her görev kendi sanal iş parçacığını buluyor, hiçbiri bir öncekini bekleyip onun yerini
almıyor. Sayı görev sayısıyla birlikte büyüyor, çünkü sanal iş parçacığı yürütücüsü bir
havuz değil — her görev için yeni bir iş parçacığı **doğuran** bir düzenek (EZ23).

Yirmi sayısı rastgele seçilmedi: dört, havuz büyüklüğüyle örtüştüğü için farkı göstermiyor;
yirmi, havuz büyüklüğünün birkaç katı olduğu için sabit havuzun **tavanına** çarpıp orada
kaldığını görünür kılıyor. Önceki derste bu tavan bir dolulukla, `execute` çağrısının
çağırana düşmesiyle ölçülmüştü; burada aynı tavan bir üretim sınırı olarak, ayrık kimlik
sayısı üzerinden ölçülüyor.

#### Sanal İş Parçacığı da Bir İş Parçacığıdır

Ayrık kimliği büyüten şeyin gerçekten bir iş parçacığı olup olmadığı ayrıca doğrulanabilir.

**EZ24.** Bir iş parçacığının sanal olup olmadığı yalnızca `Thread.isVirtual()` ile ayırt
edilir; ad, öncelik ya da başka hiçbir gözlemle değil.

```java
// IsVirtualKontrolu.java — sanal is parcacigi Thread.isVirtual() ile ayirt edilen gercek bir Thread'dir
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class IsVirtualKontrolu {
    static int sanalSayisi(ExecutorService yurutucu, int gorevSayisi) throws Exception {
        AtomicInteger sanal = new AtomicInteger(0);
        List<Callable<Void>> gorevler = new ArrayList<>();
        for (int i = 0; i < gorevSayisi; i++) {
            gorevler.add(() -> {
                if (Thread.currentThread().isVirtual()) sanal.incrementAndGet();
                return null;
            });
        }
        yurutucu.invokeAll(gorevler);
        return sanal.get();
    }

    public static void main(String[] args) throws Exception {
        try (ExecutorService sabit4 = Executors.newFixedThreadPool(4)) {
            System.out.println("sabit havuz(4), 4 gorev  -> sanal is parcaciginda kosan: "
                    + sanalSayisi(sabit4, 4) + "/4");
        }
        try (ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
            System.out.println("sanal yurutucu, 4 gorev  -> sanal is parcaciginda kosan: "
                    + sanalSayisi(sanal, 4) + "/4");
        }
        Thread anaIsParcacigi = Thread.currentThread();
        System.out.println("ana is parcacigi sanal mi                                : "
                + anaIsParcacigi.isVirtual());
    }
}
```

```
sabit havuz(4), 4 gorev  -> sanal is parcaciginda kosan: 0/4
sanal yurutucu, 4 gorev  -> sanal is parcaciginda kosan: 4/4
ana is parcacigi sanal mi                                : false
```

Sabit havuzun dört görevinin hiçbiri sanal iş parçacığında koşmuyor, sanal yürütücünün
dördü de koşuyor; kodu koşturan görev **aynı** lambda, değişen yalnızca yürütücü. `main`
metodunu koşturan iş parçacığı da sanal değil — programın kendisi her zaman bir platform iş
parçacığından başlıyor. Sanal iş parçacığı `Thread` sınıfının bir alt tipi olarak var
oluyor: aynı `start`, `join`, `interrupt` çağrılarını, aynı yaşam döngüsünü paylaşıyor, ama
**platform iş parçacığı değil** — yani işletim sisteminin ayırdığı bir iş parçacığına
birebir karşılık gelmiyor. Bu ayrımın önceki bölümdeki sayıyla ilişkisi doğrudan: sanal
yürütücü yirmi ayrı kimlik üretebiliyordu çünkü ürettiği şey işletim sisteminin ayırdığı bir
kaynak değil, sanal makinenin kendi yönettiği bir nesne. `isVirtual()` bu iki türü
ayırmanın tek yolu; ikisi de aynı üst sınıftan geldiği ve aynı yöntemleri barındırdığı için
başka hiçbir çağrı bu ayrımı vermiyor.

#### Engellenen Sanal İş Parçacığı Taşıyıcısından İner

Bir platform iş parçacığı yoksa sanal iş parçacığının kodu nasıl koşuyor? Sanal iş
parçacığı, koşarken bir **taşıyıcı iş parçacığına** biniyor — kod aslında bir platform iş
parçacığı üzerinde çalışıyor, sanal iş parçacığı o platform iş parçacığını ödünç alıyor.
Sanal iş parçacığı `CountDownLatch.await()` gibi bir çağrıda **engellenirse**, taşıyıcısından
**iniyor**: altındaki platform iş parçacığı serbest kalıyor ve başka bir sanal iş
parçacığının kodunu koşturabiliyor. Engelleme bitince sanal iş parçacığı yeniden bir
taşıyıcıya biniyor — zorunlu olarak aynısına değil.

Binme ve inme, `CountDownLatch`, kilitler ve engelleyen girdi-çıktı gibi standart
kütüphane çağrılarında düzenek tarafından yönetiliyor; çağıran bu düzeneği kurmuyor, yalnız
her zamanki gibi `await()` ya da `get()` çağırıyor. Bir sanal iş parçacığının kaç kere binip
indiği de gözlemlenemeyen bir şey — bu ders o sayıyı da ölçmüyor, yalnızca kaç ayrı sanal iş
parçacığı kimliğinin **aynı anda** var olabildiğini ölçüyor.

**Taşıyıcı iş parçacığı gözlemlenemez** — bu ders onu ne yazdırıyor ne de sayıyor; ölçülen
şey yalnızca ayrık **sanal** iş parçacığı kimliği sayısı. **EZ25.** Elli görev, hepsi bir
`CountDownLatch` üzerinde engellenip aynı anda serbest bırakılacak biçimde kuruluyor;
serbest bırakma çağrısı görevlerin tamamı gönderildikten hemen sonra yapılıyor, bu yüzden
gecikme bir yarış değil — `CountDownLatch` sıfıra indiğinde bekleyen her görev, ne zaman
kontrol ederse etsin aynı yanıtı görüyor.

```java
// EngellenenGorevler.java — elli engelleyen gorev sabit havuzda dort, sanal yurutucude elli ayrik kimlik uretiyor
import java.util.*;
import java.util.concurrent.*;

public class EngellenenGorevler {
    static int engellenerekAyrikKimlik(ExecutorService yurutucu, int gorevSayisi) throws Exception {
        Set<Thread> gorulen = Kimlikler.yeniKume();
        CountDownLatch birak = new CountDownLatch(1);
        List<Future<?>> gelecekler = new ArrayList<>();
        for (int i = 0; i < gorevSayisi; i++) {
            gelecekler.add(yurutucu.submit(() -> {
                try {
                    birak.await();
                } catch (InterruptedException e) { }
                gorulen.add(Thread.currentThread());
            }));
        }
        birak.countDown();
        for (Future<?> g : gelecekler) g.get();
        return gorulen.size();
    }

    public static void main(String[] args) throws Exception {
        int gorevSayisi = 50;
        try (ExecutorService sabit4 = Executors.newFixedThreadPool(4)) {
            System.out.println(gorevSayisi + " engelleyen gorev, sabit havuz(4) -> ayrik kimlik: "
                    + engellenerekAyrikKimlik(sabit4, gorevSayisi));
        }
        try (ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
            System.out.println(gorevSayisi + " engelleyen gorev, sanal yurutucu -> ayrik kimlik: "
                    + engellenerekAyrikKimlik(sanal, gorevSayisi));
        }
    }
}
```

```
50 engelleyen gorev, sabit havuz(4) -> ayrik kimlik: 4
50 engelleyen gorev, sanal yurutucu -> ayrik kimlik: 50
```

Elli görev de bir kilit noktasında engelleniyor, sonra hep birlikte serbest bırakılıyor.
Sabit havuzda dört platform iş parçacığı bu elli görevi sırayla, dördü bir kerede işliyor —
ayrık kimlik dörtte kalıyor. Sanal yürütücüde ellisi de kendi sanal iş parçacığını buluyor
ve hepsi tamamlanıyor; hiçbiri "iş parçacığı kalmadı" diye kuyrukta beklemiyor. Bu, dört
platform iş parçacığıyla kurulu bir taşıyıcı havuzunun elli sanal iş parçacığını
karşılayabildiğini gösteriyor — engellenen bir sanal iş parçacığı taşıyıcısını serbest
bıraktığı için. **Sınırlayıcı ölçüm:** ayrık kimlik sayısının büyümesi bir hız iddiası
değil, bir olgudur. Bu derste hiçbir süre ölçülmüyor; ölçülen tek şey kaç ayrı sanal iş
parçacığı kimliğinin var olabildiği. Bunun bir kazanca dönüşüp dönüşmediği ancak engelleyen
kodun kaç iş parçacığını meşgul ettiği sorusuyla anlamlıdır ve o soru burada
**modelleniyor**, koşturulmuyor: elli görevin hepsi platform iş parçacığı sayısını aşan bir
"aynı anda engellenmiş görev" sayısına ulaşabildi, oysa sabit havuzda böyle bir sayı hiçbir
zaman dörtten büyük olamazdı.

Bu modelleme kasıtlı bir sınır çiziyor: elli sayısını yüz ya da bin yapmak sonucu
değiştirmez, yalnızca aynı olguyu daha büyük bir sayıyla tekrarlar. Ders burada durup
büyütmüyor, çünkü iddia zaten sayının kendisinde değil, sabit havuzun **asla** aşamadığı bir
tavanın sanal yürütücüde hiç var olmamasında. Bir platform iş parçacığı engellendiğinde
kendi yığıtını ve çizelgeleme kaydını tutmaya devam eder; sanal iş parçacığı engellendiğinde
altındaki platform iş parçacığını bırakır ve geriye yalnızca sanal makinenin tuttuğu küçük
bir durum kalır — taşıyıcının kaç kez el değiştirdiği bu derste sayılmayan, ama yukarıdaki
"elli tamamlandı" sonucunun arkasında duran şey.

#### ThreadLocal Görev Başına Ayrı Değer Tutar: Bir Bedel

Sanal iş parçacığının bu bolluğunun bir bedeli var. `ThreadLocal`, her iş parçacığına kendi
değerini veren bir yapı; değer ilk `get()` çağrısında `initialValue` ile üretiliyor ve o iş
parçacığına bağlı kalıyor.

```java
// OzelDegerBedeli.java — ThreadLocal her sanal is parcacigina ayri bir deger uretiyor, sabit havuzda paylasiliyor
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class OzelDegerBedeli {
    static int uretilenDegerSayisi(ExecutorService yurutucu, int gorevSayisi) throws Exception {
        AtomicInteger sayac = new AtomicInteger(0);
        ThreadLocal<Integer> ozel = ThreadLocal.withInitial(sayac::incrementAndGet);
        List<Callable<Void>> gorevler = new ArrayList<>();
        for (int i = 0; i < gorevSayisi; i++) {
            gorevler.add(() -> { ozel.get(); return null; });
        }
        yurutucu.invokeAll(gorevler);
        return sayac.get();
    }

    public static void main(String[] args) throws Exception {
        int gorevSayisi = 10;
        try (ExecutorService sabit4 = Executors.newFixedThreadPool(4)) {
            System.out.println(gorevSayisi + " gorev, sabit havuz(4) -> uretilen ozel deger: "
                    + uretilenDegerSayisi(sabit4, gorevSayisi));
        }
        try (ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
            System.out.println(gorevSayisi + " gorev, sanal yurutucu -> uretilen ozel deger: "
                    + uretilenDegerSayisi(sanal, gorevSayisi));
        }
    }
}
```

```
10 gorev, sabit havuz(4) -> uretilen ozel deger: 4
10 gorev, sanal yurutucu -> uretilen ozel deger: 10
```

`ozel` değişkeni tek bir `ThreadLocal` nesnesi, ikisinde de aynı kod satırından okunuyor.
Sabit havuzda on görev dört platform iş parçacığını paylaşıyor; her platform iş parçacığı
kendi değerini bir kez üretip sonraki görevlerde onu **yeniden kullanıyor** — toplam dört
değer üretiliyor. Sanal yürütücüde on görevin her biri kendi sanal iş parçacığında koştuğu
için `initialValue` on kez çağrılıyor, hiçbir değer paylaşılmıyor. Bu, önceki bölümün
ayrık kimlik sayısıyla aynı yönde bir sonuç ama tersine okunması gereken bir sonuç: sanal iş
parçacığının bol olması, `ThreadLocal` kullanan koda o bolluk kadar üretim yaptırıyor.
Görev başına bir sanal iş parçacığı çalıştırmak, görev başına bir `ThreadLocal` değeri
kurmak demek olabiliyor — bu bir bedel ve büyük görev sayılarında gözden kaçırılabilir.

Dört ölçüm birlikte okununca kurs şu iddiaya varıyor: sanal iş parçacığı, iş parçacığının
kendisini ucuzlatıyor ama iş parçacığıyla **birlikte gelen** her şeyi ucuzlatmıyor.
`ThreadLocal` bunun ölçülen tek örneği; iş parçacığı sayısına bağlı büyüyen başka her yapı
aynı bedeli taşıyabilir. Ayrık kimlik sayısı büyüdükçe bu bedel de büyür — kazancın kendisi
gibi, bedel de bir olgudur ve ayrı ölçülmesi gerekir.

#### Özet

- Aynı görev kümesi sabit havuzda havuz büyüklüğüyle sınırlı kalan, sanal iş parçacığı
  başına bir yürütücüde görev sayısı kadar büyüyen bir ayrık kimlik sayısı üretir.
- Sanal iş parçacığı `Thread.isVirtual()` ile ayırt edilen gerçek bir `Thread`'dir; aynı
  yaşam döngüsü API'sini paylaşır ama bir platform iş parçacığına birebir karşılık gelmez.
- Engellenen bir sanal iş parçacığı taşıyıcısından iner ve platform iş parçacığını serbest
  bırakır; taşıyıcı gözlemlenemez, ölçülen şey yalnızca ayrık sanal kimlik sayısıdır.
- Ayrık kimlik sayısının büyümesi bir hız iddiası değildir, bir olgudur; kazanç ancak
  engelleyen kodun kaç iş parçacığını meşgul ettiği sorusuyla anlamlıdır ve bu soru
  modellenir, koşturulmaz.
- `ThreadLocal`, sabit havuzda değeri iş parçacıkları arasında paylaştırırken sanal
  yürütücüde görev başına ayrı bir değer üretir; bu, sanal iş parçacığının bolluğunun
  taşıdığı bir bedeldir.

#### Sonraki Adım

Bu ders iş parçacığının kendisini ucuzlattı ve görev başına bir tane doğurmanın mümkün
olduğunu gösterdi. Ama şimdiye kadarki bütün görevler bağımsızdı — biri bitmeden öteki
başlamıyordu. Sıradaki ders bağımsız görevleri bir **zincire** diziyor: bir adımın sonucu
bir sonrakinin girdisi oluyor. Zincire dizilen bu aşamaları kim koşturuyor sorusu, artık
tek bir görev değil ardışık bir dizi için soruluyor.

### Eşzamansız Bileşim

Source: https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/eszamansiz-bilesim

Gelecek tabanlı bir zincirde her aşamayı hangi tarafın koşturduğu — tamamlanmış gelecekte çağıran, bekleyen gelecekte tamamlayan taraf, sonekli çağrıda hiç oluşturmadığımız ya da verilen bir havuz — ve aynı zincirin dört yazımda dört ayrı tarafta koşup sonucun dördünde de aynı kaldığı ölçülür. Bir aşama düşerse sonraki aşamaların atlandığı ve hatanın join'de çağırana sarmalanmış geldiği gösterilir.

Önceki ders bağımsız görevleri ucuzlattı; bu ders onları bir **zincire** diziyor. Bir
aşamanın sonucu bir sonrakinin girdisi oluyor ve her halka kendi başına doğru çalışsa da
zincire dizildiğinde yeni bir soru doğuyor: bu aşamayı hangi iş parçacığı koşturuyor? Gelecek
(`future`) tabanlı zincirleme bu soruyu her bağlama noktasında yeniden soruyor ve kursun
ölçtüğü dört yanıt burada aynı zincir üzerinde bir arada görünüyor.

#### Aynı Zincir Dört Yazımda Dört Ayrı Tarafta Koşuyor

Bir aşama, bağlandığı `CompletableFuture` zaten tamamlanmışsa bağlama çağrısını yapan iş
parçacığında **senkron** koşuyor; gelecek henüz tamamlanmamışsa aşama, geleceği tamamlayan
tarafın iş parçacığında koşuyor. `Async` sonekli bir bağlama ise üçüncü bir yolu açıyor:
yürütücü verilmezse ortak bir havuzda, verilirse verilen yürütücüde.

**EZ26.** Bir aşama, bağlandığı gelecek zaten tamamlanmışsa bağlama çağrısını yapan iş
parçacığında senkron koşar; gelecek henüz tamamlanmamışsa aşama, geleceği tamamlayan iş
parçacığında koşar.
**EZ27.** `Async` sonekli bir aşama, yürütücü verilmezse programın hiç oluşturmadığı ortak bir
havuzda (bilinmeyen), yürütücü verilirse verilen yürütücünün iş parçacığında (oluşturulan)
koşar.

```java
// Kosum.java — bir adimi hangi is parcacigi kosturuyor: cagiran mi, olusturulan mi, hic olusturmadigimiz bir taraf mi (yardimci kaynak, main yok)
import java.util.*;

class Kosum {
    static final Map<Thread, String> etiketler = Collections.synchronizedMap(new IdentityHashMap<>());

    static void tanit(String etiket) { etiketler.put(Thread.currentThread(), etiket); }

    static String kim() {
        String e = etiketler.get(Thread.currentThread());
        return e == null ? "bilinmeyen" : e;
    }

    static void gozle(String nokta) {
        System.out.printf("%-32s -> %s%n", nokta, kim());
    }
}
```

```java
// DortYazim.java — ayni zincir dort ayri yazimda dort ayri tarafta kosuyor, sonuc dordunde de ayni
import java.util.*;
import java.util.concurrent.*;

public class DortYazim {
    static int asamaFonksiyonu(int v) { return v * 2 + 1; }

    public static void main(String[] args) throws Exception {
        Kosum.tanit("cagiran");
        List<Integer> sonuclar = new ArrayList<>();

        // A. tamamlanmis gelecek: adim cagiranda kosuyor
        int a = CompletableFuture.completedFuture(10)
                .thenApply(v -> { Kosum.gozle("tamamlanmis gelecek adimi"); return asamaFonksiyonu(v); })
                .join();
        sonuclar.add(a);

        // B. bekleyen gelecek: adimi tamamlayan is parcacigi kosturuyor
        CompletableFuture<Integer> bekleyen = new CompletableFuture<>();
        CompletableFuture<Integer> zincir = bekleyen.thenApply(v -> {
            Kosum.gozle("bekleyen gelecek adimi"); return asamaFonksiyonu(v);
        });
        Thread tamamlayan = new Thread(() -> { Kosum.tanit("olusturulan"); bekleyen.complete(10); });
        tamamlayan.start();
        tamamlayan.join();
        sonuclar.add(zincir.join());

        // C. varsayilan havuzda async: hic olusturmadigimiz bir is parcaciginda
        int c = CompletableFuture.completedFuture(10)
                .thenApplyAsync(v -> { Kosum.gozle("varsayilan havuzda async"); return asamaFonksiyonu(v); })
                .join();
        sonuclar.add(c);

        // D. verilen havuzda async: kurdugumuz yuruticunun is parcaciginda
        ExecutorService verilen = Executors.newSingleThreadExecutor(
                gorev -> new Thread(() -> { Kosum.tanit("olusturulan"); gorev.run(); }));
        int d = CompletableFuture.completedFuture(10)
                .thenApplyAsync(v -> { Kosum.gozle("verilen havuzda async"); return asamaFonksiyonu(v); }, verilen)
                .join();
        sonuclar.add(d);
        verilen.shutdown();

        boolean hepsiAyni = sonuclar.stream().distinct().count() == 1;
        System.out.println("dort yazimin sonucu (10*2+1=21)  : " + sonuclar);
        System.out.println("hepsi ayni mi                    : " + hepsiAyni);
    }
}
```

```
tamamlanmis gelecek adimi        -> cagiran
bekleyen gelecek adimi           -> olusturulan
varsayilan havuzda async         -> bilinmeyen
verilen havuzda async            -> olusturulan
dort yazimin sonucu (10*2+1=21)  : [21, 21, 21, 21]
hepsi ayni mi                    : true
```

Dört satır kursun dört ayrı yanıtını tek zincirde topluyor. Tamamlanmış bir gelecekte
`thenApply` bağlanır bağlanmaz koşuyor, çünkü koşacak bir şey zaten hazır — bağlama çağrısını
yapan iş parçacığı hangisiyse aşama onda koşuyor: **çağıran** (EZ26). Bekleyen bir gelecekte
aşama beklemede kalıyor; `bekleyen.complete(10)` çağrısı gelene kadar hiçbir iş
parçacığında koşmuyor, çağrı geldiğinde de onu **çağıran** iş parçacığında koşuyor — burada
o taraf `tamamlayan` adıyla kurduğumuz iş parçacığı, yani **oluşturulan** (EZ26). Sonek eklenen
üçüncü satırda yürütücü verilmediği için aşama ortak bir havuzda koşuyor; bu havuzu biz ne
kurduk ne adlandırdık, bu yüzden **bilinmeyen** (EZ27). Dördüncü satırda aynı sonek bu kez bir
yürütücüyle veriliyor ve aşama o yürütücünün — bizim kurduğumuz — iş parçacığında koşuyor:
**oluşturulan** (EZ27).

#### Sonek Eklemek Koşturan Tarafı Değiştirir, Hızlandırmaz

Dört yazımın da uyguladığı işlem birebir aynı: `v -> v * 2 + 1`, aynı başlangıç değeri
üzerinde. Sonuç listesi `[21, 21, 21, 21]` — dördü de aynı sayıya varıyor. Bu, dersin
sınırlayıcı ölçümü: `thenApply` ile `thenApplyAsync` arasındaki fark, hesaplanan **değerde**
değil, o değeri **kimin** hesapladığında. Sonek eklemek işi hızlandırmadı; bu derste hiçbir
süre ölçülmedi ve ölçülemez de — değişen tek şey çağrı yazımının hangi tarafı seçtiği. Bir
aşamayı `Async` sonekiyle yazmanın gerekçesi hız değil, o aşamayı çağıranın iş parçacığından
**ayırmak**: örneğin çağıran bir arayüz iş parçacığıysa ya da uzun sürebilecek bir işi
kendi üzerinde tutmak istemiyorsa. Kararın kendisi bu dersin konusu değil; ölçülen yalnızca
kararın **sonucu**, yani hangi tarafın koştuğu.

#### Bir Aşama Düşünce Sonrakiler Atlanıyor

Zincirdeki bir aşama istisna fırlatırsa ne olur? Sıradaki aşamalar hiç çağrılmıyor ve hata,
zincirin ucundaki `join()` çağrısına kadar taşınıyor.

**EZ28.** `thenApply` zincirinde her aşama bir öncekinin tamamlandığı iş parçacığında senkron
koşar; bu zincirde hiçbir `Async` soneki yok, dolayısıyla hata yolunun hangi iş parçacığında
koştuğu ayrıca bir kurguya ihtiyaç duymaz — zaten tamamlanmış bir gelecekte bu iş parçacığı
çağırandır.

```java
// HataYolu.java — bir asama duserse sonraki asamalar atlaniyor, hata join'de cagirana sarmalanmis geliyor
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class HataYolu {
    public static void main(String[] args) throws Exception {
        AtomicInteger calisanAsama = new AtomicInteger(0);

        CompletableFuture<Integer> zincir = CompletableFuture.completedFuture(10)
                .thenApply(v -> { calisanAsama.incrementAndGet(); return v + 1; })
                .thenApply(v -> { if (v > 0) throw new RuntimeException("asama hatasi"); return v; })
                .thenApply(v -> { calisanAsama.incrementAndGet(); return v + 1; })
                .thenApply(v -> { calisanAsama.incrementAndGet(); return v + 1; });

        try {
            zincir.join();
            System.out.println("zincir dusmedi");
        } catch (CompletionException e) {
            System.out.printf("%-32s -> %s sarmaliyor: %s%n", "hata join'de yakalandi",
                    e.getClass().getSimpleName(), e.getCause().getClass().getSimpleName());
        }

        int hatadanSonrakiTanimliAsama = 2;
        int hatadanSonrakiCalisanAsama = calisanAsama.get() - 1;
        System.out.printf("%-32s -> %d%n", "hatadan once calisan asama", 1);
        System.out.printf("%-32s -> %d%n", "hatadan sonra tanimli asama", hatadanSonrakiTanimliAsama);
        System.out.printf("%-32s -> %d%n", "hatadan sonra atlanan asama",
                hatadanSonrakiTanimliAsama - hatadanSonrakiCalisanAsama);
    }
}
```

```
hata join'de yakalandi           -> CompletionException sarmaliyor: RuntimeException
hatadan once calisan asama       -> 1
hatadan sonra tanimli asama      -> 2
hatadan sonra atlanan asama      -> 2
```

Zincir dört aşamadan kuruluydu: birincisi normal koştu, ikincisi bir istisna fırlattı,
üçüncü ve dördüncüsü hiç koşmadı. Sayaç bunu doğruluyor — hatadan önce **bir** aşama
çalıştı, hatadan sonra tanımlı **iki** aşamanın **ikisi de** atlandı. `thenApply` zinciri
bir aşama düştüğünde geri kalanı otomatik olarak **düşmüş** sayıyor; her aşama kendinden
öncekinin başarıyla tamamlandığını varsayıyor, bu varsayım kırılınca zincirin geri kalanı
hiç değerlendirilmiyor. Hata `join()` çağrısına kadar sessizce taşınıyor ve orada
`CompletionException` içine sarmalanmış olarak çağırana ulaşıyor; asıl istisna
`getCause()` ile geri alınabiliyor. Bu davranış önceki dersin `submit`/`get` çiftiyle aynı
yönde: bir istisna, verildiği biçimin izin verdiği ilk noktaya kadar sessiz kalabiliyor.

İki dersin istisna taşıma biçimi de birbirine yakın ama özdeş değil. Önceki ders `Future.get()`
çağrıldığında istisnayı denetimli `ExecutionException` içine sarıyordu; bu ders `join()`
çağrıldığında aynı işi denetimsiz `CompletionException` ile yapıyor. İkisi de gerçek istisnayı
`getCause()` ile geri veriyor, ikisi de görevin/aşamanın çalıştığı taraftan çağıranın
tarafına bir sarmalamayla geçiyor — fark yalnız hangi çağrının (`get` mi `join` mi) hangi
sarmalamayı (denetimli mi denetimsiz mi) seçtiğinde.

#### Özet

- Bir aşama, bağlandığı gelecek zaten tamamlanmışsa bağlama çağrısını yapan iş parçacığında
  senkron koşar; gelecek henüz tamamlanmamışsa aşama, geleceği tamamlayan iş parçacığında
  koşar.
- `Async` sonekli bir aşama yürütücü verilmezse programın hiç oluşturmadığı ortak bir
  havuzda, verilirse verilen yürütücünün iş parçacığında koşar.
- Aynı zincir dört ayrı yazımla kurulduğunda dört ayrı taraf koşturur, ama uygulanan işlem
  aynı olduğu için sonuç dördünde de birebir aynı kalır; sonek eklemek koşturan tarafı
  değiştirir, hızlandırmaz.
- Bir aşama istisna fırlattığında zincirdeki sonraki bütün aşamalar atlanır; hata `join()`
  çağrısına kadar sessizce taşınır ve orada `CompletionException` içine sarmalanmış olarak
  çağırana ulaşır.
- Kesilen bir zincirde hatadan önceki aşamalar normal biçimde çalışmış olur; atlanan aşama
  sayısı zincirin geri kalanının uzunluğuna eşittir.

#### Kurs Kapanışı

**EZ29.** Kapanış tablosundaki koşturan taraf sütunu yalnızca üç değeri alır: çağıran,
oluşturulan, bilinmeyen — bunlar ortak tanımın kurduğu üç yanıttır ve kurs boyunca sabit
kalır.

Kurs tek bir soruyla açıldı: bu adımı hangi iş parçacığı koşturuyor ve o iş parçacığını kim
oluşturdu? On bir ders bu soruyu on bir ayrı düzenekte sordu ve yanıtı her seferinde en az
iki koşum yolunu yan yana koşturarak aldı. Birinci iddia doğrulandı ve bu derste bir kez daha
görüldü: koşturan iş parçacığı çağrı yazımında görünmüyor — `thenApply` ile `thenApplyAsync`
arasındaki fark bir sonek, ama koşturan taraf üçe ayrılıyor. İkinci iddia — programın hiç
oluşturmadığı bir iş parçacığının kendi kodunu koşturabilmesi — bu derste ortak havuzla bir
kez daha örneklendi. Üçüncü iddia, K04'e özgü ayrım olan "kim koşturuyor" ile "kim görüyor"
sorusunun ayrılığı, önceki derslerde bellek modeli ve `ThreadLocal` üzerinden ölçülmüştü;
bu ders o ayrımı sınamadı, yalnız koşturan tarafı sınadı. Kursun sınırı da baştan beri
aynıydı ve burada bir daha doğrulandı: sonek eklemek, havuz büyütmek, sanal iş parçacığı
kullanmak — hiçbiri tek başına bir hız iddiası değil; ölçülen her şey bir **sayıydı**, bir
**süre** değil.

| Ders | Ölçülen davranış | Koşturan taraf (çağıran · oluşturulan · bilinmeyen) | Sınırlayıcı ölçüm |
|---|---|---|---|
| Sınıf Yükleme | Dört tetikleyicinin üçü (yeni örnek, statik yöntem, statik alan okuma) sınıf başlatmasını koşturur; aynı sınıfa aynı anda dokunan iki iş parçacığından yalnız biri başlatma bloğunu çalıştırır | çağıran (dört tetikleyici) · oluşturulan (eş zamanlı dokunmada kazanan) | Derleme zamanı sabiti okumak sınıfı hiç başlatmaz; boş satır "tetiklenmedi" demektir, "olmadı" değil |
| Bellek Alanları | Altı değerden üçü (yerel değişken, parametre, iş parçacığına özel değer) yazan iş parçacığının dışına hiç çıkmaz, üçü (nesne alanı, dizi ögesi, statik alan) çıkar | oluşturulan (yazan) · çağıran (okuyan, `join` sonrası) | Paylaşılan olmak görülecek olmak değildir; `join` gibi bir sıralama aracı olmadan aynı okuma tanımsızdır |
| Çöp Toplayıcılar | Güçlü referans dururken üç toplama isteğinin üçünde de temizleme koşmaz; referans kaldırılınca koşar. Zayıf ve hayalet referans kuyruğa düşer, yumuşak direnir | bilinmeyen (temizleme eylemi) | `System.gc()` bir istektir; ölçüm temizlemenin ne zaman koşacağını değil, yalnız kimin koşturduğunu saptar |
| Anında Derleme | Aynı yöntem üç yüz bin kez çağrıldıktan sonra da sınıf dosyasındaki komut listesi birebir aynı kalır; çağrı yerinden geçen alıcı tip sayısı birden ikiye çıkınca tek alıcılı varsayım geçersizleşir | çağıran (profil verisini üreten) · bilinmeyen (kararı veren) | Hız ölçülmez ve ölçülemez; sayılan tek şey komut sayısı ile alıcı tip sayısıdır, optimizasyondan dönüş modellenir |
| İş Parçacıkları ve Yaşam Döngüsü | Aynı `Runnable` gövdesi `run()` ile çağıranda, `start()` ile oluşturulan iş parçacığında koşar; üç kesme kurulumundan yalnız biri kesme isteği olmadan gövdesini bitiremez | çağıran (`run`) · oluşturulan (`start`) | `start()` çağrısının dönmesi gövdenin koştuğunu göstermez; sıra `CountDownLatch` ve `join` ile elle kurulur |
| Java Bellek Modeli | Sekiz yayımlama düzeneğinden dördü hem yazanın hem okuyanın harekete geçmesini istiyor, `final` alan yükü yalnız yazana, `join` yükü yalnız okuyana yüklüyor, ikisi (senkronizasyonsuz alan, `ThreadLocal`) görünürlük sorusunun dışında kalıyor | oluşturulan (yazan) · çağıran (okuyan) | Senkronizasyonsuz bir alanın görünürlük kusuru koşturularak gösterilmez, kural olarak modellenir; `volatile` atomiklik vermez |
| Senkronizasyon | Yeniden girme derinliği `0`'dan `2`'ye çıkıp `0`'a iner; üç kilit kurulumunun yalnız birinde tek kilit paylaşılır, kalan ikisinde ayrı kilit sayısı ikiye çıkar | çağıran (kilidi tutan) · oluşturulan (bekleyen) | Kilit karşılıklı dışlama ve görünürlük verir, bekleyenler arasında sıra vermez; adil kilit seçeneği modellenir |
| Eşzamanlılık Yardımcıları | Karşılaştır-ve-ata kurgulanmış çakışmada üç denemede sonuca ulaşır ve iş çağırandan hiç ayrılmaz; dört anlık görüntü sırasıyla 0, 1, 2 ve 3 öge görür, yazan taraf her değişiklikte yeni bir dizi üretir | çağıran (karşılaştır-ve-ata, gezinme) · oluşturulan (sayacı düşüren, izni bırakan) | Atomik tip yalnız tek değişkene atomiklik verir; iki değişkenin birlikte tutarlılığı koşturularak değil, kural olarak anlatılır |
| Yürütücü Servisleri | Havuz boşken kendi iş parçacığı, doluyken görevi veren taraf koşturur; reddetme yordamı çağıranın seçtiği bir parametredir | çağıran (dolu havuzda reddedilen) · oluşturulan (havuza verilen görev) | İki verme biçiminden yalnızca biri (`execute`) istisnayı kendiliğinden gösterir; `submit` yalnızca `get` çağrılırsa gösterir |
| Sanal İş Parçacıkları | Aynı görev kümesinin sabit havuzda dörtle sınırlı kalan, sanal yürütücüde görev sayısı kadar büyüyen ayrık kimliği; `ThreadLocal`'ın görev başına ayrı değer üretmesi | oluşturulan (her ikisinde de) | Ayrık kimlik sayısının büyümesi bir hız iddiası değil bir olgudur; taşıyıcı gözlemlenemez |
| Eşzamansız Bileşim | Tamamlanmış gelecekte çağıran, bekleyen gelecekte tamamlayan taraf, sonekli çağrıda bilinmeyen ya da verilen havuz koşar; hatadan sonraki aşamalar atlanır | çağıran (tamamlanmış) · oluşturulan (bekleyen, verilen havuz) · bilinmeyen (varsayılan havuz) | Sonek eklemek hızlandırmaz, yalnız koşturan tarafı değiştirir; dört yazımın sonucu birebir aynı kalır |

JVM, Eşzamanlılık ve Başarım burada kapanıyor. Kursun sorduğu soru — bu adımı hangi iş
parçacığı koşturuyor — bir Java programının her satırının altında hep durur; çağrı yazımı bu
soruyu hiçbir zaman kendiliğinden yanıtlamaz.

