İçeriğe geç

Race Condition ve Görünürlük

İleri 10 dk Çok sık karşılaşılır

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ı.

  1. Bayt: İki thread'e sayacı onar bin kez artırttım. Sonuç yirmi binden az, üstelik her çalıştırmada başka!

  2. Sen: Kod tek satır: count++. Nerede kayboluyor ki?

  3. Bayt: O tek satır aslında üç adım: oku, bir ekle, yaz. Arada başka biri de okuyabilir.

  4. 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ı:

Counter.java
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. oku
iconst_1 / iadd // 2. bir ekle
putfield count // 3. yaz

Bu üç 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.

İki artış, bir adım: kaybolan şey işlemin bölünmezliği.
Adım adım oku
  1. A defterdeki sayıyı okur: 17.
  2. A yeni değeri yazmadan önce B de okur ve o da 17 görür.
  3. İkisi de kafasında bir ekler ve deftere 18 yazar.
  4. İki artış yapıldı, sayaç yalnızca bir arttı. Hiçbir hata mesajı çıkmadı.

Bir artış nasıl kaybolur

KayipGuncelleme.java
1// count++ aslında üç ayrı adımdır:
şu an çalışan satırint tmp = count; // 1. oku
3tmp = tmp + 1; // 2. artır
4count = tmp; // 3. yaz

Debug

Adım 1/6

thread A A sayacı okur.

count
= 0
tmpA
= 0
Java 21UTF-8LF2:1

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.

Hızlı kontrolOrta

En kısa hâliyle race condition nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Bir yarış durumunun ortaya çıkması için hangi üç koşul birden gerekir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

İ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.

Hızlı kontrolİleri

Bir thread `while (!durdu) { }` ile dönüyor, başka bir thread `durdu = true` yapıyor. `durdu` sıradan bir `boolean`. Ne olabilir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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 5

Ana 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

Hız
Adım 0

Ş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.

Hızlı kontrolİleri

Her ifadeyi ait olduğu problem sınıfına yerleştir.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Görünürlük sorunu

Yazılan değer diğer thread'e ulaşmıyor

    Atomiklik sorunu

    İşlem bölünebiliyor, araya girilebiliyor

      Dört çözüm, dört farklı şey

      GörünürlükAtomiklikBloklar 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ı:

      Doğru volatile kullanımı
      private volatile boolean stopped = false; // tek yazma, tek okuma → yeterli
      public void stop() { stopped = true; } // atomik tek yazma
      public 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.

      Hızlı kontrolİleri

      `volatile int count` tanımlayıp `count++` kullanırsan sonuç doğru olur mu?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Her aracı, çözdüğü problemi hangi tarafta kapattığına göre ayır.

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Sınıflandırılmamış

      Sadece görünürlük

      Değeri diğer thread'e gösterir, araya girilmesini engellemez

        Atomiklik + görünürlük

        Hem araya girilmesini engeller hem değeri gösterir

          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 volatile alana yazma, aynı alanın sonraki okumasından önce gelir.
          • Thread.start() ve join() 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
          Proje dosyaları

          src/main/java/account/ UnsafeAccount.java Güvensiz: 'bakiye yetiyor mu?' kontrolü ile düşme ayrı adımlar. İki thread aynı bakiyeyi görüp ikisi de çekebilir; bakiye eksiye düşer ya da bir çekim kaybolur.

          src/main/java/account/UnsafeAccount.java
          // Amounts in kuruş (minor units) as long: exact, and cheap for this demo.
          public class UnsafeAccount implements Account {
          private long balance;
          public UnsafeAccount(long balance) {
          this.balance = balance;
          }
          @Override
          public boolean withdraw(long amount) {
          if (balance >= amount) { // check ...
          balance -= amount; // ... then act: another thread can run in between,
          return true; // and `balance -= amount` is itself read, subtract, write
          }
          return false;
          }
          @Override
          public long balance() {
          return balance;
          }
          }

          src/main/java/account/ Accounts.java Güvenli iki yol: kontrol ve düşmeyi kilitle tek adım yap, ya da compareAndSet döngüsüyle kilitsiz yap.

          src/main/java/account/Accounts.java
          // The shared contract and the two safe versions, in one listing for brevity.
          interface Account {
          boolean withdraw(long amount);
          long balance();
          }
          final class SynchronizedAccount implements Account {
          private long balance;
          SynchronizedAccount(long balance) {
          this.balance = balance;
          }
          @Override
          public synchronized boolean withdraw(long amount) { // check and act as one step
          if (balance < amount) return false;
          balance -= amount;
          return true;
          }
          @Override
          public synchronized long balance() {
          return balance;
          }
          }
          final class AtomicAccount implements Account {
          private final AtomicLong balance;
          AtomicAccount(long balance) {
          this.balance = new AtomicLong(balance);
          }
          @Override
          public boolean withdraw(long amount) {
          while (true) {
          long current = balance.get();
          if (current < amount) return false;
          // Succeeds only if nobody changed the balance since we read it; otherwise retry.
          if (balance.compareAndSet(current, current - amount)) return true;
          }
          }
          @Override
          public long balance() {
          return balance.get();
          }
          }

          src/main/java/account/ TransferIntake.java Görünürlük: volatile olmadan transfer kabul eden döngü, gün sonu kesimini hiç görmeyebilir.

          src/main/java/account/TransferIntake.java
          public class TransferIntake implements Runnable {
          // Set by the end-of-day job at cut-off. Without volatile, this loop may keep
          // reading a cached `true` forever and accept transfers into a closed business day.
          private volatile boolean accepting = true;
          @Override
          public void run() {
          while (accepting) {
          Thread.onSpinWait(); // in real code: take the next transfer from a queue
          }
          System.out.println("cut-off reached, intake stopped");
          }
          public void cutOff() {
          accepting = false;
          }
          }

          src/main/java/account/ Main.java Hepsini aynı yükle çalıştırır. Güvensiz hesabın sonucu çalıştırmadan çalıştırmaya değişir.

          src/main/java/account/Main.java
          public class Main {
          static final int ATMS = 8;
          static final int ATTEMPTS_PER_ATM = 250_000;
          static final long OPENING_BALANCE = 1_000_000; // kuruş
          public static void main(String[] args) throws Exception {
          List<Account> accounts = List.of(new UnsafeAccount(OPENING_BALANCE),
          new SynchronizedAccount(OPENING_BALANCE), new AtomicAccount(OPENING_BALANCE));
          for (Account account : accounts) {
          var approved = new LongAdder();
          try (var pool = Executors.newFixedThreadPool(ATMS)) {
          for (int t = 0; t < ATMS; t++) {
          pool.submit(() -> {
          for (int i = 0; i < ATTEMPTS_PER_ATM; i++) {
          if (account.withdraw(1)) approved.increment();
          }
          });
          }
          } // close() waits for all tasks
          System.out.printf("%s: balance %,d, approved %,d (paid out + balance should be %,d)%n",
          account.getClass().getSimpleName(), account.balance(), approved.sum(), OPENING_BALANCE);
          }
          // 2,000,000 attempts against 1,000,000 kuruş: exactly 1,000,000 may be approved.
          // UnsafeAccount: approved + balance != 1,000,000, and it differs every run.
          // SynchronizedAccount and AtomicAccount: balance 0, approved 1,000,000.
          TransferIntake intake = new TransferIntake();
          Thread thread = Thread.ofPlatform().start(intake);
          Thread.sleep(100);
          intake.cutOff(); // with volatile, the loop sees this and exits
          thread.join();
          }
          }
          AtomicInteger nasıl çalışır?· istersen atla

          Kilit yok; işlemcinin “hâlâ eski değerse yaz” (compare-and-swap) komutu var:

          AtomicInteger.incrementAndGet() — özü
          int prev, next;
          do {
          prev = get(); // oku
          next = prev + 1; // hesapla
          } while (!compareAndSet(prev, next)); // bellekte hâlâ prev varsa yaz, yoksa baştan

          Kalabalı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.

          Şimşek turu1/5

          count++ tek ve bölünmez bir işlemdir.

          Soru 1/4İleri

          İki thread aynı sayacı `synchronized` olmadan artırıyor. Program ne yazdırır?

          Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
          Counter.java
          1int count = 0;
          2
          3Runnable task = () -> {
          4 for (int i = 0; i < 10_000; i++) {
          5 count++;
          6 }
          7};
          8
          9var t1 = Thread.ofPlatform().start(task);
          10var t2 = Thread.ofPlatform().start(task);
          11t1.join();
          12t2.join();
          13
          14System.out.println(count);
          Java 21UTF-8LF

          Bu çıktı garanti değildir — program non-deterministiktir, farklı çalıştırmada başka sonuç verebilir.

          Aklında kalacak üç şey

          1. 1 count++ tek adım değil, üç adımdır: oku, bir ekle, yaz. Başka bir thread araya girebilir.
          2. 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. 3 AtomicInteger kilitsiz çalışır ama kalabalıkta denemeyi tekrarlar. Çok yoğun sayaçlarda LongAdder daha iyi ölçeklenir.
          Sonraki kapı İki thread, iki kilit ve bir sonsuzluk: kimse suç işlemeden program nasıl sonsuza kadar donar? Concurrency Araçları — Executor, Lock, Atomic ve Deadlock · 9 dk

          5 kart sonraki derste seni bekliyor

          0/5 kart bu dersten toplandı

          Bu dersin üstüne kurulanlar

          Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.