JavaProgramlamaKıdemli Java Geliştirici

Platform thread'lerden Sanal Thread'lere geçişte ana tehlike, taşıyıcı thread'lerin sabitlenmesini tetikleyen monitör çatışması ve senkronize bloklarla ilgilidir. Bu tehlike nerede yatar?

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

Soruya Cevap

Sanal thread'ler Project Loom'da ForkJoinPool'dan çekilen taşıyıcı thread'ler üstünde montelenmiş devamlar olarak çalışmaktadır. Bir sanal thread synchronized blokla karşılaştığında veya yerel kod çalıştırdığında, temel taşıyıcı thread'ini sabitler ve bu da planlayıcının sanal thread'i bloklama I/O işlemleri sırasında çıkaramasına neden olur. Bu, etkili bir şekilde eşzamanlılık seviyesini taşıyıcı havuzun boyutuna (genellikle CPU çekirdek sayısına eşit) düşürür ve bu da rekabet eden sanal thread'lerin sabit taşıyıcı havuzu monopolize etmesiyle yük altında verim kaybına yol açabilir.

Hayattan Bir Durum

Bir finans hizmetleri firması, eski sipariş işleme geçidini geleneksel Tomcat thread-başına-istek modeli (500 platform thread ile sınırlı) yerine sanal thread'lerle Jetty'ye taşımıştır ve 50.000 eşzamanlı WebSocket bağlantısını yönetmeyi beklemiştir. Dağıtımın hemen ardından, sanal thread'lerin benimsenmesine rağmen, gecikme birkaç saniyeye fırlamış ve verim yalnızca 800 TPS'de platoya ulaşmıştır. Thread dökümleri, 24 taşıyıcı thread'in BLOCKED durumunda synchronized bloklar içinde takılı kaldığını ve I/O için kuyrukta bekleyen binlerce sanal thread'in ilerleyemediğini göstermiştir.

İlk olarak düşünülen çözüm, ForkJoinPool paralelliğini -Djdk.virtualThreadScheduler.parallelism ile 1000'e çıkararak artırmaktı. Bu, sabitlenmiş yükü emmek için daha fazla taşıyıcı thread sağlayacak ve etkili bir şekilde büyük bir platform thread havuz davranışına geri dönecekti. Ancak bu yaklaşım, esas mimari hatayı gizlerken aşırı OS kaynakları tüketmiş ve sanal thread sanallaşmasının vaat ettiği bellek verimliliği faydalarını ortadan kaldırmıştır.

İkinci çözüm, paylaşılan hız sınırlayıcı önbelleklerini koruyan tüm synchronized blokların yerine ReentrantLock kullanarak yeniden yapılandırılmasıydı. İçsel monitörlerden farklı olarak, ReentrantLock sanal thread planlayıcısı ile entegre olduğundan, taşıyıcıyı sabitlemeden rekabet veya bloklama işlemleri sırasında çıkarılmasına izin verir. Bu yaklaşım, sanal thread'lerin hafif doğasını korur ancak bir sistematik kod denetimi ve kilit kesintisi anlamlarının dikkatli bir şekilde ele alınmasını gerektirir.

Üçüncü çözüm, eşzamanlı hash harita önbelleklerini, tamamen kilitsiz veri yapıları olan ConcurrentHashMap hesaplama yöntemleri veya iyimser okumalar için StampedLock ile değiştirmeyi önerdi. Bu, birçok okuma yolu için bloke olmayı ortadan kaldırsa da, karışık dış kaynaklara (örneğin, veritabanı bağlantısı almak) özel erişimi gerektiren senaryolara yanıt veremez çünkü bunlar doğal olarak karşılıklı dışlama gerektirir.

Ekip, profilin bu alanları sabitleme sıcak noktaları olarak belirlemesinin ardından, ReentrantLock'e hedeflenen geçiş yapmayı öncelikli olarak ele aldı ve kritik elli synchronized bölümünü dönüştürdü. Bu seçim, rekabet sırasında sanal thread'lerin çıkarılmasına izin vererek temel nedenin doğrudan ele alınmasına olanak tanıdı, temel uygulama iş mantığını değiştirmeden veya bellek ayak izini artırmadan.

Yeniden yapılandırma ve yeniden dağıtımın ardından, sistem 50.000 eşzamanlı bağlantı hedefini başardı ve stabil 100ms altı p99 gecikmesi ile çalıştı. Taşıyıcı thread havuzu, CPU çekirdekleriyle eşleşen varsayılan boyutta 24'te kaldı ve bu, sanal thread'lerin sadece içsel senkronizasyondan kaçınıldığında gerçek ölçeklenebilirlik sağladığını gösterdi.

// Önce: Taşıyıcı thread'i sabitleme synchronized (rateLimiter) { // Burada bloke olursa sanal thread çıkamaz externalApi.call(); } // Sonra: Çıkış izni verir rateLimiter.lock(); try { // Sanal thread çıkar, taşıyıcıyı serbest bırakır externalApi.call(); } finally { rateLimiter.unlock(); }

Adayların Sıkça Gözden Kaçırdığı Noktalar

Neden sabitleme özellikle senkronize bloklarla ve yerel yöntemlerle ortaya çıkıyor fakat ReentrantLock çıkmaya izin veriyor?

Sabitleme, JVM'nin içsel monitörleri (synchronized) thread-yığını bazlı monitör kayıtları ve C++ düzeyindeki VM iç yapıları kullanarak uygulamasından kaynaklanmaktadır. Bu yapılar, fiziksel OS thread'in yürütme bağlamıyla doğrudan ilişkilidir. Bir sanal thread senkronize bir bloğa girdiğinde, JVM'nin devamı başka bir taşıyıcıya güvenli bir şekilde taşımasını sağlamak mümkün değildir; bu, monitör durumunu bozar veya yerel düzeyde gerçekleşme-sonrası garantilerini ihlal eder. Öte yandan, ReentrantLock yalnızca Java’da AbstractQueuedSynchronizer üstünde uygulanır ve bu da VarHandle ve LockSupport.park ilkel bilgisini kullanır; bu, sanal thread planlayıcısının araya girmesine olanak tanır ve taşıyıcılar arasında güvenli bir şekilde çıkarma ve tekrar montaj yapmayı sağlar.

Taşıyıcı thread sabitlemesi, ForkJoinPool'un iş çalmaları ile nasıl etkileşir ve potansiyel bokluk senaryoları oluşturur?

Normal çalışmada, ForkJoinPool görevlerin CPU bağlı veya blok olmayan olduğunu varsayar; bir işçi thread bloke olduğunda, paralellik limitine kadar ek işçi oluşturmak veya aktive etmek için telafi eder. Ancak, sabitlenmiş bir sanal thread taşıyıcısını etkili bir şekilde sinyal vermeden bloke eder. Sonuç olarak, yirmi sanal thread birden yirmi taşıyıcıyı sabitlerse (örneğin, senkronize bloklara girdiğinde), planlayıcıda bekleyen binlerce hazır sanal thread'i çalıştırmak için hiçbir taşıyıcı kalmaz. Bu, engellenmemiş işin ilerleyemediği ve böylece dinamik olarak kullanılabilir havuz boyutunu felakete yol açacak şekilde küçülten bir öncelik tersine çevrilmesi yaratır.

ThreadLocal değişkenlerinin agresif kullanımı, sanal thread ortamlarında taşıyıcı thread sabitlemesine neden olabilir mi?

ThreadLocal değişkenleri sabitlemeye neden olmaz çünkü sanal thread uygulaması, montaj ve çıkarma işlemleri sırasında thread-lokal haritayı taşıyıcılar arasında taşır. Ancak, adaylar genellikle ThreadLocal'in ayrı bir bellek yönetimi felaketi olduğunun gözden kaçırıldığını unutur: milyonlarca kısa ömürlü sanal thread, her taşıyıcı thread'in daha önce barındırdığı her sanal thread için kendi ThreadLocalMap'inde giriş biriktirmesiyle sonuçlanır. Bu haritalar yalnızca anahtarın (sanal thread) açıkça kaldırılması veya çöp toplanması sırasında temizlenir, bu da uzun süreli taşıyıcı thread'lerde sınırsız bellek büyümesine yol açar. Bu, sabitleme ile ilgili olmayan fakat büyük ölçekli sanal thread dağıtımları için aynı derecede yıkıcı olan bir bellek sızıntısı oluşturur ve uygun temizlik için ScopedValue'ye (JEP 446) geçiş gerektirir.