ClassLoader senkronizasyon tarihçesi, iş parçacığı güvenli sınıf yüklemesini zorunlu kılan, ancak başlangıçta sadece ClassLoader örneği monitörü üzerinde kaba düzeyde kilitleme sağlayan orijinal JVM spesifikasyonuna kadar uzanır. Java 7'den önce, her loadClass() çağrısı this üzerinde senkronize oluyordu ve bu, uygulama sunucuları gibi çoklu iş parçacıklı ortamlarda yaygın olan eşzamanlı sınıf yükleme sırasında küresel bir darboğaza yol açıyordu. Java 7, yükleyicilerin ince düzeyde kilitleme şemalarına katılmasına izin veren registerAsParallelCapable() API'sini tanıttı ve böylece verimliliği önemli ölçüde artırdı.
Temel sorun, ebeveyn-delegasyonun ve senkronize yöntemlerin özyinelemeli doğasından kaynaklanmaktadır. Bir çocuk ClassLoader loadClass() yöntemini geçersiz kıldığında ve kendi örneği üzerinde senkronize olduğunda, ebeveynin loadClass()'ını çağırırken o kilidi tutar ve ebeveyn kilidini alır. Karmaşık hiyerarşilerde—örneğin, çift yönlü paket ithalatına sahip OSGi paketleri veya dairesel görünürlük gereksinimlerine sahip eklenti mimarileri—bu, Thread-A'nın Child-A'yi tutup Parent için beklediği ve Thread-B'nin Parent'ı tutup Child-A için beklediği klasik kilit sırası döngüleri oluşturur.
Çözüm, senkronizasyonu yükleme işlemi üzerindeki yükleyici örneğinden, yüklenen belirli sınıf adlarına kaydırır. Bir ClassLoader'ın statik başlatıcısında registerAsParallelCapable() çağrıldığında, JVM paralel-capable yükleyicilerin bir ConcurrentHashMap'ini tutar ve sınıf adı için kilitler interned string üzerinde yükleyici nesnesi yerine. Bu, aynı yükleyici içindeki farklı iş parçacıkları tarafından farklı sınıfların eşzamanlı olarak yüklenmesine olanak tanır. Ancak, bu yeni bir tehlike getirir: Eğer Loader-A "X" sınıf adı üzerinde kilitlenir ve bir bağımlılık için Loader-B'ye devrederse, Loader-B aynı anda "Y" sınıf adı üzerinde kilitlenip "X" için geri Loader-A'ya devrederse, iş parçacıkları döngüsel bekleme durumuna girer—standart monitör analizine görünmeyen farklı sınıf adları üzerindeki farklı yükleyici ad alanlarında.
Yüksek frekanslı bir ticaret platformu, her algoritma jar'ının özel URLClassLoader çocukları aracılığıyla pazar verisi sınıfları için paylaşılan bir ana yükleyiciye referansla yüklendiği modüler bir strateji motoru uyguladı. Pazar açıkken, 500 iş parçacığı aynı anda stratejileri etkinleştirdi ve ana yükleyicinin monitöründe büyük bir çekişmeye neden oldu, bu da kaçırılan ticaret fırsatlarına yol açtı.
Çözüm 1: Varsayılan Senkronizasyon
Başlangıçta uygulama, miras alınan synchronized loadClass yöntemine dayanıyordu. happens-before tutarlılığını garanti ederken, bu yaklaşım tüm sınıf yüklemelerini tek bir monitör üzerinden seri hale getirdi. Performans profilleme, iş parçacıklarının %95'inin ana ClassLoader kilidi için beklerken engellendiğini ortaya koydu ve bu da kritik başlangıç penceresinde etkili verimliliği tek iş parçacıklı seviyelere indirdi.
Çözüm 2: Senkronize Olmayan Özel Yükleme
Geliştiriciler, değişmez jar içeriklerinin idempotent yüklemeyi garanti ettiğini varsayarak senkronizasyonu tamamen kaldırmaya çalıştılar. Bu, aynı yükleyici içinde tanımları özdeş olan birden fazla farklı Class nesnesinin bulunmasına neden oldu ve bu da yarışan iş parçacıkları tarafından yüklenen tekrar eden sınıf tanımları nedeniyle LinkageError ve karmaşık ClassCastException mesajlarıyla sonuçlandı. "Strateji Strateji'ye dönüştürülemez" şeklinde.
Çözüm 3: Paralel-Capable Kayıt
Ekip, bir özel ClassLoader alt sınıfında registerAsParallelCapable() implementasyonu yaptı ve paralel kilit mekanizmasını korumak için loadClass() yerine findClass() yöntemini geçersiz kıldı. Bu, ebeveyn-delegasyon zincirini korurken farklı sınıf adlarının eşzamanlı çözümlemesine izin verdi. Çözüm, kardeş yükleyiciler arasındaki dairesel paket bağımlılıklarını ortadan kaldıracak şekilde eklenti hiyerarşisinin yeniden yapılandırılmasını gerektirdi. Sonuç: başlangıç gecikmesi %120 saniyeden %8 saniyeye düştü ve üretim ticaretinde altı ay boyunca sıfır ClassLoader deadlock'u tespit edildi.
Neden loadClass() yerine findClass() geçersiz kılmak paralel-capable optimizasyonları sessizce devre dışı bırakıyor?
Paralel-capable mekanizma, JDK tarafından sağlanan loadClass(String name, boolean resolve) şablon yönteminde ince düzeyde kilitleme gömülü. Bir alt sınıf loadClass(String)'ı geçersiz kılınca, ClassLoader'ın iç parallelLockMap'i aracılığıyla sınıf adları üzerinde kilitleri edinen iç mantığı atlar. Alt sınıf, ya tekrarlanan sınıf tanımı yarışlarına neden olan senkronize edilmemiş erişime geri döner ya da this üzerinde manuel olarak senkronize olmak zorundadır ve bu da küresel darboğazı yeniden tanıtır. Doğru desen, önbellek kontrolleri ve ebeveyn delegasyonu için super.loadClass()'a yetki verir ve özel bayt dizisi-sınıf dönüşüm mantığını findClass()'a kısıtlar, bu da belirli ad için önceden tanımlanmış kilit bağlamında çalışır.
Paralel-capable ClassLoaders ile bile ServiceLoader desenleri deadlock'ları nasıl tetikleyebilir?
Ana ClassLoader'da çalışan ServiceLoader, Child-A'da bulunan bir hizmet uygulamasını başlatmayı denediğinde, implicit olarak Child-A.loadClass()'ı çağırır. Eğer o uygulama sınıfı, Parent'tan (örneğin bir logger) bir yardımcı sınıf yükleyen statik bir başlatmayı tetiklerse ve başka bir iş parçacığı farklı bir hizmet uygulamasını Child-A'dan yüklemek için Parent kilidi bekliyorsa, bir döngüsel bekleme oluşur. Thread-1, "Logger" için Parent'ın sınıf adı kilidini tutar ve Child-A'nın "ServiceImpl" için kilidini bekler. Thread-2, Child-A'nın "ServiceImpl" kilidini (ilk ServiceLoader çağrısı nedeniyle) tutar ve Parent'ın "Logger" kilidini bekler. Bu, başlatma sırasında yükleme sırasında dairesel bekleme zincirleri oluşturur ve standart iş parçacığı döküm analizörleri, ClassLoader örneği monitörlerini izledikleri için bunları tanımlamakta zorluk çekerler.
"defineClass penceresi" yarış durumu nedir ve neden paralel yetenek bunu engellemez?
Paralel yetenek, aynı sınıf adı için loadClass işlemlerinin seri hale getirilmesini garanti eder, ancak defineClass() kendisi farklı bir yerel işlem olarak yarış koşullarına karşı savunmasızdır. Eğer özel bir yükleyici, findLoadedClass kontrolü dışında dış önbellekleme veya bayt kodu dönüştürme uygularsa—örneğin, yükleme işlemini kesen bir Java ajanı aracılığıyla—iki iş parçacığı aynı "yüklenmedi" doğrulamasını geçebilir ve aynı ikili ad için defineClass(byte[], ...) çağrısında bulunabilir. İkinci iş parçacığı, LinkageError: attempted duplicate class definition hatasını alır. Bu, SystemDictionary kontrolü ve ekleme işleminin JVM seviyesinde atomik olmasından kaynaklanır, ancak özel ön kontrol ve defineClass çağrısı arasındaki pencere, kod tam olarak şablon yöntem desenini takip etmedikçe paralel-capable ad kilidi ile korunmaz ve ek yan etkiler veya ek senkronizasyon içermez.