İlk betadan beri Java'da kod yazıyorum; o zamanlar bile konular favori özellikler listemin başında yer alıyordu. Java, iş parçacığı desteğini kendi diline getiren ilk dildi; o zamanlar tartışmalı bir karardı. Geçtiğimiz on yılda, her dil eşzamansız/beklemeyi içerecek şekilde yarıştı ve … Ancak Java zagging yerine zikzak çizdi ve çok daha üstün tanıttı. Bu yazı bununla ilgili değil. Java'nın bile bunun için bazı üçüncü taraf desteği vardı sanal iş parçacıklarını (Loom projesi) Bence bu harika ve Java'nın temel gücünü kanıtlıyor. Sadece dil olarak değil kültür olarak da. Moda trendine acele etmek yerine, değişiklikleri kasıtlı olarak yapma kültürü. Bu yazıda Java'da iş parçacığı oluşturmanın eski yollarını yeniden gözden geçirmek istiyorum. Ben , , vb. işlemlere alışkınım. Ancak bunların Java'da iş parçacığı oluşturma konusunda üstün bir yaklaşım olmasından bu yana uzun zaman geçti. synchronized wait notify Ben sorunun bir parçasıyım; Bu yaklaşımlara hâlâ alışkınım ve Java 5'ten beri var olan bazı API'lere alışmakta zorlanıyorum. Bu bir alışkanlıktır. T Buradaki videolarda tartıştığım iş parçacıklarıyla çalışmak için birçok harika API var, ancak temel ama önemli olan kilitler hakkında konuşmak istiyorum. https://www.youtube.com/watch?v=pUnX8r-6IIo&embedable=true https://www.youtube.com/watch?v=pOrhJ8o9TT0&embedable=true Senkronize ve ReentrantLock Senkronize ayrılma konusunda yaşadığım isteksizlik, alternatiflerin çok daha iyi olmamasıydı. Bugün bunu bırakmanın temel motivasyonu, şu anda senkronize olmanın Loom'da iplik sabitlemeyi tetikleyebilmesidir ki bu da ideal değildir. JDK 21 bunu düzeltebilir (Loom GA'ya geçtiğinde), ancak onu bırakmak yine de mantıklıdır. Senkronize edilenin doğrudan değiştirilmesi ReentrantLock'tur. Ne yazık ki, ReentrantLock'un senkronizeye göre çok az avantajı vardır, bu nedenle geçişin faydası en iyi ihtimalle şüphelidir. Aslında çok önemli bir dezavantajı var; Bunu anlamak için bir örneğe bakalım. Senkronize edilmiş kullanımı şu şekilde kullanırız: synchronized(LOCK) { // safe code } LOCK.lock(); try { // safe code } finally { LOCK.unlock(); } ilk dezavantajı ayrıntıdır. Blok içinde bir istisna meydana geldiğinde kilit kalacağı için try bloğuna ihtiyacımız var. Bizim için sorunsuz bir şekilde senkronize edilmiş kollar. ReentrantLock Bazı kişilerin kilidi ile sarmak için kullandığı bir numara var ve kabaca şuna benziyor: AutoClosable public class ClosableLock implements AutoCloseable { private final ReentrantLock lock; public ClosableLock() { this.lock = new ReentrantLock(); } public ClosableLock(boolean fair) { this.lock = new ReentrantLock(fair); } @Override public void close() throws Exception { lock.unlock(); } public ClosableLock lock() { lock.lock(); return this; } public ClosableLock lockInterruptibly() throws InterruptedException { lock.lock(); return this; } public void unlock() { lock.unlock(); } } İdeal olan Kilit arayüzünü uygulamadığıma dikkat edin. Bunun nedeni, lock yönteminin yerine otomatik kapatılabilir uygulamayı döndürmesidir. void Bunu yaptıktan sonra, bunun gibi daha kısa bir kod yazabiliriz: try(LOCK.lock()) { // safe code } Azaltılmış ayrıntı hoşuma gidiyor, ancak kaynakla deneme temizleme amacıyla tasarlandığından ve kilitleri yeniden kullandığımızdan bu sorunlu bir kavramdır. Close'u çağırıyor ama biz bu metodu aynı nesne üzerinde tekrar çağıracağız. Kilit arayüzünü desteklemek için kaynak sözdizimi ile denemeyi genişletmenin güzel olabileceğini düşünüyorum. Ancak bu gerçekleşene kadar bu değerli bir numara olmayabilir. ReentrantLock'un Avantajları kullanmanın en büyük nedeni Loom desteğidir. Diğer avantajları da güzel ama hiçbiri “mükemmel özellik” değil. ReentrantLock Sürekli bir blok yerine yöntemler arasında kullanabiliriz. Kilit alanını en aza indirmek istediğiniz için bu muhtemelen kötü bir fikirdir ve arıza bir sorun olabilir. Bu özelliği bir avantaj olarak görmüyorum. Adil olma seçeneği vardır. Bu, ilk önce kilitte duran ilk iş parçacığına hizmet edeceği anlamına gelir. Bunun önemli olacağı, karmaşık olmayan, gerçekçi bir kullanım durumu düşünmeye çalıştım ve boşluklar çiziyorum. Bir kaynakta sürekli olarak kuyruğa alınan birçok iş parçacığının bulunduğu karmaşık bir zamanlayıcı yazıyorsanız, diğer iş parçacıkları gelmeye devam ettiğinden iş parçacığının "aç kaldığı" bir durum yaratabilirsiniz. Ancak bu tür durumlar muhtemelen eşzamanlılık paketindeki diğer seçeneklerle daha iyi karşılanır. . Belki burada bir şeyleri özlüyorum… bir kilitlenmeyi beklerken bir iş parçacığını kesmemizi sağlar. Bu ilginç bir özellik ama yine de gerçekçi bir fark yaratacağı bir durum bulmak zor. lockInterruptibly() Kesintiye karşı çok duyarlı olması gereken bir kod yazarsanız, bu yeteneği kazanmak için API'sini kullanmanız gerekir. Ancak yönteminde ortalama ne kadar zaman harcıyorsunuz? lockInterruptibly() lock() Bunun muhtemelen önemli olduğu ancak gelişmiş çok iş parçacıklı kod yazarken bile çoğumuzun karşılaşmayacağı uç durumlar vardır. OkumaYazmaReentrantLock Çok daha iyi bir yaklaşım . Kaynakların çoğu sık okuma ve az yazma işlemi ilkesini izler. Bir değişkeni okumak iş parçacığı açısından güvenli olduğundan, değişkene yazma sürecinde olmadığımız sürece kilide gerek yoktur. ReadWriteReentrantLock Bu, yazma işlemlerini biraz daha yavaşlatırken okumayı en üst düzeye çıkarabileceğimiz anlamına gelir. Bunun sizin kullanım durumunuz olduğunu varsayarsak, çok daha hızlı kod oluşturabilirsiniz. Okuma-yazma kilidiyle çalışırken iki kilidimiz olur; Aşağıdaki resimde görebileceğimiz gibi bir okuma kilidi. Birden fazla iş parçacığının geçmesine izin verir ve etkili bir şekilde "herkes için ücretsizdir". Değişkene yazmamız gerektiğinde aşağıdaki görselde göreceğimiz gibi bir yazma kilidi elde etmemiz gerekiyor. Yazma kilidini istemeye çalışıyoruz, ancak değişkenden hâlâ okunan iş parçacıkları var, bu yüzden beklememiz gerekiyor. Konuların okunması bittiğinde tüm okumalar engellenecek ve aşağıdaki resimde görüldüğü gibi yazma işlemi yalnızca tek bir iş parçacığından gerçekleşebilecektir. Yazma kilidini serbest bıraktığımızda ilk görseldeki “herkes için ücretsiz” durumuna geri döneceğiz. Bu, koleksiyonları çok daha hızlı hale getirmek için kullanabileceğimiz güçlü bir modeldir. Tipik bir senkronize liste oldukça yavaştır. Tüm işlemlerde (okuma veya yazma) senkronize olur. Okumak için hızlı olan bir CopyOnWriteArrayList'imiz var, ancak yazma işlemleri çok yavaştır. Yöntemlerinizden yineleyicileri döndürmekten kaçınabileceğinizi varsayarsak, liste işlemlerini kapsülleyebilir ve bu API'yi kullanabilirsiniz. Örneğin, aşağıdaki kodda isim listesini salt okunur olarak gösteriyoruz, ancak bir isim eklememiz gerektiğinde yazma kilidini kullanıyoruz. Bu, listelerden kolayca daha iyi performans gösterebilir: synchronized private final ReadWriteLock LOCK = new ReentrantReadWriteLock(); private Collection<String> listOfNames = new ArrayList<>(); public void addName(String name) { LOCK.writeLock().lock(); try { listOfNames.add(name); } finally { LOCK.writeLock().unlock(); } } public boolean isInList(String name) { LOCK.readLock().lock(); try { return listOfNames.contains(name); } finally { LOCK.readLock().unlock(); } } Damgalı Kilit hakkında anlamamız gereken ilk şey, onun yeniden girişli olmamasıdır. Diyelim ki bu bloğa sahibiz: StampedLock synchronized void methodA() { // … methodB(); // … } synchronized void methodB() { // … } Bu çalışacak. Senkronize edilmiş yeniden giriş olduğundan. Kilidi zaten tutuyoruz, dolayısıyla dan ye geçmek engellemez. Bu, aynı kilidi veya aynı senkronize nesneyi kullandığımızı varsayarak ReentrantLock ile de çalışır. methodB() methodA() kilidi açmak için kullandığımız bir damgayı döndürür. Bu nedenle bazı sınırları vardır. Ama yine de çok hızlı ve güçlü. Aynı zamanda paylaşılan bir kaynağı korumak için kullanabileceğimiz bir okuma ve yazma damgasını da içerir. StampedLock Ancak farklı olarak kilidi yükseltmemize olanak tanır. Bunu neden yapmamız gerekiyor? ReadWriteReentrantLock, Daha önceki yöntemine bakın… Peki ya onu "Shai" ile iki kez çağırırsam? addName() Evet, bir Set kullanabilirim… Ancak bu alıştırmanın amacı için diyelim ki bir listeye ihtiyacımız var… Bu mantığı ile yazabilirim: ReadWriteReentrantLock public void addName(String name) { LOCK.writeLock().lock(); try { if(!listOfNames.contains(name)) { listOfNames.add(name); } } finally { LOCK.writeLock().unlock(); } } Bu berbat. Bazı durumlarda yalnızca 'u kontrol etmek için yazma kilidi için "ödeme yaptım" (çok sayıda kopya olduğunu varsayarak). Yazma kilidini almadan önce işlevini çağırabiliriz. O zaman şunları yaparız: contains() isInList(name) Okuma kilidini tut Okuma kilidini serbest bırakın Yazma kilidini tut Yazma kilidini serbest bırakın Her iki kapma durumunda da kuyruğa girebiliriz ve bu fazladan uğraşmaya değmeyebilir. ile okuma kilidini yazma kilidine güncelleyebilir ve gerekirse değişikliği yerinde yapabiliriz: StampedLock public void addName(String name) { long stamp = LOCK.readLock(); try { if(!listOfNames.contains(name)) { long writeLock = LOCK.tryConvertToWriteLock(stamp); if(writeLock == 0) { throw new IllegalStateException(); } listOfNames.add(name); } } finally { LOCK.unlock(stamp); } } Bu durumlar için güçlü bir optimizasyondur. Nihayet Yukarıdaki video serisinde buna benzer pek çok konuyu ele alıyorum; bir göz atın ve ne düşündüğünüzü bana bildirin. Senkronize koleksiyonlara sıklıkla ikinci kez düşünmeden ulaşıyorum. Bu bazen makul olabilir, ancak çoğu için muhtemelen optimalin altındadır. İş parçacığıyla ilgili temel öğelerle biraz zaman harcayarak performansımızı önemli ölçüde artırabiliriz. Bu, özellikle temeldeki çekişmenin çok daha hassas olduğu Loom ile uğraşırken geçerlidir. 1 milyon eşzamanlı iş parçacığında okuma işlemlerini ölçeklendirdiğinizi hayal edin… Bu durumlarda, kilit çekişmesini azaltmanın önemi çok daha fazladır. koleksiyonların neden ve hatta kullanamadığını düşünebilirsiniz. synchronized ReadWriteReentrantLock StampedLock API'nin yüzey alanı o kadar büyük olduğundan, onu genel bir kullanım durumu için optimize etmek zor olduğundan bu sorunludur. Düşük seviyeli temel öğeler üzerindeki kontrolün, yüksek verim ile engelleme kodu arasındaki farkı yaratabileceği yer burasıdır.