Soruya verilecek cevap:
Soru geçmişi.
Java 5'te tanıtılan ReentrantReadWriteLock, birden fazla eşzamanlı okuyucuya izin vererek tekli mutexlere göre önemli bir eşzamanlılık geliştirmesi sağladı. Ancak, tasarımı açıkça kilit yükseltme işlemlerini yasaklıyor - yani okuma kilidini tutarken yazma kilidi almak - çünkü uygulama, her bir iş parçacığı için okuma tutma sayımlarını takip ediyor. Okuma kilidini tutan bir iş parçacığı yazma kilidini almaya çalıştığında, kendisini ölü kilit durumuna sokar: yazma kilidi, özel sahiplik gerektirir ve herhangi bir okuma kilidi (iş parçacığının kendisi dahil) tutulmaya devam edilirse bu sağlanamaz. Java 8'de tanıtılan StampedLock, iyimser okuma damgaları ile bu sınırlılığı giderdi; bu damgalar, okuma aşaması sırasında lock sahipliği gerektirmez, atomik doğrulama ve dönüştürme mekanizmaları ile birleştirilmiştir.
Problem.
Temel tehlike, kilit edinim anlamsal asimetrisi nedeniyle ortaya çıkar. ReentrantReadWriteLock'ta, yükseltme işlemi, yazma kilidini almadan önce okuma kilidini serbest bırakmayı gerektirir; bu da, diğer iş parçacıklarının serbest bırakma ile yeniden edinim arasında yazma kilidini alabileceği veya durumu değiştirebileceği zayıf bir pencere oluşturarak geliştiricilerin karmaşık çift kontrol kilit desenleri veya yeniden denemeler uygulamak zorunda kalmasına neden olur, bu da kod karmaşıklığını ve gecikmesini artırır. Dahası, bir geliştirici yanlışlıkla doğrudan yükseltme (readLock() kilidi tutarken writeLock().lock()) yapmaya çalışırsa, iş parçacığı, kendisinin okuma iznini serbest bırakmasını beklerken geri alınamaz bir ölü kilit durumuna girer.
Çözüm.
StampedLock, tryOptimisticRead() ile bu tehlikeyi ortadan kaldırır; bu, herhangi bir kilidi almak veya okur sayısını artırmadan uzun bir damga döndürür. İş parçacığı okuma işlemlerini gerçekleştirir ve ardından validate(stamp) çağrısında bulunur; damga geçerli kalırsa (araya herhangi bir yazma işlemi girmediyse), okuma bloke olmadan tutarlıdır. İş parçacığı yazma işlemine ihtiyaç duyduğunda tryConvertToWriteLock(stamp)'i denemek için çalışır; bu, damgayı atomik olarak doğrular ve durum, iyimser okuma başladığından bu yana değişmediği sürece yazma kilidini alır. Bu yaklaşım, iş parçacığının geçiş sırasında çelişkili bir okuma kilidi tutmasını önler ve yükseltmeyi durum tutarlılığına bağlı hale getirerek serbest bırakma ve yeniden edinim stratejilerinin yarış penceresini ortadan kaldırır.
Kod örneği.
import java.util.concurrent.locks.StampedLock; public class AtomicUpgradeCache { private final StampedLock lock = new StampedLock(); private int value = 0; public void conditionalUpdate(int threshold, int newValue) { long stamp = lock.tryOptimisticRead(); int current = value; // İşlemden önce doğrula if (!lock.validate(stamp)) { stamp = lock.readLock(); try { current = value; } finally { lock.unlockRead(stamp); } } if (current < threshold) { // Atomik yükseltme denemesi stamp = lock.tryConvertToWriteLock(stamp); if (stamp == 0L) { // Dönüşüm başarısız oldu, temiz bir yazma kilidi edin stamp = lock.writeLock(); } try { // Özel kilit altında durumu tekrar kontrol et if (value < threshold) { value = newValue; } } finally { lock.unlock(stamp); } } } }
Hayattan bir durum
Problem tanımı.
Yüksek frekanslı bir ticaret platformu, canlı piyasa derinliğini temsil eden bir bellek içi emir defteri önbelleği sürdürmekteydi; bu, yüzlerce iş parçacığından saniyede yaklaşık 50,000 okuma gerektiriyordu, ancak fiyat değişiklikleri geldiğinde yalnızca ara sıra güncellemeler gerektiriyordu. Başlangıçta synchronized bloklar kullanıldı ve bu, iş parçacıklarının monitör üzerinde rekabet ettiği durumlarda, piyasa dalgalanması sırasında felaket düzeyde gecikme zirvelerine neden oldu; okuma gecikmesi bazen 500 milisaniyeyi aşmaktaydı. Mühendislik ekibi, okuma tarafındaki tüm rekabeti tamamen ortadan kaldırırken, fiyat güncellemelerinin piyasa koşullarını atomik olarak doğrulayabileceğinden ve gözlemden değişime geçerken ölü kilit durumları yaşamadan defteri değiştirip değiştirebileceğinden emin olmalıydı.
Değerlendirilen farklı çözümler.
Çözüm 1: Serbest bırak ve yeniden edin ile ReentrantReadWriteLock.
Bu yaklaşım, piyasa koşullarını incelemek için okuma kilidini edinmeyi, serbest bırakmayı ve güncelleme gerekli olduğunda hemen yazma kilidini edinmeyi denemeyi içeriyordu. Bu yöntem ölü kilidi önlese de, önemli bir yarış durumu getirdi: okuma kilidinin serbest bırakılması ile yazma kilidinin edinilmesi arasında, rekabet eden iş parçacıkları aynı eski durumu gözlemleyebilir ve gereksiz veritabanı sorguları başlatabilir veya API çağrıları değiş tokuş edebilirdi; bu da gürültü çetesi davranışına ve israf edilen hesaplama kaynaklarına yol açtı. Ayrıca, okuma ve yazma modları arasında sürekli bağlam değiştirme, yüksek hacimli ticaret dönemlerinde ölçülebilir aşırılık ekledi.
Çözüm 2: Değiştirilemez anlık görüntülerle volatilde referanslar.
Bu çözüm, kilitleri tamamen terk ederek, emir defterini volatilde bir alan tarafından referans edilen değiştirilemez bir veri yapısı olarak sürdürmeyi tercih etti. Okuyucular, konsisten bir anlık görüntü elde etmek için volatili kolayca kullandılar; yazarlar ise tamamen yeni emir defteri kopyaları oluşturdu ve referans üzerinde atomik karşılaştır ve ayar işlemleri gerçekleştirdi. Bu, okuma rekabetini tamamen ortadan kaldırdı ve mükemmel okuma performansı sağladı. Ancak, her küçük fiyat güncellemesi, tüm emir defterinin kopyalanmasını gerektirdiği için büyük bir tahsis baskısı üretir ve bu, sık sık genç nesil çöp toplama duraklamalarına neden olarak uygulamanın 10 milisaniye gecikme SLA'larını ihlal etti.
Çözüm 3: İyimser okumalar ve koşullu dönüşüm ile StampedLock.
Seçilen çözüm, StampedLock'ı kullanarak sıcak yolda iyimser okuma erişimini sağladı: iş parçacıkları, tryOptimisticRead()'i kullanarak emir defterinin durumunu ilham verici okuma işlemleri yapacak, damgayı doğrulayacak ve yalnızca eşzamanlı bir yazma işlemi olmamışsa devam edeceklerdi. Nadir yazma işlemleri için, sistem, iyimser damgayı doğrudan yazma kilidine dönüştürmeyi denedi ve böylece gözlemlenen durumun geçerli kaldığını atomik olarak doğruladı ve yalnızca geçerli olduğunda özel erişim sağladı. Dönüşüm başarısız olursa, sistem, geleneksel yeniden deneme mantığı ile açık yazma kilidi edinmeye geri döner. Bu yaklaşım, okuma başına neredeyse sıfır fazla yük sağlarken (ham volatili erişimle benzer) ReentrantReadWriteLock yükseltmelerinde meydana gelen ölü kilit risklerini önlemekteydi.
Hangi çözüm seçildi (ve neden).
Ekip, okuyucu yığınını (iyimser okumalar işlem sayısına göre doğrusal olarak ölçeklenir) ve koşullu güncellemeler için atomik güvenlik gereksinimlerini dengede tutma yeteneğinden dolayı Çözüm 3'ü seçti. Çözüm 1 ile yükseltme sırasında okuma serbest bırakma ile yazma edinme arasındaki yarış penceresini ortadan kaldırmıştı; Çözüm 2 ile her küçük fiyat düzeltmesi için tam yapı kopyası gerektiren bellek tahsis baskısını ortadan kaldırmıştı. Doğrulama ve dönüştürme atomik olarak sağlandığı için fiyat güncellemelerinin yalnızca piyasa durumu karar kriterlerine tam olarak uyduğunda meydana gelmesi sağlanmış olup, önceki prototiplerin iki tutarlılık ihlalini önlemiştir.
Sonuç.
Uygulama uygulandıktan sonra, 50,000 eşzamanlı okuma işlemi, p99.9 gecikmeleri 15 mikro saniyenin altında tutularak sürdürüldü ve bu, önceki synchronized yaklaşımına göre 30 kat iyileşme sağladı. Simüle edilmiş piyasa volatiliteleri ile 1,000 eşzamanlı fiyat güncellemeleri sağlandığında, sistem sıfır ölü kilit olayı yaşadı ve çöp toplama duraklamaları 2 milisaniyenin altında kaldı. StampedLock uygulaması, üretim ticareti süresince birinci bir eşzamanlılık ilişkili olay veya veri yarışı yaşamadan altı ay boyunca başarılı bir şekilde kullanıldı ve yüksek frekanslı okuma senaryoları için iyimser kilitlemenin mimari kararını doğruladı.
Adayların sıklıkla kaçırdığı noktalar
Neden StampedLock yeniden giriş desteği sağlamaz ve bir iş parçacığı aynı kilidi yinelemeli olarak almaya çalıştığında hangi felaket durumları meydana gelir?
StampedLock, iç durum takibini en aza indirmek ve verimliliği maksimize etmek için açıkça yeni giriş olmayan bir kilit olarak tasarlanmıştır. ReentrantReadWriteLock'tan farklı olarak, bu kilit, hangi iş parçacığının sahip olduğunu değil, yalnızca herhangi bir iş parçacığının erişimi tuttuğunu takip eder. Sonuç olarak, bir okuma kilidini tutan bir iş parçacığı, aynı StampedLock örneğinde başka bir okuma kilidi (veya yazma kilidi) almak için ne zaman çalışırsa, hemen ölü kilit durumuna girer: edinme çağrısı, tüm mevcut kilitlerin serbest bırakılmasını beklerken bloke olur, ancak engellenen iş parçacığı, bu kilitlerden birini tutmaya devam eder ve bu da çözümsüz bir dairesel bağımlılık oluşturur. Geliştiricilerin iç API'leri üzerinde bir daha çok durumsal kilit edinimi girişimine dayanmak yerine, geçerli damgayı bir yöntem parametresi olarak iletmek üzere kodu yeniden yapılandırmaları gerekebilir; bu genellikle iç API'lerin önemli mimari değişiklikler gerektirdiği anlamına gelir.
StampedLock'un iyimser okuma modu ile kötümser okuma kilidinin bellek görünürlük anlamsal özellikleri nasıl farklıdır ve neden validate() yalnızca uygun bir şekilde gerçekleşmeden tutarlılığı sağlamak için yeterli değildir?**
tryOptimisticRead() aracılığıyla yapılan iyimser okuma, kendisi başına hiçbir gerçekleşme garantisi sağlamaz; bu, yalnızca bir sürüm damgası yakalar ve bellek engellerini yerleştirmeden veya talimat yeniden sıralamasını önlemeden önce. İyimser aşama sırasında gözlemlenen veriler, Java JVM bellek modeli iyimser okumaları kilitlenmemiş değişken erişimleri olarak ele aldığından, önbellek hatalı veya kısmi olarak oluşturulmuş nesneleri yansıtabilir. Yalnızca validate(stamp) geçerli döndüğünde, iyimser okumaya başlandığı andan itibaren bir yazma kilidinin alınmadığını gösterir; böylece, en son yazma kilidi serbest bırakma ile ilgili gerekli gerçekleşme kenarını oluşturur. Ancak, adaylar genellikle validate()'ın yalnızca kilit durumunu garanti ettiğini unutur; korunan veri yapısının dahili tutarlılığını değil: korunan veri, değiştirilebilir nesnelere olan volatili referanslar içeriyorsa, iyimser okuma, başka bir iş parçacığı tarafından hala başlatılmakta olan bir nesneye referansı gözlemleyebilir (güvensiz yayılma). Bu nedenle, iyimser okumalar, kilidin bellek anlamsal özelliklerinden bağımsız olarak güvenli yayılma sağlamak için korunan durumun tamamen volatilde referanslar veya değişmez nesneler içermesini gerektirir.
StampedLock ile Sanal İş Parçacıkları (Proje Loom) arasındaki temel uyumsuzluk nedir ve bu neden modern yüksek eşzamanlılık uygulamalarında StampedLock'tan kaçınılması gerektiğini gerektirir?**
StampedLock uygulamaları, bir sanal iş parçacığı kilidi tutarken bloke olduğunda altyapı Platform Thread (taşıyıcı iş parçacığı) işlemlerini kullanarak sabitleyerek, sanal iş parçacıklarının ardından diğer iş parçacıklarını bekler. Bir sanal iş parçacığı rekabetçi StampedLock'ı (okuma veya yazma) edinmeye çalıştığında, JVM, sanal iş parçacığını taşımaya uyduramaz çünkü kilitlerin içerikleri, sanal iş parçacığı için henüz uyarlanmış yerel senkronizasyon işlemlerini kullanmaz. Bu sabitleme, sanal iş parçacıklarının çekirdek taşma vaadi ile bağdaşmaz; çünkü binlerce sanal iş parçacığını az sayıda platform iş parçacığı üzerine çoklayarak dağıtır. Birden çok sanal iş parçacığı, StampedLock rekabetinde eş zamanlı olarak bloke olursa, tüm taşıyıcı iş parçacığı havuzunu tekelleştirir ve uygulamanın dondurulmasına neden olurken milyonlarca sanal iş parçacığı teorik olarak kullanılabilir kalmaya devam eder. Buna karşılık, ReentrantLock ve Semaphore sanal iş parçacıklarından çağrıldığında, sabitlemeyi önlemek için başarısızlaşmış veya özel yer bırakma mekanizmaları kullanarak yeniden yapılandırılmıştır. Bu nedenle, VirtualThread yürütücülerini kullanan modern uygulamaların, taşıyıcı iş parçacığı açlığını önlemek için StampedLock yerine ReentrantLock veya eşzamanlı veri yapılarıyla değiştirmeleri gerekmektedir.