Ders 01 / 11
Sınıf Yükleme
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.
İçindekiler
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 datoStringçı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.
// 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, Ozelin bayt kodu
yalnızca alttakinin baktığı dizinde. Böylece iki dizin, iki ayrı yükleyicinin görebildiği iki
ayrı kaynağı temsil eder.
// 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
CountDownLatchile 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.
// 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.
// YeniOrnekTetigi.java — yeni ornek olusturma baslatmayi tetikler mi
class YeniOrnekTetigi { static { Bas.gozle("yeni ornek olusturma"); } }
// StatikYontemTetigi.java — statik yontem cagirma baslatmayi tetikler mi
class StatikYontemTetigi { static { Bas.gozle("statik yontem cagirma"); } static void yontem() { } }
// StatikAlanTetigi.java — final olmayan statik alan okuma baslatmayi tetikler mi
class StatikAlanTetigi { static { Bas.gozle("statik alan okuma"); } static int deger = 5; }
// 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.
// 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; }
// 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çinnulldö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.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.