JavaProgramlamaKıdemli Java Geliştirici

**ConcurrentHashMap**'ın **computeIfAbsent** metodunun değer başlatma sırasında aynı anahtar üzerinde kendisini özyinelemeli olarak çağırdığında hangi spesifik reentransiyon tehlikesi ortaya çıkar ve implementasyon bu dairesel hesaplama senaryosunu nasıl tespit eder ve engeller?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun Cevabı

ConcurrentHashMap'ın computeIfAbsent metodu, tüm tabloyu kilitlemek yerine, hash bin seviyesinde ince kilitleme kullanarak atomik, iş parçacığı güvenli değer hesaplama sağlar. Bu metoda sağlanan mappingFunction'ın, execution sırasında aynı harita örneği içinde aynı anahtara özyinelemeli erişim sağlaması durumunda kritik bir reentransiyon tehlikesi ortaya çıkar ve bu potansiyel dairesel bağımlılık yaratır.

Java 8'de, bu özyinelemeli erişim bir deadlock'a neden oldu çünkü implementasyon, hesaplama sırasında belirli bir hash binini kilitlemişti ve özyinelemeli çağrı, mevcut iş parçacığı tarafından zaten tutulmakta olan aynı kilidi edinmeye çalışıyordu. Java 9'dan itibaren, implementasyon bu özyinelemeyi, hesaplama sırasında bin içine bir ReservationNode yer tutucusu ekleyerek tespit eder ve onu "devam eden" olarak işaretler. Eğer aynı iş parçacığı, aynı anahtar için dolaşırken bu ReservationNode ile karşılaşırsa, metod bir IllegalStateException atar ve mesaj olarak "Özyinelemeli güncelleme" verir, böylece deadlock olmadan geçersiz özyineleme hakkında anında geri bildirim sağlar.

Bu fail-fast mekanizması, ForkJoinPool ortak havuzunda ve deadlock'ların felaket olabileceği diğer yürütücü bağlamlarda iş parçacığı açlığı ve canlılık sorunlarını önler. Ancak, geliştiricilerin anahtarlar arasında dairesel bağımlılıkları önlemek için hesaplama mantıklarını dikkatlice yapılandırmalarını gerektirir, bu da sıklıkla alan katmanında açık döngü tespiti gerektirir.

Hayattan Bir Durum

Bu tehlikeyle, finansal enstrümanlar için türev hesaplamalarını önceden yüklenmiş Monte Carlo simülasyonlarından kaçınmak amacıyla önbelleğe alan yüksek verimli bir fiyatlandırma motorunda karşılaştık. Önbellek, aynı opsiyon fiyatlandırma taleplerinin tek seferde deduplike edilmesini ve her piyasa verisi vuruşunda tam olarak bir kez hesaplanmasını sağlamak için ConcurrentHashMap<String, CompletableFuture<BigDecimal>> ile computeIfAbsent kullandı. Bu desen, maliyetli hesaplamaların birden fazla eşzamanlı talep arasında paylaşılması gereken asenkron veri yükleme senaryolarında yaygındır.

Sorun, karmaşık türevleri hesaplarken, veri modelleme hatası nedeniyle aynı önbellek içindeki diğer türevleri istemeden referans vermekten kaynaklandı. Özellikle, Enstrüman A'nın fiyatlandırma formülü, Enstrüman B'yi bir alt olarak referans alıyordu, oysa Enstrüman B'nin formülü beklenmedik bir şekilde tekrar Enstrüman A'yı referans alıyordu ve bu bir dairesel bağımlılık oluşturuyordu. Bu, A için computeIfAbsent çağrısının, değer başlangıç aşamasında aynı iş parçacığı içinde başka bir computeIfAbsent çağrısını tetiklemesine neden oldu.

İlk düşünebildiğimiz çözüm, hesaplama sırasında eşzamanlı değişikliğin herhangi bir olasılığını önlemek için önbellek erişimini kaba-kilitli synchronized bloklarına sarmak oldu. Bu yaklaşım deadlock riskini ortadan kaldırırdı, ancak tüm fiyatlandırma hesaplamalarını haritanın tamamı boyunca seri hale getirir ve etkinliği tek iş parçacıklı bir HashMap seviyesine düşürerek gerçek zamanlı ticaret için gerekli olan performans özelliklerini yok ederdi.

İkinci yaklaşım, harita işlemi öncesinde supplyAsync() ile oluşturulan önceden hesaplanmış CompletableFuture örnekleri kullanarak putIfAbsent ile çözüm bulmaktı. Bu, hesaplama sırasında kilitleri tutmaktan kaçınırdı ancak anahtar önbellekte zaten mevcut olsa bile pahalı fiyatlandırma hesaplamalarını hevesle başlatırdı, bu da son derece önemli CPU kaynaklarının gereksiz hesaplamalara harcanması anlamına gelirdi ve önbelleğin amacını boşa çıkartırdı.

Üçüncü çözümümüz, mevcut iş parçacığının çağrı yığını içerisinde "şu anda hesaplanan anahtarlar" içeren bir ThreadLocal<Set<String>> tutarak açık döngü tespiti uygulamak oldu. Herhangi bir computeIfAbsent işlemi başlatmadan önce, sistem bu kümede hedef anahtarı kontrol eder, dairesel referanslar için DomainException atar ve harita katmanına ulaşmadan önce hata üretirdi. Bu, ConcurrentHashMap'ın kilit serbest eşzamanlılığını korurken, geçersiz enstrüman hiyerarşileri hakkında anlamlı iş bağlamı sağladı.

Üçüncü çözümü seçtik çünkü bu, sadece belirtileri maskelemek yerine geçersiz dairesel finansal modellerin temel nedenini ele aldı. Bununla birlikte, ConcurrentHashMap'ın eşzamanlı performans özelliklerini tamamen korudu. Açık doğrulama, hangi belirli enstrümanların geçersiz dairesel bağımlılıklar oluşturduğunu gösteren net denetim izleri sağladı ve veri ekibine kaynak verisi hatalarını düzeltme olanağı sundu.

Implementasyon, üretim IllegalStateException çöküşlerini ortadan kaldırdı ve gereksiz fiyatlandırma hesaplamalarını yaklaşık %40 oranında azalttı, aynı zamanda ticaret platformu için alt milisaniye gecikme gereksinimlerini korudu. Açık döngü tespiti ayrıca, yanlış enstrüman hiyerarşilerinin kaynakta düzeltilmesini sağladı ve onları kod içinde sessizce işlemek yerine düzeltmeye zorladı.

Adayların Sıkça Atladığı Şeyler

ConcurrentHashMap neden null anahtarları ve değerleri reddederken HashMap bunlara izin veriyor?

ConcurrentHashMap, atomik işlemlerinde içsel bir gönderim değer olarak null'u kullanarak "anahtar yok" ile "hesaplama devam ediyor" arasında ayırt etme işlevini görür. computeIfAbsent ve merge gibi metodlar, atomik güncellemeler sırasında belirsiz bir şekilde yokluğu belirtmek için bu gönderimi kullanır ve ek aramalar gerektirmeden yarış koşullarını yaratmadan çalışır. get metodu hem kayıp anahtarlar hem de null'a eşit anahtarlar için null döndürdüğünden, null değerlerin izin verilmesi, eşzamanlı değişiklikler sırasında bir anahtarın gerçekten haritada var olup olmadığını belirlemeyi imkansız kılardı ve bileşen işlemlerin atomiklik garantilerini bozardı.

Java 8+'in bin seviyesi kilitlemesi, Java 7'nin segment temelli eşzamanlılığından nasıl farklıdır?

Java 7, her biri bağımsız bir ReentrantLock ile korunan 16 segmentten oluşan sabit bir dizi kullandı ve bu da mevcut donanıma bakılmaksızın maksimum yazma eşzamanlılığını 16 iş parçacığıyla sınırladı. Java 8+, segmentasyonu ortadan kaldırıp her bir hash bin düzeyinde ince kilitlemeyi tercih etti; bu, kilitlenmeyi önlemek amacıyla her kovadaki ilk düğümde synchronized bloklar kullanarak ve rekabetsiz yazma ve okuma için kilitsiz CAS işlemleri uyguladı. Bu mimari, binlere karşı birbirinden bağımsız olarak binlerce iş parçacığının eşzamanlı olarak yazmasına olanak tanırken, yeniden boyutlandırma işlemleri, okuma işlemlerinin göç sırasında devam etmesine olanak tanımak için volatile next-table işaretçileri ile ilerlemeli transfer kullanır.

computeIfAbsent'ı putIfAbsent'dan ne zaman tercih etmeliyim ve hangi kilitleme etkileri göz önünde bulundurulmalıdır?

computeIfAbsent, değer oluşturulmasının pahalı olduğu ve yalnızca anahtar yoksa atomik olarak gerçekleşmesi gerektiğinde esastır, çünkü yalnızca gerektiğinde çalışan bir Function kabul eder. Ancak, implementasyon, işlevin yürütülmesi süresince tüm hash binini kilitler, bu durum uzun süreli hesaplamaların o bin üzerinde anahtarların tüm erişimini seri hale getirebileceği anlamına gelir ve bu da performans darboğazına neden olabilir. putIfAbsent, çağrı öncesinde değerin önceden hesaplanmasını gerektirir, bu da değer oluşturma işleminin anahtarın varlığına bakılmaksızın gerçekleştiği anlamına gelir; fakat kilit, yalnızca kısa bir ekleme kontrolü süresince tutulur, bu da değer oluşturmanın ucuz veya idempotent olduğu durumlarda tercih edilir.