Önsöz
22.09.2022 tarihinde JDK19 yayımlandı, bu sürümün en dikkat çekici özelliği sanal iş parçacıklarını desteklemesiydi; böylece hafif iş parçacığı ailesine güçlü bir üye daha katıldı。Sanal iş parçacıkları, JVM'yi iş parçacıklarının zamanlanması için işletim sistemine bağlı olmaktan kurtarır ve zamanlamayı JVM'nin kendisi yapar。Aslında sun, ilk dönemlerde Solaris işletim sistemindeki sanal makinede JVM tarafından iş parçacığı zamanlamasını uygulamıştı; karmaşıklık ve bakım kolaylığı dikkate alınarak sonunda yeniden iş parçacıklarının işletim sistemi tarafından zamanlandığı modele dönüldü。
长安归来锦衣客,昨日城南起新宅。 (Chang'an'dan sırmalı giysiler içinde dönen yolcu, dün şehrin güneyinde yeni bir konak yaptırdı.) Buraya kadar geldiğimiz yolu düşününce, çoklu iş parçacığı kavramları insanın gözünü kamaştırıyor; internetteki ilgili açıklamaların da haddi hesabı yok, ancak hep bütüncül bir bakış açısının eksik olduğu hissediliyor. Bu nedenle Java'da çoklu iş parçacığının gelişim tarihini sistematik biçimde derledim; umarım çoklu iş parçacığı bilgisini kavramanıza yardımcı olur.
Bu yazıda ele alınmayanlar:
1 Bazı teknik konuların ayrıntılı uygulama ilkeleri anlatılmaz, kaynak kod çözümlenmez, çizimler yapılmaz. Bu yazıda ilginizi çeken kavram ve teknolojiler bulursanız bunları kendiniz araştırabilirsiniz 2 Quasar, Akka ve Guava gibi eşzamanlılığı destekleyen kütüphane ve çerçeveler ele alınmaz
Bu makale neyi anlatıyor
1 JDK'nin çoklu iş parçacığı desteğinin evrim tarihini anlatır 2 Bu evrimdeki bazı teknolojilerin işlevlerini, çalışma ilkelerini, arka planını ve hangi sorunları çözdüklerini anlatır 3 Yazarın bazı teknolojilere ilişkin görüşlerini aktarır; farklı görüşleri olanlar yorumlarda tartışmaya davetlidir
Dönüm noktası
Her zamanki gibi önce bir özet tabloyla başlayalım. Bu tabloda, JDK'nın farklı sürümlerindeki iş parçacıklarıyla ilgili temel kavramlar derlenmiştir. Burada, belki de tam isabetli olmayan bir benzetmeyle, çoklu iş parçacıklarının gelişimini otomobillere benzetebiliriz: bu gelişim sırasıyla manuel vites döneminden (JDK1.4 ve öncesi), otomatik vites dönemine(JDK5-JDK18), ardından otonom sürüş dönemine (JDK19 ve sonrası) geçmiştir. Bu benzetmenin tek amacı, okuyucuya JDK5'ten itibaren çoklu iş parçacıklarını daha rahat yönetmenin mümkün olduğunu, JDK19'dan itibaren ise bunun rahatlığın da ötesine geçerek yoğun IO kullanan hizmetlerin performansında niteliksel bir sıçrama sağladığını anlatmaktır。
| Çağ | Sürüm | Yayımlanma zamanı | Temel kavramlar |
|---|---|---|---|
| Manuel vites | JDK1.0 | 1996-01-23 | Thread ve Runnable |
| Manuel vites | JDK1.2 | 1998-12-04 | ThreadLocal、Collections |
| Otomatik vites | JDK1.5/5.0 | 2004-09-30 | Java bellek modelinin netleştirilmesi, eşzamanlılık paketinin eklenmesi |
| Otomatik vites | JDK1.6/6.0 | 2006-12-11 | synchronized optimizasyonu |
| Otomatik vites | JDK1.7/7.0 | 2011-07-28 | Fork/Join çerçevesi |
| Otomatik vites | JDK1.8/8.0 | 2014-03-18 | CompletableFuture、Stream |
| Otomatik vites | JDK1.9/9.0 | 2014-09-08 | Kilit çekişmesi mekanizmasını iyileştirme |
| Otomatik vites | JDK10 | 2018-03-21 | İş parçacığına yerel denetim |
| Otomatik vites | JDK15 | 2020-09-15 | Taraflı kilitlemeyi devre dışı bırakma ve kullanımdan kaldırma |
| Otonom sürüş | JDK19 | 2022-09-22 | Sanal iş parçacıkları |

Düz vites çağı
JDK1.4 ve öncesini, çoklu iş parçacığının “manuel vites” dönemi olarak adlandırıyorum; buna yerel çoklu iş parçacığı dönemi de denir. İş parçacığı işlemleri henüz oldukça temel düzeydeydi ve kullanılabilecek iş parçacığı havuzları yoktu. Geliştiriciler, sık sık iş parçacığı oluşturmanın neden olduğu kaynak israfını önlemek için araçları kendileri yazmak ve paylaşılan kaynakları elle kilitlemek zorundaydı. Birçok başarılı çoklu iş parçacığı çatısı da bu dönemde filizlendi; en ünlüsü JDK5.0 tarafından benimsendi.
JDK 1.0 Thread ve Runnable
1996 yılının 1. ayında çıkan JDK1.0 sürümü, Java'nın en temel iş parçacığı modelini daha başlangıçta belirledi. Bu model, sonradan yapılan düzeltme ve iyileştirmelerde esaslı bir değişikliğe uğramadı; sürekliliğini koruyan iyi bir tasarım olduğu söylenebilir. Kesintili ve iş birlikli zamanlama, yaygın iki süreç/iş parçacığı zamanlama yöntemidir. İşletim sistemleri süreçlerini zamanlamak için kesintili yönteme çok uygundur: Farklı süreçlere zaman dilimleri atar, uzun süre yanıt vermeyen süreçlerin kaynaklarını elinden alabilir, hatta onları zorla durdurabilir. İş birlikli yöntemde süreçlerin kaynakları kendi istekleriyle, etkin biçimde bırakması gerekir. Bu zamanlama yönteminde uzun süre çalışan bir iş parçacığı, CPU'ya ihtiyaç duyan diğer tüm iş parçacıklarını “aç bırakabilir”. Java hotspot sanal makinesi kesintili zamanlama kullandığından Java dili de en başından itibaren kesintili iş parçacığı zamanlamasını benimsemiştir. JDK 1.0 sürümünde iş parçacıkları esas olarak Thread sınıfından türeyerek veya Runnable arayüzünü uygulayarak oluşturulur; nesne örneğinin start metodu ile başlatılır, paralel işlenecek kod run metoduna konur ve iş parçacıkları arasındaki iş birliği ve iletişim stop/resume/suspend gibi basit ve kaba metotlarla sağlanır.

stop/resume/suspend kavramları nasıl açıklanabilir? Ana iş parçacığının, alt iş parçacığının sonlandırma, duraklatma ve sürdürme metotlarını doğrudan çağırabilmesidir。Çocukken taşınabilir kasetçalar kullandıysanız, üzerinde üç düğme vardı: durdur, duraklat, devam et。Aynı anda 3 taşınabilir kasetçalar dinlediğinizi düşünün; bu üç kasetçalar üç alt iş parçacığıdır, siz de ana iş parçacığısınız; bu üç cihazın başlatılıp durdurulmasını dilediğiniz gibi kontrol edebilirsiniz。
Bu mekanizmanın ölümcül bir sorunu vardır: kolayca kilitlenmeye yol açabilir. Çünkü A iş parçacığı bir kaynağı kilitleyip henüz serbest bırakmadan ana iş parçacığı tarafından askıya alınırsa (suspend yöntemi kilidi serbest bırakmaz), B iş parçacığı bu kaynağı kullanmak istediğinde ancak A iş parçacığının çalışmaya devam edip (resume) kaynağı serbest bırakmasını bekleyebilir. Aksi takdirde kaynağı hiçbir zaman elde edemez ve kilitlenme oluşur.
JDK 1.2
Bu sürümde kaba stop/resume/suspend mekanizmasının kullanımı yasaklandı; onun yerine wait/notify/sleep gibi birden fazla iş parçacığının birlikte çalıştığı yöntemlere geçildi. Bu sürümde AtomicityXXX atomik nesnelerinin de tasarlanmış olduğunu belirtmek gerekir; bunlar esas olarak i++ işleminin atomik olmaması sorununu çözüyordu. ThreadLocal ve Collections'ın eklenmesi, çoklu iş parçacığı kullanımına yeni yollar kazandırdı. Bu iki teknoloji nedeniyle ben bu döneme Java'nın turboşarj çağı diyorum.
ThreadLocal
ThreadLocal, iş parçacığı güvenli olmayan nesnelerin birden çok iş parçacığında kullanımını kilit kullanmadan sağlayan bir çözümdür. “Banka hesabına veya stok miktarına ekleme yapma ya da bunlardan düşme” gibi sorunları çözemez; güçlü olduğu alan, “araç” niteliğindeki sınıfların kopyalarını kullanarak “araç” metotlarını güvenle çalıştırmaktır. SimpleDateFormat ve veritabanı bağlantıları tipik örneklerdir. Tasarımının son derece incelikli olduğunu da belirtmek gerekir. Bunu sizin tasarladığınızı düşünün; akla gelen basit yaklaşım genellikle şudur: ThreadLocal içinde, anahtarı iş parçacığı ve değeri paylaşılan nesne olan, küresel ve iş parçacığı güvenli bir Map tutmak. Bu tasarımın dezavantajı bellek sızıntısıdır; çünkü daha fazla iş parçacığı eklendikçe Map sınırsız büyür. Bellek sızıntısını çözmek için iş parçacığı sona erdiğinde bu Map'in temizlenmesi gerekir; bu da GC yeteneklerinin güçlendirilmesini gerektirir ve açıkça maliyetine değmez. Bu nedenle ThreadLocal, Map'i kendisi tutacak şekilde değil, Thread'in kendisi tutacak şekilde tasarlanmıştır. Anahtar ThreadLocal değişkeni, değer ise ilgili değerdir. Her Thread, kullandığı ThreadLocal değişkenlerini burada saklar(elbette bu tasarımın doğurduğu başka sorunlar da vardır; burada bunlara değinilmiyor, ilgilenenler kendileri araştırabilir).
Collections
Collections yardımcı sınıfı bu sürümde tasarlandı; SynchronizedList gibi bazı iş parçacığı güvenli koleksiyon sarmalayıcıları sunuyordu. İş parçacığı güvenli koleksiyonların yalnızca Hashtable, Vector, Stack ve benzerlerinden ibaret olduğu o dönemde ortaya çıkması, dönem açısından da önemliydi. Collections aracının temel fikri şudur:İş parçacığı güvenli olmayan koleksiyonları sizin için iş parçacığı güvenli hâle getiririm; böylece mevcut kodunuzu yükseltmek ve uyarlamak için fazla zaman harcamanız gerekmez, koleksiyonu oluştururken sunduğum metotla başlatmanız yeterlidir. Bu, motor hacmini değiştirmeden motorun gücünü ve torkunu artıran otomobillerdeki turbo teknolojisine benzer. Java'nın turbo çağı geldi^\_^
Otomatik vites çağı
JDK 5.0
Eşzamanlılık paketini içe aktarma
Doug Lea’nın Çince adı 道格·利’dir. ABD’de bir üniversite öğretim üyesi ve alanının ustalarından biridir; J.U.C onun eseridir. JDK1.5 öncesinde, programların eşzamanlı erişimini denetlemek ve kodu senkronize etmek için yalnızca synchronized kullanabiliyorduk. O dönemde synchronized’ın performansı henüz iyileştirilmemişti ve iyi değildi; iş parçacıklarını denetlemek için de yalnızca Object’in wait ve notify yöntemlerini kullanabiliyorduk. Bu sırada Doug Lea, JCP’ye JSR-166 önerisini sundu. JSR-166 önerisini sunmadan önce Doug Lea, J.U.C paketindekine benzer işlevlere sahip kodları üç yılı aşkın süredir kullanıyordu. İşte bu kodlar J.U.C’nin ilk örneğiydi.
J.U.C; atomik nesneler, kilitler ve yardımcı araç takımları, iş parçacığı havuzları ve iş parçacığı güvenli kapsayıcılar gibi birkaç ana araç kategorisi sunar. Geliştiriciler bu olanakları esnek biçimde kullanarak kendi ürünlerini oluşturabilir; ister ReentrantLock ile altyapı çerçeveleri kurabilir, ister hazır araç ve kapsayıcılarla doğrudan iş mantığı kodu yazabilirler. Tarihsel açıdan bakıldığında J.U.C, 2004 yılında tartışmasız biçimde “ileri teknoloji ürünü” olarak adlandırılabilirdi. Java'nın yaygınlaşmasına çok büyük katkı sağladı. Java'nın otomatik vites dönemi gelmişti; tıpkı otomatik vitesli araçların sürücülüğü kolaylaştırması gibi, J.U.C de programcıların çoklu iş parçacıklarını kullanmasını büyük ölçüde kolaylaştırdı. Bu, bir dönemi başlatan bir üründü。
Elbette J.U.C'nin de bazı kusurları vardır:
Yüksek CPU yükü:Döngü içinde tekrar denenen CAS uzun süre başarılı olmazsa CPU üzerinde çok büyük bir yük oluşturur.
Çözüm:JUC içinde bazı yerlerde CAS döngülerinin sayısı sınırlandırılmıştır, örneğin BlockingQueue kapsamındaki SynchronousQueue'da.
ABA sorunu:Bir değer başlangıçta A iken B'ye dönüşüp sonra yeniden A olursa, CAS kontrolü değerin değişmediğini görür, ancak gerçekte değer değişmiştir; buna ABA sorunu denir。Çoğu durumda ABA sorunu programın eşzamanlı çalışmasının doğruluğunu etkilemez。
Çözüm: Her değişkene bir sürüm numarası ekleyin ve her değişiklikte 1 artırın; yani A —> B —> A, 1A —> 2B —> 3A olur. Java, bu sorunu çözmek için AtomicStampedReference sunar. AtomicStampedReference, [E,Integer] ikilisini sarmalayarak nesnelere sürüm damgası (stamp) ekler ve böylece ABA sorununu önler.
Yalnızca tek bir paylaşılan değişken üzerindeki işlemin atomikliğini garanti edebilir:CAS mekanizması yalnızca tek bir değişken üzerindeki işlemin atomikliğini garanti eder; bütün kod bloğunun atomikliğini garanti edemez.
Çözüm: Örneğin 3 değişkenin birlikte atomik olarak güncellenmesini sağlamak gerekiyorsa Synchronized kullanmak zorunlu olur. Birden fazla değişkeni AtomicReference ile sarmalamak da düşünülebilir; böylece birden fazla paylaşılan değişkenin bulunduğu durumlar ele alınabilir.
Java bellek modelini netleştirme
JDK'nin bu sürümü, Java bellek modelini yeniden netleştirdi. Bundan önce yaygın bellek modelleri arasında sıralı tutarlılık bellek modeli ve önce gerçekleşme modeli bulunuyordu. Sıralı tutarlılık modelinde programın yürütülme sırası, kodda görünen sırayla tamamen aynıdır. Bunu, çok çekirdekli ve komut yürütme optimizasyonları kullanan modern CPU'larda garanti etmek oldukça zordur. Ayrıca sıralı tutarlılık garantisi, JVM'nin kod üzerinde çalışma zamanında yapabildiği optimizasyonları ciddi biçimde sınırlar.
Ancak bu sürümde JSR 133 belirtiminin tanımladığı önce gerçekleşme (Happens-before) ilişkisi, talimatların yürütülme sırasına esneklik kazandırır:
Aynı iş parçacığında, kodun yürütülme sırasına (yani kodun anlamsal sırasına) göre önceki işlem sonraki işlemden önce gerçekleşir Bir monitor nesnesinin kilidini açma işlemi, aynı monitor nesnesinin daha sonraki kilitleme işleminden önce gerçekleşir Bir volatile alana yazma işlemi, bu alanın daha sonraki okuma işleminden önce gerçekleşir Bir iş parçacığını başlatma işlemi (iş parçacığı nesnesinin start() yöntemini çağırmak), o iş parçacığındaki diğer tüm işlemlerden önce gerçekleşir Bir iş parçacığındaki tüm işlemler, başka herhangi bir iş parçacığının bu iş parçacığı üzerinde join() yöntemini çağırmasından önce gerçekleşir A işlemi B'den önce, B işlemi de C'den önce gerçekleşiyorsa A işlemi C'den önce gerçekleşir
Bellek tahsisi açısından, her iş parçacığının kendi çalışma belleğini ana bellekten ayırmak, JVM'ye iş parçacığı içindeki talimatların yürütülmesini optimize etmek için daha da geniş bir alan sağlar. Ana bellekteki değişkenler, ayrı olarak işlenmek üzere iş parçacığının çalışma belleğine kopyalanabilir; yürütme bittikten sonra sonuçlar belirli bir anda ana belleğe geri yazılabilir: Ancak farklı iş parçacıkları arasındaki veri tutarlılığı nasıl sağlanır? JLS'nin (Java Dil Belirtimi) sunduğu yaklaşım şudur: Varsayılan olarak, herhangi bir andaki veri tutarlılığı garanti edilemez; ancak anlamları güçlendirilmiş synchronized, volatile ve final anahtar sözcükleri kullanılarak veri tutarlılığı sağlanabilir.
JDK 6.0 synchronized optimizasyonu
“Cumhuriyetin ilk oğlu” sayılan synchronized anahtar sözcüğü, 5.0 sürümünde ReentrantLock'un gölgesinde kalmıştı. Bu sürümde durumu tersine çevirmesi gerekiyordu; bu nedenle JDK 6.0, kilit döndürme, kilit eleme, kilit birleştirme, hafif kilitler ve yanlı kilitleme gibi bazı kilit optimizasyonları yaptı. Bu optimizasyon, “ayrıntılı yönetim” anlayışının bir uygulamasıydı. Optimizasyondan önce synchronized ile kilitlenen nesnelerin yalnızca iki durumu vardı:kilitsiz ve kilitli(ağır kilit). Optimizasyondan sonra kilitlerin toplam 4 durumu oldu; düşükten yükseğe sıralama şöyleydi:kilitsiz, yanlı kilit, hafif kilit, ağır kilit. Bu durumlar çekişmeye bağlı olarak kademeli biçimde yükselir, ancak düşürülemez; amaç kilit edinme ve bırakma verimliliğini artırmaktır(yazara göre aslında konu fazla karmaşık olduğu için JVM geliştiricileri buna girişmekten çekinmiştir).
Bu optimizasyon synchronized'ın yüzünü güldürdü; bundan sonra kimsenin performansının ReentrantLock'tan kötü olduğunu söylemesine izin vermeyecekti. Ama asıl olay daha sonra geldi: yanlı kilitleme JDK 15 ile kullanımdan kaldırıldı(─.─||). Bana göre synchronized'ın dezavantajı, yalnızca bir anahtar sözcük olması; alt düzey işlemlerini JVM yürütür ve uygulama kilit alırken hangi yaklaşımın uygun olduğunu JVM'nin “tahmin etmesi” gerekir. ReentrantLock ise bu işi doğrudan programcıya bırakır: adalet istiyorsanız adil kilit, adaletsizlik istiyorsanız adil olmayan kilit kullanırsınız. Tasarım açısından biraz işten kaçmak sayılabilir ama aynı zamanda esneklik de sağlar.
JDK 7.0 Fork/Join çatısı
Fork/Join da ortaya çıktığında oldukça ileri bir üründü; temel üstünlüğü, görevleri özyinelemeli biçimde parçalara ayırmayı ve her görevin sonucunu birleştirmeyi desteklemesidir. Ancak hem tanıdık hem de yabancı bir teknolojidir: Tanıdıktır, çünkü ileride JDK8.0 kapsamında ele alınacak CompletableFuture ve Stream gibi pek çok yerde kullanılır; yabancıdır, çünkü iş uygulamalarını geliştirirken onu pek az kullanıyor gibiyiz.
Hatta bazıları bunun pek işe yaramadığını düşünüyor. Bana göre, iş gereksinimlerine yönelik geliştirme yapıyorsanız pek işe yaramaz; çünkü temelde kullanılmaz. Büyük veri hacimli senaryolarda veri ambarı araçları vardır, diğer senaryolarda ise iş parçacığı havuzları kullanılabilir. Ara yazılım çerçeveleri geliştiren biriyseniz işe yarar, belki kullanmanız gerekebilir. Çince internette bu teknolojiyi sorgulayan çok az kişi var, ancak yurt dışında tartışılmaya başlandı bile; ilgilenenler doğrudan bağlantıya gidip inceleyebilir Java'daki Fork-Join çerçevesi bozuk mu?
JDK 8.0
Bu sürümün yayımlanması Java için çığır açıcıydı; öyle ki bugün dünya genelinde çalışan Java programlarının yarısından fazlası bu sürümü kullanıyor. Ancak çoklu iş parçacığıyla ilgili yenilikler JDK5.0 kadar köklü değildi. Birkaç atomik nesne eklenmesinin yanı sıra bu sürümün en dikkat çekici yenilikleri aşağıdaki ikisiydi.
CompletableFuture
İnternette CompletableFuture hakkında pek çok tanıtım yazısı var; çoğu, çalışma ilkelerini ve nasıl kullanılacağını anlatıyor. Ancak yazar olarak bir soruyu bir türlü anlayamıyordum: Bu kadar çok iş parçacığı havuzu aracı varken CompletableFuture neden ortaya çıktı, hangi sorunu çözüyor? Onu asıl öne çıkaran özellik tam olarak ne? Üzerine düşündüyseniz sizin de bu soruyu soracağınıza inanıyorum. Merak etmeyin, cevabı sizin için buldum.
Sonuç: CompletableFuture'ın temel üstünlüğü Görev orkestrasyonu。CompletableFuture, Future arayüzünün özelliklerini devralır,görevleri eşzamanlı yürütme gibi yetenekleri başka araçlarla da sağlanabilir。Ancak görev orkestrasyonu yeteneğinin yerini tutan yoktur,temel API'lerindeki görev zinciri oluşturma ve görev sonuçlarını birleştirme gibi işlevlerin hepsi görev orkestrasyonu için tasarlanmıştır。Bu nedenle JDK'nin bu sürümde bu çerçeveyi sunmasının temel amacı,iş uygulaması geliştirmede giderek daha büyük bir sorun hâline gelen görev orkestrasyonu ihtiyacını karşılamaktır。
Son olarak bir şey daha ekleyeyim, CompletableFuture altyapısında Fork/Join çerçevesi kullanılarak gerçekleştirilmiştir。
Stream
《架构整洁之道》 (Temiz Mimari) kitabında üç programlama paradigmasından söz edilir: yapısal programlama (prosedürel programlama), nesne yönelimli programlama ve fonksiyonel programlama. Stream, fonksiyonel programlamanın Java dilindeki yansımalarından biridir. Bana göre başlangıç düzeyindeki bir programcının orta düzeye geçerken mutlaka aşması gereken aşama, Stream'e hâkim olmaktır. Stream ile ilk karşılaştığınızda kesinlikle epey yabancılık çekersiniz; ancak alıştıktan sonra programlama biçiminiz için yeni bir düşünce kapısı açılır. Yazılım geliştiriciler olarak fonksiyonel programlama, Stream ve Lambda ifadeleri kavramlarını sık sık karıştırır, üçünün de aynı şeyi anlattığını düşünürüz. Benim anlayışım şöyle:
•İşlevsel programlama bir programlama yaklaşımıdır ve çeşitli programlama dillerinde uygulamaları bulunur
•Stream, JDK8.0 ile gelen yeni bir özelliktir; işlevsel programlama anlayışına uyum sağlamak amacıyla ortaya atılmış yeni bir kavram olarak da düşünülebilir. Stream aracılığıyla koleksiyon sınıfları üzerinde işlevsel programlama yapılabilir
•Lambda ifadesi(lambda expression), işlevsel programlamayı daha özlü ve verimli ifade etmeyi sağlayan anonim bir fonksiyondur
Peki, bu kadar anlattıktan sonra Stream ile çoklu iş parçacığı arasındaki ilişki nedir? Stream'deki ilgili paralel yöntemler, altyapıda Fork/Join çatısı kullanılarak uygulanır. Effective Java'daki ilgili tavsiyelerden biri “Stream paralelliğini dikkatli kullanın” şeklindedir. Bunun nedeni, tüm paralel işlemlerin ortak bir Fork/Join havuzunda çalışmasıdır; bir işlem hattının anormal çalışması, ilgisiz diğer bölümlerin performansını olumsuz etkileyebilir.
JDK 9.0
Kilit çekişmesi mekanizmasını iyileştirme
Kilit çekişmesi, birçok Java çok iş parçacıklı uygulamasının performansını sınırlar. Yeni kilit çekişmesi mekanizması, Java nesne monitörlerinin performansını iyileştirmiş ve JVM'nin azami işlem hacmini tahmin edebilen çeşitli kıyaslama testleriyle(Volano gibi)doğrulanmıştır. Uygulamada, yeni kilit çekişmesi mekanizması 22 farklı kıyaslama testinde de mükemmel sonuçlar elde etmiştir. Yeni mekanizma Java 9 içinde kullanılabilirse, uygulamaların performansı büyük ölçüde artacaktır. Basitçe açıklamak gerekirse, birden fazla iş parçacığı bir kilit için çekiştiğinde, optimizasyondan önce: sonradan gelen iş parçacıklarının hepsi kilidi beklemek için aynı standart süreci izler. Optimizasyondan sonra: JVM, optimize edilebilecek bazı durumları belirlediğinde, sonradan gelen iş parçacıklarının doğrudan bir “VIP geçişi” üzerinden kilidi ele geçirmeye çalışmasına izin verir.
Ayrıntılı açıklama için bkz.: Çekişmeli kilitlerin açıklaması – performans odaklı bir yaklaşım
Reaktif akışlar
Reaktif Akışlar (Reactive Streams), asenkron veri akışlarını engellemesiz geri basınç yöntemiyle işleyen bir standarttır; gerekli işlemleri ve varlıkları tanımlamak için asgari bir arayüz, metot ve protokol kümesi sunar.
Bloklamayan geri basınç nedir?背压, back pressure ifadesinin kısaltmasıdır; basitçe üretici tüketiciye veri gönderir, tüketici artık yetişemediğinde bunu üreticiye bildirir ve üretici üretim hızını düşürür. Bu mekanizmayı bloklayarak uygulamak en kolay yoldur; yani veri gönderilirken basınç bilgisi doğrudan geri döndürülür. Bloklamayan uygulama tasarımın karmaşıklığını artırırken performansı da yükseltir. Not:背压 teriminin çevirisinin iyi olmadığını düşünüyorum, yalnızca sözcük anlamına bakarak yorumlanamaz. 反压(ters basınç)daha iyi olmaz mıydı^\_^
Tüketicinin büyük kaynak baskısına maruz kalıp çökmesi sorununu çözmek için veri akışının hızı kontrol edilmelidir; buna akış kontrolü denir ve hızlı veri akışının hedefi aşırı yüklemesini önler. Bu nedenle geri basınç gerekir; üretici ve tüketici, bir geri basınç mekanizması uygulayarak birlikte çalışmalıdır. Bu mekanizma asenkron ve engellemesiz olmalıdır. Senkron ve engelleyici olursa, tüketici verileri işlerken üreticinin beklemesi gerekir ve bu da performans sorunlarına yol açar.
Reactive Streams, bir dizi varlık, arayüz ve birlikte çalışma yöntemi tanımlayarak engellemesiz geri basıncın uygulanmasına yönelik bir standart sunar. Üçüncü taraflar bu standardı izleyerek somut çözümler geliştirir; yaygın örnekler arasında Reactor, RxJava, Akka Streams ve Ratpack bulunur.
JDK 10 iş parçacığına yerel denetim
Safepoint ve eksiklikleri:
Safepoint, Hotspot JVM’de tüm uygulamaların durmasını sağlayan bir mekanizmadır. JVM, bazı alt düzey işlemleri yapabilmek için Stop The World uygulayarak bütün uygulama iş parçacıklarını durdurmak zorundadır. Ancak bunu doğrudan ve zorla yapamaz; bunun yerine uygulama iş parçacıklarına durmaları gerektiğini bildiren bir komut sinyali gönderir. Uygulama iş parçacığı yürütme sırasında bir Safepoint noktasına ulaştığında bu komuta uyar ve yanıt verir. Safepoint adının nedeni de budur. Adındaki safe sözcüğü, JVM’nin genel kapsamlı ve güvenli bazı işlemler yapacağını vurgular; bu yüzden bu noktaya safe denmiştir.
Küresel güvenli nokta işlemleri şunlardır: 1、Çöp toplama duraklatması 2、Kod optimizasyonunun geri alınması(Code deoptimization). 3、Kod önbelleğinin boşaltılması. 4、Sınıf dosyalarının yeniden tanımlanması(Class redefinition, örneğin çalışırken güncelleme veya instrumentation). 5、Taraflı kilidin iptali(Biased lock revocation). 6、Çeşitli hata ayıklama işlemleri(örneğin: kilitlenme denetimi veya yığın izi dökümü vb.).
Ancak tüm iş parçacıklarının en yakın güvenli noktada (safepoint) durmasını sağlamak bile oldukça uzun sürer. Üstelik tüm iş parçacıklarını durdurmak biraz fazla düşüncesiz ve keyfî görünmüyor mu? Bu nedenle Java10, tüm iş parçacıklarını durdurmayı gerektirmeyen bir yöntem getirdi: iş parçacığına özgü el sıkışma (Thread Local Handshake).
Örneğin aşağıdaki senaryolar tüm iş parçacıklarını durdurmadan çözülebilir: 1、Yanlı kilidin iptali. Bunun için yalnızca tek bir iş parçacığını durdurmak yeterlidir; tüm iş parçacıklarını durdurmak gerekmez. 2、Farklı tanılama sorgularının VM'nin genel gecikmesine etkisini azaltmak; örneğin çok sayıda Java iş parçacığı bulunan bir VM'deki tüm iş parçacıklarının yığın izlerini almak yavaş bir işlem olabilir. 3、Sinyallere(signals)bağımlılığı azaltarak daha güvenli yığın izi örneklemesi yapmak. 4、Asimetrik Dekker eşzamanlama tekniği denilen yöntemle Java iş parçacıklarıyla el sıkışarak bazı bellek bariyerlerini ortadan kaldırmak. Örneğin G1 ve CMS'de kullanılan “koşullu kart işaretleme kodu”(conditional card mark code)artık “bellek bariyeri” denen şeye ihtiyaç duymayacak. Böylece G1'in gönderdiği “yazma bariyeri(write barrier)” optimize edilebilecek ve “bellek bariyerini” atlatmaya çalışan dallar da silinebilecek.
JDK 15 yanlı kilitlemeyi devre dışı bırakıyor ve kullanımdan kaldırılacak olarak işaretliyor
Yanlı kilitleme neden kullanımdan kaldırılıyor? Yanlı kilitlemenin geçmişte sağladığı performans artışı artık o kadar belirgin görünmüyor. Yanlı kilitlemeden yararlanan uygulamalar genellikle eski Java koleksiyon API’lerini (JDK 1.1) kullanan programlardır; bu API’ler (Hashtable ve Vector) her erişimde eşzamanlama yapar. JDK 1.2, tek iş parçacıklı senaryolar için eşzamanlanmamış koleksiyonları (HashMap ve ArrayList), JDK 1.5 ise çok iş parçacıklı senaryolar için daha yüksek performanslı eşzamanlı veri yapılarını getirdi. Bu, gereksiz eşzamanlama nedeniyle yanlı kilitlemeden yararlanan uygulamaların, kodları daha yeni sınıfları kullanacak şekilde güncellenirse büyük bir performans artışı görebileceği anlamına gelir. Ayrıca, iş parçacığı havuzu kuyrukları ve çalışan iş parçacıkları etrafında kurulan uygulamalar, yanlı kilitleme devre dışı bırakıldığında genellikle daha iyi performans gösterir.
Aşağıda Hashtable ve Vector kullanan API'lerle gerçekleştirilmiştir: java.lang.Classloader Vector kullanır java.util.Properties extends Hashtable java.security.Provider extends Properties java.net.URL Hashtable kullanır java.net.URConnection Hashtable kullanır java.util.ZipOutputStream Vector kullanır javax.management.timer.TimerMBean arayüzde Vector bulunur
Otonom sürüş çağı
Sanal iş parçacıkları Java'yı otonom sürüş çağına taşıdı. Go、C#、Erlang、Lua gibi pek çok dilde “sanal iş parçacıklarına” benzer teknolojiler vardır; bunlara “eş yordam” denir. Java bu kez hiçbir yeni anahtar sözcük, hatta yeni bir kavram bile eklemedi; sanal iş parçacıklarını anlamak goroutine ve eş yordamları anlamaktan çok daha kolaydır, adına bakınca bile ne yaptıkları az çok anlaşılır.
JDK 19 sanal iş parçacıkları
Geleneksel Java iş parçacığı modeli, işletim sistemiyle 1:1 eşleşir; iş parçacığı oluşturmanın ve iş parçacıkları arasında geçiş yapmanın maliyeti yüksektir ve işletim sistemi kısıtlamaları nedeniyle yalnızca sınırlı sayıda oluşturulabilir. Eşzamanlılık çok yüksek olduğunda her istek için bir iş parçacığı oluşturulamaz. İş parçacığı havuzu kullanmak sorunu hafifletebilir; havuz, iş parçacığı oluşturma maliyetini azaltır ancak iş parçacığı sayısını artıramaz. Eşzamanlı istek sayısı 2000 ve havuzda yalnızca 1000 iş parçacığı varsa, aynı anda yalnızca 1000 istek işlenebilir; kalan 1000 istek işlenemez, reddedilebilir veya bir iş parçacığı serbest kalana kadar bekletilebilir.
Sanal iş parçacıklarından önceki çözüm, asenkron bir yaklaşım kullanmaktı. Pek çok çatı, iş parçacıklarını paylaşarak daha yüksek kullanılabilirlik sağlayan asenkron eşzamanlı programlamayı zaten uygulamıştı (Spring5'in Reactor'ü gibi). Temel ilke, iş parçacıklarının paylaşılmasıyla aralarındaki geçişleri azaltmak, kaynak tüketimini düşürmek ve aynı zamanda bloklanmayı önlemektir: İş parçacıkları yalnızca program çalışırken kullanılır; programın beklemesi gerektiğinde ise iş parçacığı meşgul edilmez. Asenkron yaklaşım gerçekten de birçok iyileştirme sağlar, ancak dezavantajları da vardır. Asenkron çatıların çoğu zincirleme yazım kullanarak programı çok sayıda adıma böler ve her adım farklı bir iş parçacığında yürütülebilir. Artık alışık olduğunuz ThreadLocal gibi eşzamanlı programlama API'lerini kullanamazsınız; aksi takdirde hatalar oluşabilir. Programlama tarzı da büyük ölçüde değişir; geleneksel programlama tarzından çok daha karmaşıktır, öğrenme maliyeti yüksektir ve projedeki mevcut modüllerin birçoğunu asenkron modele uyarlamak için değiştirmeniz gerekebilir.
Sanal iş parçacıklarının çalışma ilkesi bazı asenkron çerçevelere benzer,bunlar da iş parçacıklarını paylaşır,dolayısıyla havuzlama gerektirmez。Kullanırken sanal iş parçacıklarının sınırsızca mevcut olduğunu düşünebilirsiniz,istediğiniz kadar oluşturabilirsiniz,sorun çıkmasından endişelenmenize gerek yoktur。Üstelik,sanal iş parçacıkları hata ayıklamayı destekler,Java ile ilgili izleme araçları tarafından da desteklenir,bu çok önemlidir。Sanal iş parçacıkları programınızın bellek kullanımını büyük ölçüde azaltır,Web Servers gibi tüm G/Ç yoğun uygulamalar,aynı donanım koşullarında,G/Ç işlem hacmini büyük ölçüde artırabilir。Önceden 1G bellekle aynı anda 1000 erişim barındırılabilirken,sanal iş parçacıkları kullanıldığında,resmî açıklamaya göre,100 on bin eşzamanlı erişim kolayca karşılanabilir,belirli iş senaryolarında bunun desteklenip desteklenemeyeceğini yük testleri gösterecektir,ancak bu sayıyı düşürsek bile,10 on bin erişime kolayca ulaşılabilmelidir,üstelik bunun için herhangi bir bedel ödemeniz gerekmez,belki kodu bile değiştirmeniz gerekmez。Çünkü sanal iş parçacıkları geleneksel programlama tarzını,yani her istek için bir iş parçacığı modelini korumanızı sağlar,sanal iş parçacıklarını normal iş parçacıkları gibi kullanırsınız,programda yalnızca çok az değişiklik gerekir。Sanal iş parçacıkları yeni bir sözdizimi de getirmez,öğrenme ve geçiş maliyetinin son derece düşük olduğu söylenebilir。
Sanal iş parçacıklarının altyapısında hâlâ Fork/Join çerçevesinin kullanıldığını da belirtmek gerekir.