Tarihçe: Java 8'den önce, eşzamanlı toplama işlemleri AtomicLong'a dayanıyordu; bu, bir hafıza konumunun aşırı önbellek satırı geçersiz kılma nedeniyle işlemci çekirdekleri arasında ölçeklenebilirlik darboğazı haline gelmesiyle sonuçlandı. LongAdder, birden çok dolgu hücresi arasında yazma işlemlerini dinamik olarak bölme tekniği ile bunu aşmak için java.util.concurrent.atomic paketinin bir parçası olarak tanıtıldı ve Striped64 algoritmasından esinlenildi.
Sorun: Çok sayıda iş parçacığı aynı anda paylaşılan AtomicLong üzerinde CAS işlemleri yapmaya çalıştığında, her başarısızlık bir önbellek tutarlılığı yayını başlatır; bu, bellek trafiğini sıralar ve çekirdek sayısıyla birlikte verimliliği üssel olarak azaltır. Bu olgu, önbellek satırı sıçraması olarak bilinir ve bunu takiben doğrusal ölçeklenebilirliği engeller, aksi takdirde tamamen paralel görevlerde bile.
Çözüm: LongAdder, öncelikle CAS kullanarak tek bir base alanında güncellemeler yapmayı dener; yalnızca rekabet tespit edildiğinde - özellikle bir iş parçacığının olasılıklı bir sorgulama dizisi (tipik olarak Striped64 içinde bir çakışma sayacı ve iş parçacığına özel karma ile uygulanır) sonrası base kilidini elde edemediğinde - @Contended etiketi ile işaretlenmiş Cell nesnelerinin bir dizisini tembel bir şekilde ayırır. Her iş parçacığı daha sonra farklı bir hücreye karma yaparak, izole önbellek satırlarında rekabetsiz eklemeler gerçekleştirirken, sum() metodu, yalnızca tutarlı bir anlık görüntünün gerektiği zamanlarda bu değerleri tembel bir şekilde toplar.
Bir yüksek frekanslı ticaret platformu, 64 çekirdekli dağıtımda sipariş akış hızını doğrulamak için küresel bir sayaca ihtiyaç duydu ve bu başlangıçta AtomicLong kullanılarak uygulandı. Piyasa dalgalanması zirveleri sırasında, sistem, 99. yüzde tepki süresinin on katına çıktığı doğrusal olmayan bir gecikme düşüşü gösterdi; profilleme, CPU döngülerinin %40'ının, sayaçın tek hafıza adresi için rekabet eden önbellek uyum protokollerine harcandığını ortaya koydu.
Mühendislik ekibi üç mimari çözümü değerlendirdi. İlk olarak, her iş parçacığının ConcurrentHashMap içindeki bağımsız bir AtomicLong tuttuğu bir manuel iş parçacığı yerel sayaç haritasını değerlendirdiler; bu, rekabeti ortadan kaldırdı, ancak her iş parçacığı için önemli bellek yükü ve karmaşık yaşam döngüsü yönetimi getirdi, thread havuzunun yeniden boyutlandırılması sırasında bellek sızıntısı riski oluşturdu. İkincisi, Thread.currentThread().getId() % 64 ile dizinlenmiş 64 adet AtomicLong nesnesinin bir dizisini kullanan özel bir parçalama stratejisi prototiplediler; bu önbellek trafiğini azalttı ancak iş parçacığı havuzları kimlikleri yeniden kullandığında dengesiz dağılım yaşadı ve trafik büyüdüğünde dizi boyutlandırmasının manual olarak ele alınmasını gerektirdi ve kırılgan bir bakım yükü ekledi. Üçüncüsü, @Contended dolgu ile otomatik şeritleme sunan LongAdder'a geçiş yapmayı değerlendirdiler; ancak bu, okuma işlemlerinin kesin değerler yerine zayıf tutarlılıkta tahmini değerler döneceği karşılığında bir takas sundu.
Ekip nihayet LongAdder'ı seçti çünkü iş gereksinimi izleme panelleri için hafif şekilde bayat okuma değerlerine tolerans gösterdi, bu arada yazma yoğun doğrulama yolu maksimum verimlilik talep etti. Otomatik hücre genişletme kestirimi, düşük trafik dönemlerinde nesnenin hafif kalmasını (tek bir temel alan) sağlarken, yüksek rekabet, dolgu hücreleri arasında şeffaf ölçeklenmeyi tetikledi. Dağıtım sonrası gecikmeler stabil hale geldi ve verimlilik, önbellek geçersiz kılma trafiği farklı bellek bölgelerine dağıldıkça 64 çekirdeğe kadar doğrusal olarak ölçeklendi.
Soru: LongAdder.sum()’ın sık çekilmesi, şeritleme ile sağlanan performans avantajlarını nasıl bertaraf edebilir ve bu metod hangi tutarlılık garantilerini sağlar?
Cevap: sum() metodu toplamı hesaplamak için base alanına ve dizideki her aktif Cell'e erişmek zorundadır; bu, tüm katılımcı çekirdekler arasında önbellek uyum senkronizasyonunu tetikleyen bellek engelleri gerektirir; dolayısıyla sürekli okuma açısından ağır iş yükleri, şeritli yazmaları etkin bir şekilde serileştirir ve LongAdder'ın kaçındığı rekabeti yeniden getirir. Ayrıca, sum() sadece zayıf tutarlılık sunar; çağrıldığı anda kesin bir değer döndürür, ancak eşzamanlı güncellemeler ile atomiklik garantisi yoktur, yani sonuç bazı iş parçacıklarının artışlarının görünürken diğerlerinin görünmediği geçici bir durumu temsil edebilir.
Soru: LongAdder içindeki @Contended anotasyonu, sahte paylaşıma karşı nasıl koruma sağlar ve bu dolgu davranışını yöneten JVM bayrağı nedir?
Cevap: @Contended, HotSpot derleyicisine her Cell içinde value alanının etrafına 128 bayt (ya da -XX:ContendedPaddingWidth ile belirtilen değer) dolgu eklemesi talimatını verir; böylece bitişik dizi elemanları, nesne yerleşim optimizasyonlarından bağımsız olarak farklı önbellek satırlarında bulunur. Bu dolgu olmadan, ardışık hücreler 64 baytlık bir önbellek satırını paylaşır ve bir hücredeki yazmaların diğer çekirdeklerdeki komşuların önbelleğe alınmış kopyalarını geçersiz kılmasına yol açar ve yeniden önbellek sıçramalarını getirir. Adaylar, bu anotasyonun, -XX:-RestrictContended açıkça devre dışı bırakılmadıkça kullanıcı kodu gerektirecek şekilde JDK iç sınıfları için ayrıldığını sık sık gözden kaçırır.
Soru: LongAdder hangi belirli koşullar altında AtomicLong'dan daha kötü bir performans sergileyebilir ve longValue() uygulaması bu riski nasıl etkiler?
Cevap: LongAdder, rekabetsiz tek iş parçacıklı yürütme sırasında Cell dizisi ve karma hesaplama mantığı için tahsisat yükü de taşır; bu, düşük rekabet senaryolarında veya yalnızca bir iş parçacığı tarafından güncellenen sayacın üzerinde AtomicLong'ın daha üstün olmasını sağlar. Ayrıca, longValue() doğrudan sum()‘a delegedir; bu durum, sayacın değerinin sürekli kontrol edildiği herhangi bir kod yolunu - örneğin, bir döngü kilidi veya geri besleme algoritması - küresel toplama için tekrar tekrar zorlayarak tüm önbellek satırlarını senkronize eder ve etkin bir şekilde şeritli yapıyı rekabet eden bir singleton haline getirir ve ölçeklenebilirliği yok eder.