Ders 06 / 11
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.
İçindekiler
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.
// 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
bekleadlı 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.
// 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 (finalalan) 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. finalalanı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.
volatilegörünürlük ve sıralama verir, atomiklik vermez; oku-artır-yaz üçlemesi birvolatilealanda 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?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.