İçeriğe geç

ConcurrentHashMap — Güvenli Map, Güvensiz Sayaç

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

Önce şunu oku: Race Condition ve Görünürlük , HashMap, equals ve hashCode

30 saniyede özet

ConcurrentHashMap her çağrıyı tek tek güvenli yapar, iki çağrıdan kurduğun işi değil. get ile put arasına başka bir thread girer ve artışlar kaybolur. Çözüm: işi merge ya da compute ile tek çağrıya indirmek.

Sitene kaç kişinin girdiğini sayan sayaç, gerçekte gelenlerden az gösteriyor. Oysa kod “thread-safe” etiketli ConcurrentHashMap kullanıyor.

  1. Bayt: Ziyaretçi sayacım gerçekte gelenlerden az gösteriyor.

  2. Sen: Ama güvenli map'i kullanıyorsun. O güvenli değil miydi?

  3. Bayt: Her çağrısı tek tek güvenli. Peki senin işin kaç çağrıdan oluşuyor?

  4. Bayt: Önce tahmin et, sonra bankaya gidelim.

Güvenli çağrı, güvenli iş demek değil

İki thread aynı anda hits.put(page, hits.getOrDefault(page, 0) + 1) çalıştırıyor. hits bir ConcurrentHashMap. Sayı her zaman doğru mu? Cevabı göster

Hayır. getOrDefault ve put tek tek güvenli, ama ikisinin arası değil. İki thread aynı değeri okur, ikisi de aynı sonucu yazar: bir artış kaybolur.

Bakiyeyi okuyup yazmak ya da bankaya 100 ekle demek.
Adım adım oku
  1. A ve B aynı anda bakiyeyi okur. İkisi de 500 görür.
  2. İkisi de kafasında 100 ekler ve 600 yazar. Her işlem güvenliydi, ama bir yatırım kayboldu.
  3. merge ile okumak yerine talimat gönderilir: bu anahtarın değerine 100 ekle.
  4. Map talimatları sırayla uygular: 600, sonra 700. Hiçbir yatırım kaybolmaz.

Bu kalıba check-then-actÖnce bir durumu okuyup sonra ona göre yazmak: 'yoksa ekle', 'oku ve bir artır'. Okuma ile yazma arasında başka bir thread durumu değiştirebilir.Sözlükte gör → denir: önce bak, sonra baktığına göre davran. Bakmak ile davranmak arasında başka bir thread değeri değiştirebilir.

Koleksiyonun thread-safe olması bu boşluğu kapatmaz: her çağrı kendi başına güvenlidir, iki çağrının arası değil. synchronizedMap de aynı durumdadır.

Kafam karıştı, daha basit anlat

Önce bakıp sonra yazmak iki ayrı adımdır. Sen bakarken başka biri değeri değiştirebilir. Güvenli bir map bile bu arayı senin yerine kapatmaz.

Hızlı kontrolOrta

`hits` bir ConcurrentHashMap. `hits.put(page, hits.getOrDefault(page, 0) + 1)` yük altında neden artış kaybeder?

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

Tek çağrıda oku, değiştir, yaz

ConcurrentHashMap’in asıl marifeti, oku-değiştir-yaz adımlarını tek bir 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 → (bölünemez) çağrıda birleştiren metotlarıdır.

İhtiyaçYanlışDoğru
Sayaç artırput(k, get(k) + 1)merge(k, 1, Integer::sum)
Yoksa oluşturif (get(k) == null) put(k, v)computeIfAbsent(k, key -> new V())
Varsa güncelleget + hesapla + putcomputeIfPresent(k, (key, old) -> …)

Bu metotlar, anahtarın bulunduğu bölmeyi fonksiyon çalışırken kilitler. Aynı anahtara yazmak isteyen diğer thread’ler bekler.

Kafam karıştı, daha basit anlat

merge ve compute, bakma ile yazmayı tek bir hamlede yapar. O hamle bitene kadar aynı anahtara kimse dokunamaz.

Hızlı kontrolOrta

`map` bir ConcurrentHashMap. Her ifadeyi, eşzamanlı çağrıldığında anahtar başına atomik olup olmamasına göre ayır.

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

Sınıflandırılmamış

Atomik

Oku ve yaz tek çağrıda.

    Yarışa açık

    İki çağrının arasına bir thread girebilir.

      Kendin gör

      Thread-safe map, thread-safe olmayan sayaç

      Tohum 931061
      HitCounter.java
      1Map<String, Integer> hits = new ConcurrentHashMap<>();
      2// iki thread, her biri 3 kez:
      3int current = hits.getOrDefault(page, 0); // tek başına güvenli
      4hits.put(page, current + 1); // tek başına güvenli — arada boşluk var
      Java 21UTF-8LF

      Oynat ya da adımla.

      Hız
      Adım 0

      Şu an ne oldu?

      İki thread, her biri 3 ziyaret sayacak — beklenen 6

      Her thread sayfanın sayacını okuyup bir artırıyor. Map'in kendisi thread-safe mi, artırma işlemi mi?

      Görevler0/3

      • ConcurrentHashMap kullanırken artış kaybetaçık

        İpucu

        İki ayrı çağrı, iç içe zamanlama.

      • Yarışlı kodun doğru sonuç verdiği zamanlamayı bulaçık

        İpucu

        Tek thread'li bir test neyi görür?

      • İç içe zamanlamada tam 6 sayaçık

        İpucu

        Okuma ve yazmayı tek çağrıda birleştir.

      Olay günlüğü (0)

      Henüz olay yok. Oynat veya adımla.

      1. Varsayılanla oynat. ConcurrentHashMap, get + put: 6 yerine 3.
      2. HashMap ve synchronizedMap’i dene. Sonuç aynı; kilit tek çağrıyı korur.
      3. Zamanlamayı “sırayla” yap. Yarışlı kod doğru sayar: tek thread’li testin gördüğü budur.
      4. merge seç, zamanlamayı “iç içe” yap. Tam 6.
      Hızlı kontrolOrta

      Simülatörde yarışlı get + put, "sırayla" zamanlamada tam 6 saydı. Bu bulgu neyi anlatıyor?

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

      İçeride ne var?

      Kilitler nerede?· istersen atla

      Java 8’den beri map’in tamamını ya da büyük parçalarını kilitlemez. Boş bir bölmeye CAS ile yazar, dolu bölmede yalnızca o bölmenin ilk düğümünü kilitler; get hiç kilit almaz.

      Null kabul etmez. get null döndürdüğünde bu “anahtar yok” demektir; null değer olsaydı ikisi ayırt edilemezdi.

      Gezinirken map değişirse hata fırlatmaz, ama değişikliği görmeyebilir de; buna weakly consistent iteratorGezinme sırasında koleksiyon değişirse exception fırlatmayan, ama değişikliği görmeyebilen iterator. Anlık bir görüntü değildir.Sözlükte gör → denir. Yoğun anlarda size() da yalnızca yaklaşık bir sayıdır.

      Kafam karıştı, daha basit anlat

      Map bütün dolabı kilitlemez, yalnızca yazılan çekmeceyi kilitler. Okumak için hiç kilit gerekmez, bu yüzden hızlıdır.

      Hızlı kontrolOrta

      ConcurrentHashMap neden null anahtar ve null değer kabul etmez? (HashMap eder.)

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

      Bir thread ConcurrentHashMap üzerinde for-each ile gezinirken başka bir thread yeni anahtarlar ekliyor. Ne olur?

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

      Tuzaklar

      computeIfAbsent içinde uzak çağrı. Fonksiyon çalıştığı sürece aynı bölmeye yazmak isteyen herkes bekler. Pahalı işi dışarıda yap ya da değer olarak bir CompletableFuture sakla.

      Fonksiyonun içinden map'e dokunmak· istersen atla

      compute ya da merge’e verilen fonksiyonun içinden map’in başka bir anahtarına yazmak yasaktır. Sonu IllegalStateException ya da kilitlenme olabilir.

      Yoğun sayaç. Çok thread aynı anahtarı artırıyorsa değer olarak LongAdder sakla: computeIfAbsent(k, key -> new LongAdder()).increment().

      synchronizedMap üzerinde gezinmek. Her çağrı kilitli, gezinme değil; döngüyü synchronized (map) içine almak gerekir.

      Hızlı kontrolİleri

      Aynı kullanıcı için bazen iki farklı Session nesnesi dolaşıyor ve biri kayboluyor. Hatalı satır hangisi?

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

      Hatalı satıra dokun, sonra kontrol et.

      SessionRegistry.java
      Java 21UTF-8LF

      Bir fiyat cache'i `prices.computeIfAbsent(sku, s -> pricingClient.fetch(s))` ile yazılmış; fetch bir HTTP çağrısı. Yük altında ne olabilir?

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

      Aşağıdaki program bir kart işlemcisinin hız kontrolünü üç biçimde yapıyor: check-then-act ile bozuk, atomik metotlarla doğru ve yüksek eşzamanlılıkta sayaç için LongAdder ile.

      Derinleş · Kart hız kontrolü: ConcurrentHashMap'i doğru kullanmak 4 dosya · ~71 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/fraud/ BrokenVelocityCounter.java Bozuk: get ve put ayrı ayrı güvenli, ama ikisinin arası değil. Sayılar kaybolur ve dolandırıcılık kuralı eşiği hiç görmez.

      src/main/java/fraud/BrokenVelocityCounter.java
      // Velocity check: how many authorisations has this card had in the current window?
      // A stolen card is often tested with many small payments in a short time.
      public class BrokenVelocityCounter {
      private final Map<String, Integer> counts = new ConcurrentHashMap<>();
      // Each call is thread-safe on its own; the SEQUENCE is not.
      // Two threads can both read 7 and both write 8: one attempt is lost.
      public void record(String cardToken) {
      Integer current = counts.get(cardToken);
      counts.put(cardToken, current == null ? 1 : current + 1);
      }
      public long count(String cardToken) {
      return counts.getOrDefault(cardToken, 0);
      }
      }

      src/main/java/fraud/ VelocityCounter.java Doğru: computeIfAbsent ve merge tek atomik adım. Sayaç LongAdder ile çekişmesiz.

      src/main/java/fraud/VelocityCounter.java
      public class VelocityCounter {
      private final ConcurrentMap<String, LongAdder> counts = new ConcurrentHashMap<>();
      // computeIfAbsent is atomic per key: one LongAdder per card, never two.
      // LongAdder spreads increments across cells, so a hot card does not serialize threads.
      public void record(String cardToken) {
      counts.computeIfAbsent(cardToken, key -> new LongAdder()).increment();
      }
      public long count(String cardToken) {
      LongAdder adder = counts.get(cardToken);
      return adder == null ? 0 : adder.sum();
      }
      // merge: atomic "put if absent, otherwise combine" for immutable values,
      // e.g. the total amount per merchant for the day.
      public static void addAmount(ConcurrentMap<String, BigDecimal> totals, String merchantId, BigDecimal amount) {
      totals.merge(merchantId, amount, BigDecimal::add);
      }
      }

      src/main/java/fraud/ IssuerClientRegistry.java compute içinde uzun iş yapmamak: pahalı nesne dışarıda hazırlanır, map'e yalnızca yerleştirilir.

      src/main/java/fraud/IssuerClientRegistry.java
      // A payment processor talks to many card-issuing banks; one client per issuer.
      public class IssuerClientRegistry {
      private final ConcurrentMap<String, IssuerClient> clients = new ConcurrentHashMap<>();
      // The mapping function runs while the key's bin is locked: other writers to
      // nearby keys wait. Keep it short and never touch the same map inside it.
      public IssuerClient forIssuer(String issuerCode) {
      IssuerClient existing = clients.get(issuerCode); // fast path, no locking
      if (existing != null) return existing;
      return clients.computeIfAbsent(issuerCode, IssuerClient::connect); // connect() must be quick
      }
      }

      src/main/java/fraud/ Main.java Hepsini aynı yükle çalıştırır; beklenen ve gerçek sonuçlar yan yana.

      src/main/java/fraud/Main.java
      public class Main {
      public static void main(String[] args) {
      var broken = new BrokenVelocityCounter();
      var counter = new VelocityCounter();
      String card = "tok_4f9a"; // a token, never the card number itself
      try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
      for (int i = 0; i < 10_000; i++) {
      pool.submit(() -> {
      broken.record(card);
      counter.record(card);
      });
      }
      }
      System.out.println("broken: " + broken.count(card)); // often less than 10000, varies per run
      System.out.println("counter: " + counter.count(card)); // always 10000
      }
      }

      Kendini sına

      Şimşek turu1/5

      ConcurrentHashMap'in her metodu tek başına thread-safe'tir.

      Soru 1/2Orta

      `Map m = Collections.synchronizedMap(new HashMap<>())` üzerinde, başka thread'ler yazarken `for (var e : m.entrySet())` ile geziniyorsun. Doğru kullanım hangisi?

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

      Aklında kalacak üç şey

      1. 1 Thread-safe bir koleksiyon tek tek çağrıları korur. get ve put'tan kurulan bir artırma, iki çağrının arasında güncelleme kaybeder.
      2. 2 merge, compute ve computeIfAbsent anahtar başına tek seferde çalışır. İçlerine verilen fonksiyon kısa olmalı ve map'in başka anahtarlarına dokunmamalıdır.
      3. 3 ConcurrentHashMap null kabul etmez ve gezinirken anlık bir kopya vermez. Tek thread'li bir test yarışı hiçbir zaman göstermez.
      Sonraki kapı Bir milyon thread açmak bilgisayarı çökertir mi? Artık çökertmiyor. Peki ne değişti? Virtual Threads vs Platform Threads · 10 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı