Java Bellek Modeli (JMM), bir yapıcı tamamlandıktan sonra, final alanlara yapılan yazımların, nesne referansını okuyan herhangi bir iş parçacığına görünür hale geleceğini garanti eder, eğer bu referans inşaat sırasında dışarı sızmadıysa. Eğer this referansı erken sızarsa—başka bir iş parçacığına geçirilerek ya da yapıcı geri dönmeden önce statik bir koleksiyona kaydedilerek—yapıcının final alana yazışı ile diğer iş parçacığının okuması arasındaki happens-before ilişkisi kopar. Sonuç olarak, gözlemleyen iş parçacığı varsayılan değeri (sıfır, false veya null) görme olasılığına sahip olur ve bu da görünür değişmezliği bozar. Güvenli yayın, inşaat tamamlanana kadar inşa edilen nesneye ait hiçbir referansın dışarı sızmamasını gerektirir, böylece final alanlar üzerindeki dondurma işlemi, herhangi bir iş parçacığının referansı yüklemeden önce gerçekleşir.
Bunu yüksek frekanslı bir ticaret sisteminde, Service örneklerinin yapıcıları sırasında küresel bir ConcurrentHashMap içine kaydolduğu bir durumda gördük. Sınıf, yapıcı parametresinden başlatılan final long instrumentId tanımladı, ancak izleme iş parçacıkları, yaratımın hemen ardından kaydı sorgularken arada sırada sıfır okudu.
Önerilen çözümlerden biri, instrumentId alanını volatile olarak tanımlamaktı, böylece çekirdekler arasında anlık görünürlük sağlanabilecekti. Bu yaklaşım, atomiklik ve görünürlük sağladı ancak değişmezlik sözleşmesini yok saydı ve her okuma işleminde tam bir bellek bariyeri maliyeti yükledi, böylece inşaat sonrası değişmeyen bir değer için gereksiz yere verimliliği düşürdü ve nesnenin durumunu anlamayı karmaşıklaştırdı.
Bir başka öneri, kayıt işlemlerine tüm erişimleri korumak için synchronized bloklarla yapıcı mantığını kapsayacak şekilde senkronize etmeyi içeriyordu; kilitlemenin bellek önbelleklerini temizleyeceği teorize ediliyordu. Ancak bu, yarış koşullarını önlese de global kayıt kilidinde ağır bir rekabet getirdi, böylece eşzamanlı bir yapı seri bir darboğaz haline geldi ve piyasa verisi alımı için katı gecikme gereksinimlerini ihlal etti.
Biz, nesnenin oluşturulmasını kayıttan ayıran bir fabrika desenini tercih ettik. Yapıcı özel kalırken, fabrika metodu new Service(id) çağrısını tamamen yaptı ve yalnızca sonrasında tam oluşturulmuş referansı ConcurrentHashMap içine yayınladı. Bu, JMM’nin final alan dondurma semantiğinden yararlandı, senkronizasyon maliyeti olmadan, instrumentId’nin alınma anında hemen görünür olmasını sağladı.
Değişiklik, sıfır görünürlük anomalilerini ortadan kaldırdı ve hizmet arama için beklenen mikro-saniye ölçeğinde gecikmeyi geri kazandırdı, aynı zamanda değişmez tasarım amacını korudu.
final anahtar kelimesi, referansı basitçe bir thread-safe koleksiyon olan ConcurrentHashMap üzerinden yayımlarsam neden görünürlüğü garanti etmez?
ConcurrentHashMap'in put ve get işlemleri tarafından sağlanan happens-before ilişkisi, haritanın iç durum değişiklikleri arasında bir sıralama kurar, yapıcının yazmaları ile haritanın yayınlanması arasında değil. Eğer this inşaat sırasında dışarı sızarsa, final alana yazma bir iş parçacığında gerçekleşirken harita yayınlaması eşzamanlı olarak olur, bu da talimat yeniden sıralamalarını önlemek için gereken happens-before ilişkisinin eksikliğine yol açar. Bu nedenle, okuma iş parçacığı, yapıcının yazımlarının ana belleğe aktarılmadan önce harita üzerinden referansı gözlemleyebilir ve varsayılan değeri görebilir.
Bunu, nesnenin alanları yerine kayıt alanını volatile yaparak düzeltebilir miyim?
Kayıt referansını volatile yapmak yalnızca kayıt değişkeninin kendisinde yapılan değişikliklerin görünür olmasını garanti eder, içerdiği nesnelerin iç durumunu değil. Sorun, nesnenin alan yazımlarının zamanlaması ile referansın görünür hale gelmesi arasındadır; konteyner üzerinde volatile kullanmak, yapıcı ile nesnenin tüketeni arasındaki gerekli sıralamayı oluşturmaz. Hala kısmen oluşturulmuş örnekleri gözlemlemeye devam edersiniz.
Yapıcı içinde synchronized kullanmak, güvensiz yayını önler mi?
Yapıcı üzerinde synchronized kullanmak veya kaydı korumak, diğer iş parçacıklarının kritik bölgeye eşzamanlı olarak girmesini önler, ancak kayıt yöntemi, referansı o kilidin dışında çalışan bir iş parçacığına sızdırırsa, this referansının dışarı sızmasını engellemez. JMM, final alan semantiği için yapıcının tamamlanmadan önce nesneye ait hiçbir referansın dışarı sızmamasını gerektirir; uygun yayın sıralaması olmadan senkronizasyon o garantiyi yeniden canlandıramaz.