Race Condition ve Görünürlük
30 saniyede özet
İki thread aynı sayacı artırınca bazı artışlar kaybolur, çünkü count++ tek adım değildir. volatile bunu düzeltmez: kayıp artış ve bayat okuma iki ayrı sorundur.
İki kişi aynı anda defterdeki sayıyı birer artırıyor, ama sayı yalnızca bir artıyor. Kimse hata yapmadı, kimse uyarı da almadı.
-
Bayt: İki thread'e sayacı onar bin kez artırttım. Sonuç yirmi binden az, üstelik her çalıştırmada başka!
-
Sen: Kod tek satır:
count++. Nerede kayboluyor ki? -
Bayt: O tek satır aslında üç adım: oku, bir ekle, yaz. Arada başka biri de okuyabilir.
-
Bayt: İki artış yapıldı, sayaç bir arttı. Ve ortada tek bir hata mesajı yok!
Bilgisayarda aynı sayacı paylaşan iki thread de tam olarak bunu yaşar. Sonuç her seferinde farklı:
int count = 0;
Runnable task = () -> { for (int i = 0; i < 10_000; i++) { count++; }};// iki thread çalıştır, join et, count'u yazdırİki thread × 10.000 artış. Beklenen 20.000. Gerçekte 20.000’den küçük, ve her seferinde farklı bir sayı.
count++ neden tek adım değil?
Bytecode seviyesinde üç ayrı adımdır:
getfield count // 1. okuiconst_1 / iadd // 2. bir ekleputfield count // 3. yazBu üç adım atomicBölünemeyen işlem — ya tamamen olur ya hiç olmaz, arada başka thread araya giremez. `count++` atomik değildir; oku-artır-yaz üç adımdır.Sözlükte gör → değildir, yani bölünebilir: aralarına başka bir thread girebilir. İki thread aynı değeri okursa ikisi de aynı sonucu yazar; iki artış yapılmış, sayaç bir artmıştır. Buna kayıp güncelleme denir.
İki thread `count++` çalıştırıyor ve `count` 0. Sonuç kesin olarak 2 midir? Cevabı göster
Hayır. 2 de olabilir, 1 de. İkisi de aynı değeri okursa bir artış sessizce kaybolur.
Adım adım oku
- A defterdeki sayıyı okur: 17.
- A yeni değeri yazmadan önce B de okur ve o da 17 görür.
- İkisi de kafasında bir ekler ve deftere 18 yazar.
- İki artış yapıldı, sayaç yalnızca bir arttı. Hiçbir hata mesajı çıkmadı.
Bir artış nasıl kaybolur
// count++ aslında üç ayrı adımdır:int tmp = count; // 1. okutmp = tmp + 1; // 2. artırcount = tmp; // 3. yazDebug
thread A A sayacı okur.
- count
- = 0
- tmpA
- = 0
Sol/sağ ok tuşlarıyla da gezebilirsin.
Kafam karıştı, daha basit anlat
İki kişi aynı kumbaradaki parayı sayıyor: ikisi de 10 görüyor, ikisi de bir lira ekleyip “11” yazıyor. İki lira eklendi ama kumbarada 11 yazıyor. count++ da böyle üç adımdır ve araya başkası girebilir.
En kısa hâliyle race condition nedir?
Bir yarış durumunun ortaya çıkması için hangi üç koşul birden gerekir?
İkinci ve daha sinsi sorun: görünürlük
İkinci sorun visibilityBir thread'in yazdığı değeri başka bir thread'in görüp görememesi. Görünürlük sorunu bir sıralama sorunu değildir: değer hiç görünmeyebilir.Sözlükte gör →: bir thread’in yazdığını öteki ne zaman görecek? Senkronizasyon yoksa Java hiçbir söz vermez; teorik olarak hiç görmeyebilir.
Bu bir hata değil, hız için verilmiş bir karar: işlemci değerleri kendi cebinde (register, cache) tutabilsin diye. Her thread’in kendi not defteri var gibi düşün; ortak deftere ne zaman geçirileceği belli değil.
Kafam karıştı, daha basit anlat
Her thread’in kendi not defteri var. Birinin yazdığı not, ortak panoya ne zaman asılacak belli değil. Senkronizasyon, “şimdi panoya as” demenin yoludur.
Bir thread `while (!durdu) { }` ile dönüyor, başka bir thread `durdu = true` yapıyor. `durdu` sıradan bir `boolean`. Ne olabilir?
Kendin gör
Simülatörü önce varsayılan (Hiçbiri) ayarla çalıştır. Sonra senkronizasyon modunu tek tek değiştir ve sonucu karşılaştır.
count++ — kayıp güncelleme ve görünürlük
Tohum 5Ana bellek · count
0
beklenen: 12
- T0bekliyorregister = 0cache = —kalan 6
- T1bekliyorregister = 0cache = —kalan 6
count++ aslında üç işlemdir:
1. oku → 2. bir ekle → 3. yaz
Şu an ne oldu?
Zamanlayıcı sırayı seçiyor
count++ üç adımdır: oku, bir ekle, yaz. Zamanlayıcı bu adımların arasında başka bir thread’e geçebilir ve hangi sırayla geçeceğini kimse belirleyemez.
Aklında kalsın: Bu non-determinizm yüzünden yarış durumları testlerde çoğu zaman görünmez. "Bende çalışıyor" bir kanıt değildir.
Görevler0/3
Kilitsiz bir çalıştırmada kayıp güncelleme yakalaaçık
İpucu
Senkronizasyon "Hiçbiri" iken sonuna kadar oynat. Sonuç beklenenden azsa bir artış ezildi demektir. Olmazsa "Yeni senaryo" dene.
volatile’ın kayıp güncellemeyi durdurmadığını kanıtlaaçık
İpucu
Modu volatile yap ve thread sayısını artır. Görünürlük düzelir, ama oku-ekle-yaz hâlâ bölünebilir.
AtomicInteger ile doğru sonuca ulaş ve en az bir CAS tekrarı göraçık
İpucu
Modu AtomicInteger yap, 3-4 thread seç. Doğruluk bedava değil: çakışan her yazı döngüyü baştan başlatır.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
Dört modu da mutlaka dene:
Hiçbiri— hem kayıp güncelleme hem bayat okuma.Cache tazelenme şansını 0 yap: thread’ler birbirinin yazdığını hiç görmez. JMM bunu açıkça serbest bırakır.volatile— bayat okuma sıfırlanır, kayıp güncelleme devam eder. En kritik gözlem bu.synchronized— sonuç her zaman doğru. Karşılığında aynı anda yalnızca bir thread ilerliyor.AtomicInteger— sonuç doğru, kilit yok, ama CAS tekrar sayacına bak. Thread sayısını 4 yap ve tekrarların nasıl arttığını gör.
Simülatörün altındaki Görevler listesi bu gözlemleri tek tek hedefe çeviriyor.
Her ifadeyi ait olduğu problem sınıfına yerleştir.
Dört çözüm, dört farklı şey
| Görünürlük | Atomiklik | Bloklar mı | |
|---|---|---|---|
int count | ✕ | ✕ | — |
volatile int count | ✓ | ✕ | Hayır |
synchronized | ✓ | ✓ | Evet |
AtomicInteger | ✓ | ✓ | Hayır (CAS döngüsü) |
volatile satırındaki ✓ / ✕ kombinasyonu, en sık yanlış bilinen şeydir. volatile bir kilit değildir. Tek bir okuma veya tek bir yazma için yeterlidir; oku-değiştir-yaz için değildir.
volatile’ın doğru kullanımı:
private volatile boolean stopped = false; // tek yazma, tek okuma → yeterli
public void stop() { stopped = true; } // atomik tek yazmapublic void run() { while (!stopped) { doWork(); } // her turda belleği okur}Kafam karıştı, daha basit anlat
volatile panoya hemen asmayı garanti eder, ama iki kişinin aynı anda yazmasını engellemez. Oku-değiştir-yaz gerekiyorsa kilit ya da AtomicInteger kullan.
`volatile int count` tanımlayıp `count++` kullanırsan sonuç doğru olur mu?
Her aracı, çözdüğü problemi hangi tarafta kapattığına göre ayır.
Asıl kural: happens-before
Java’nın tek sözü şudur: iki işlem arasında bir happens-beforeJava Memory Model'in verdiği garanti: A işlemi B'den önce gerçekleşiyorsa, A'nın yazdığı her şeyi B görür. `synchronized` ve `volatile` bu ilişkiyi kurar.Sözlükte gör → bağı varsa, önce olanın yazdığı her şey sonrakine görünür. Bağ yoksa hiçbir söz yok.
Bu bağı ne kurar?· istersen atla
- Aynı thread içindeki program sırası.
- Bir kilidin bırakılması, aynı kilidin sonraki alınmasından önce gelir.
- Bir
volatilealana yazma, aynı alanın sonraki okumasından önce gelir. Thread.start()vejoin()da bu bağı kurar.
Bu yüzden join() sonrası count’u okumak görünürlük açısından güvenlidir; kayıp güncellemeler ise zaten çoktan olmuştur.
Aşağıdaki program bir bankanın en bilinen yarışını canlandırıyor: sekiz ATM aynı hesaptan aynı anda para çekiyor. Hesap üç biçimde yazılıp aynı yük altında çalışıyor; bir de gün sonu kesimini volatile ile gören bir döngü var.
Derinleş · Aynı hesaptan aynı anda para çekmek: çalıştırılabilir örnek 4 dosya · ~125 satır · ilk okumada atlayabilirsin
AtomicInteger nasıl çalışır?· istersen atla
Kilit yok; işlemcinin “hâlâ eski değerse yaz” (compare-and-swap) komutu var:
int prev, next;do { prev = get(); // oku next = prev + 1; // hesapla} while (!compareAndSet(prev, next)); // bellekte hâlâ prev varsa yaz, yoksa baştanKalabalık yoksa tek turda biter; kalabalık arttıkça döngü tekrar eder. Çok yoğun sayaçlarda LongAdder daha iyi ölçeklenir.
Kendini sına
Önce hızlı bir ısınma: puan yok, kayıt yok.
count++ tek ve bölünmez bir işlemdir.
İki thread aynı sayacı `synchronized` olmadan artırıyor. Program ne yazdırır?
Aklında kalacak üç şey
- 1 count++ tek adım değil, üç adımdır: oku, bir ekle, yaz. Başka bir thread araya girebilir.
- 2 volatile, değişikliğin diğer thread'lerce görülmesini sağlar ama aynı anda yazmayı engellemez. count++ volatile ile de bozulur.
- 3 AtomicInteger kilitsiz çalışır ama kalabalıkta denemeyi tekrarlar. Çok yoğun sayaçlarda LongAdder daha iyi ölçeklenir.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.
- Core Java & EşzamanlılıkConcurrentHashMap — Güvenli Map, Güvensiz SayaçGüvenli olduğu söylenen bir map'te sayaç neden yine artış kaybeder?Derse git
- Core Java & EşzamanlılıkJava Memory Model ve Yeniden SıralamaKodun, yazdığın sırayla çalıştığından emin misin? İşlemci her zaman o kadar emin değil.Derse git