JavaProgramlamaKıdemli Java Backend Geliştirici

Sonrasında Java Bellek Modeli revizyonları neden çift kontrol kilitleme kalıbını güvence altına almak için volatile semantiği zorunlu kıldı?

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

Sorunun cevabı

Tarih

Java 5'ten önce Java Bellek Modeli (JMM), birçok popüler eşzamanlılık kalıbının güvensiz hale gelmesine yol açan zayıf bellek görünürlük garantileri ile sıkıntı yaşıyordu. Çift Kontrol Kilitleme deseni, tembel başlatma için önerilen bir performans optimizasyonu olarak 1990'ların sonlarında ortaya çıktı, ancak talimatların yeniden sıralanması ile ilgili ölümcül bir hataya sahipti. JSR-133, 2004 yılında, bu tür görünürlük sorunlarını tam senkronizasyon yükü olmadan çözmek için volatile anahtar kelimesinin anlamını yeniden tanımladı.

Problem

volatile olmadan, JVM ve temel CPU mimarileri, bir değişkene referans atamasının, yapıcının yürütülmesinin tamamlanmasından önce gerçekleşmesine izin verecek şekilde talimatları yeniden sıraya koyma yetkisine sahiptir. Bu, başka bir iş parçacığının, alanları varsayılan veya başlatılmamış değerler içeren bir nesne için boş olmayan bir referans gözlemleyebileceği bir pencere oluşturur ve bu da tahmin edilemez davranışa veya NullPointerException'a yol açar. Eşzamanlılık tehlikesi özellikle sinsi olduğu için, sadece belirli zamanlama koşulları ve donanım bellek modelleri altında kendini gösterir, bu da test sırasında yeniden üretmeyi zorlaştırır.

Çözüm

Örnek alanı volatile olarak bildirmek, yapıcıdaki yazma eylemi ile diğer iş parçacıkları tarafından sonraki okumalar arasında bir meydana gelme ilişkisi kuran bir bellek engeli ekler. Bu, derleyici ve işlemcinin yapılandırıcıda önceden yapılan yazmalar ile volatile alanındaki yazmayı yeniden sıralamasını engeller ve nesnenin referansının görünür hale gelmeden önce tamamen inşa edilmesini garanti eder. Bu desen, iş parçacıklarının başlangıçtan sonra kilitlemeden referansı kontrol etmelerine olanak tanır ve hem iş parçacığı güvenliği hem de yüksek performans sağlar.

public class ConnectionPool { private static volatile ConnectionPool instance; private ConnectionPool() { // Ağır başlatma } public static ConnectionPool getInstance() { if (instance == null) { synchronized (ConnectionPool.class) { if (instance == null) { instance = new ConnectionPool(); } } } return instance; } }

Hayattan bir durum

Bir yüksek hacimli mikro hizmet, bir PostgreSQL kümesine JDBC bağlantılarını yönetmek için tekil bir ConnectionPool gerektiriyordu. Trafik zirvesinde, servisin ilk başladığında binlerce iş parçacığı aynı anda getInstance() yöntemini çağırdı ve bu, kilit içeriğini en aza indiren bir iş parçacığı güvenli başlatma stratejisini zorunlu kıldı. Başlatma dizisi, TCP soketlerini kurma, doğrudan bayt tamponları ayırma ve şema doğrulama sorgularını yürütmeyi kapsıyordu ve bu da otomatik ölçekleme senaryolarında hevesli başlatmayı aşırı maliyetli hale getiriyordu.

Hevesli Başlatma

Hevesli Başlatma, havuzu bir statik başlatıcı blokta oluşturmayı içeriyordu. Bu yaklaşım, sınıf yükleme mekanikleri aracılığıyla iş parçacığı güvenliğini garanti etti ve synchronized bloklara tamamen olan ihtiyacı ortadan kaldırdı. Ancak, bağlantı kurma üç saniye TCP el sıkışması ve kimlik doğrulama alışverişi gerektiriyordu ve bu da otomatik ölçekleme etkinlikleri sırasında soğuk başlatma süreleri için hizmet anlaşmasını ihlal ediyordu.

Senkronize Yöntem

Senkronize Yöntem, getInstance() yöntemini synchronized anahtar kelimesi ile sarmaladı. Bu, tüm erişimi seri hale getirerek yarış koşulunu düzeltmesine rağmen, yük altında ciddi bir performans düşüşü getirdi. Profilleme, başlatmadan sonra iş parçacıklarının sabit olarak inşa edilmiş havuzun doğasına rağmen monitör kilidini edinmek için gereksiz döngüler harcadığını ve her çağrıda yaklaşık 18 milisaniye gecikme eklediğini ortaya koydu.

Volatile ile Çift Kontrol Kilitleme

Volatile ile Çift Kontrol Kilitleme en iyi yaklaşım olarak seçildi. Bu çözüm, null olup olmadığını kontrol etmek için senkronize edilmemiş hızlı bir yol kullanıyordu, ardından kritik bölüm için bir synchronized bloğu, birden fazla örneklendirmeyi önlemek için içeride ikinci bir null kontrolü ile birlikte geldi. Volatile değişkeni, tamamen başlatılmış havuz durumunun, yayımlandıktan hemen sonra tüm CPU çekirdekleri tarafından görünür olmasını sağladı ve girişte sıfır kilit yükü ile tembel başlatmayı dengeledi.

Seçilen çözüm, kilitlenme olmadan başarılı bir tembel başlatma ile sonuçlandı ve hizmetin ilk havuz yaratımından sonra milisaniye altı yanıt süreleri ile saniyede 50,000 isteği işlemesine olanak sağladı. Uygulama, başlatma sırasında yarış koşullarını ortadan kaldırırken, sürdürülebilir işletim sırasında kilitsiz erişimi korudu ve daha önce yüksek eşzamanlılık senaryolarında meydana gelen NullPointerException örneklerini önledi. İzleme, JVM'nin, tekil oluşturulduktan sonra açık bir senkronizasyon olmadan tüm 64 çekirdek arasında bellek görünürlüğünü doğru bir şekilde yönettiğini doğruladı.

Adayların sıklıkla kaçırdığı şeyler

Çift kontrol kilitleme kalıbı, neden tek bir senkronize kontrol yerine iki ayrı null kontrolü gerektirir?

İlk kontrol, örneğin zaten mevcut olduğu en yaygın durumda hızlı, kilitsiz bir yol sağlamak için synchronized blok dışındadır. Synchronized blok içindeki ikinci kontrol, birden fazla iş parçacığının örneğin hala başlatılmadığı durumlarda aynı anda ilk null kontrolünü geçebileceğinden esastır. Bu ikinci doğrulama olmadan, her iş parçacığı sırasıyla kilidi edinir ve ayrı örnekler oluşturur, bu da tekil mülkiyetini ihlal eder. İç kontrol, kritik bölgeye giren ilk iş parçacığının inşayı gerçekleştirmesini sağlarken, sonraki iş parçacıkları zaten başlatılmış örneği keşfeder ve oluşturmayı atlar.

Java Bellek Modeli, volatile yazımının görünürlük garantileri ile senkronize blok çıkışı arasındaki farkı nasıl ayırır?

Her iki yapı da meydana gelme ilişkileri kurarken, farklı ayrıntı düzeylerinde ve performans özelliklerinde çalışırlar. Synchronized blok çıkışı, iş parçacığının çalışma belleğindeki tüm değiştirilmiş değişkenleri ana belleğe boşaltır ve küresel bir bellek engeli görevi görür. Buna karşın, volatile yazımı, yalnızca o belirli değişkenin çevresindeki talimatlarla yeniden sıralanmasını engeller ve yazımın hemen görünür olmasını garanti eder. Java 5'ten önce, volatile bu garantilerden yoksundu, bu da güvenli yayın için yetersiz bırakıyordu; modern JMM, volatile yazımlarını, C++ serbest işlemleri ve okumaları alım işlemleri ile benzer şekilde ele alır, hedefe yönelik görünürlük sağlar ve monitör kilitlemenin tam maliyetini yüklemez.

Değişmez nesneler, çift kontrol kilitleme kalıbındaki volatile ihtiyacını ortadan kaldırabilir mi?

Hayır, çünkü final alanlar yalnızca yapıcı tamamlandığında değişmezliği garanti eder; referansın kendisinin yayılma sırasında değil. Volatile olmadan, talimatların yeniden sıralanması, referansın ana belleğe yazılmasına yol açabilirken, yapıcı tamamlandıkça başka bir iş parçacığının, kısmen inşa edilmiş bir nesneye boş olmayan bir referans gözlemlemesine izin verebilir. Final alanları, değerlerin inşadan sonra değişmeyeceğini garanti etse de, referansın erken sızması durumunda varsayılan veya başlatılmamış değerlerin görünürlüğünü engelleyemez. Güvenli yayın, inşa ile görünürlük arasında meydana gelme ilişkisini sağlamak için ya volatile ya da synchronized gerektirir; nesnenin içsel değişmezliği ne olursa olsun.