ConcurrentHashMap — Güvenli Map, Güvensiz Sayaç
Ö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.
-
Bayt: Ziyaretçi sayacım gerçekte gelenlerden az gösteriyor.
-
Sen: Ama güvenli map'i kullanıyorsun. O güvenli değil miydi?
-
Bayt: Her çağrısı tek tek güvenli. Peki senin işin kaç çağrıdan oluşuyor?
-
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.
Adım adım oku
- A ve B aynı anda bakiyeyi okur. İkisi de 500 görür.
- İkisi de kafasında 100 ekler ve 600 yazar. Her işlem güvenliydi, ama bir yatırım kayboldu.
- merge ile okumak yerine talimat gönderilir: bu anahtarın değerine 100 ekle.
- 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.
`hits` bir ConcurrentHashMap. `hits.put(page, hits.getOrDefault(page, 0) + 1)` yük altında neden artış kaybeder?
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ır | put(k, get(k) + 1) | merge(k, 1, Integer::sum) |
| Yoksa oluştur | if (get(k) == null) put(k, v) | computeIfAbsent(k, key -> new V()) |
| Varsa güncelle | get + hesapla + put | computeIfPresent(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.
`map` bir ConcurrentHashMap. Her ifadeyi, eşzamanlı çağrıldığında anahtar başına atomik olup olmamasına göre ayır.
Kendin gör
Thread-safe map, thread-safe olmayan sayaç
Tohum 931061Map<String, Integer> hits = new ConcurrentHashMap<>();// iki thread, her biri 3 kez:int current = hits.getOrDefault(page, 0); // tek başına güvenlihits.put(page, current + 1); // tek başına güvenli — arada boşluk varOynat ya da adımla.
Ş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.
- Varsayılanla oynat. ConcurrentHashMap, get + put: 6 yerine 3.
- HashMap ve synchronizedMap’i dene. Sonuç aynı; kilit tek çağrıyı korur.
- Zamanlamayı “sırayla” yap. Yarışlı kod doğru sayar: tek thread’li testin gördüğü budur.
- merge seç, zamanlamayı “iç içe” yap. Tam 6.
Simülatörde yarışlı get + put, "sırayla" zamanlamada tam 6 saydı. Bu bulgu neyi anlatı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.
ConcurrentHashMap neden null anahtar ve null değer kabul etmez? (HashMap eder.)
Bir thread ConcurrentHashMap üzerinde for-each ile gezinirken başka bir thread yeni anahtarlar ekliyor. Ne olur?
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.
Aynı kullanıcı için bazen iki farklı Session nesnesi dolaşıyor ve biri kayboluyor. Hatalı satır hangisi?
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?
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
Kendini sına
ConcurrentHashMap'in her metodu tek başına thread-safe'tir.
`Map m = Collections.synchronizedMap(new HashMap<>())` üzerinde, başka thread'ler yazarken `for (var e : m.entrySet())` ile geziniyorsun. Doğru kullanım hangisi?
Aklında kalacak üç şey
- 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 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 ConcurrentHashMap null kabul etmez ve gezinirken anlık bir kopya vermez. Tek thread'li bir test yarışı hiçbir zaman göstermez.
5 kart sonraki derste seni bekliyor