JAVA MULTITHREADING — Bolum-3: Locking Strategies, Synchronization and Reentrant Locks
Merhaba, “Java Multithreading” yazı serisinin 3. bölümünde aşağıdaki konulardan bahsedeceğim.
- Lock Strategies
- Synchronizations
- Reentrant Locks
- Reentrant vs Synchronized
Bir önceki yazıda critical section, race conditions ve atomic operations konularından bahsetmiştim. İncelemek isterseniz buradan ulaşabilirsiniz.
Locking Strategies
Birden fazla paylaşılan kaynağımız(shared resources) olduğunu varsayarsak, paylaşılan kaynaklar için tek bir lock mı kullanmalıyız yoksa her kaynak için ayrı bir lock mı kullanmalıyız sorusu locking stratejimizi belirleyecektir.
Coarse-Grained Locking Strategy
Bütün paylaşılan kaynaklar üzerinde tek bir lock koymak istediğimizde Coarse-Grained locking stratejisini kullanabiliriz. Bu yaklaşımda; eğer thread paylaşılan kaynakların çoğu üzerinde işlem yapıyorsa, bu stratejiyi kullanmak bizim için faydalı olacaktır çünkü tüm kaynakları aynı anda kilitleyip concurrent access’e karşı koruyor olacağız. Yukarıdaki örnekte thread-1 taskları repository’den getirirken aynı anda thread-2 tasksQueue’ya task ekleyemeyecektir ta ki thread-1 işini bitirene kadar.
Fine-Grained Locking Strategy
Paylaşılan kaynaklar üzerinde ayrı ayrı lock koymak istediğimizde bu strateji bizim için uygun olacaktır. Buradaki avantaj paylaşılan resource’ler üzerince concurrent access’e izin verilebilir çünkü her bir kaynak üzerinde farklı bir lock koymuşuzdur. Dezavantaj olarak düşünürsek kaynaklar üzerinde ayrı ayrı lock unlock mekanizmalarını planlamak bizim için zahmetli olabilir.
Şimdi bu lock stratejilerini nasıl uygulayacağımıza bakabiliriz.
Synchronizations
Kilitleme(Locking) mekanizmalarından biri olan “Syncronization” ‘ı örnekler üzerinden inceleyerek yorumlamaya çalışalım.
- Example-1 ‘ı göz önünde bulundurursak thread-1 SharedClass içindeki herhangi bir methodu çalıştırmak istediğinde, thread-2 hiçbir şekilde “getTaskFromDB” ve “addTaskToQueue” methodlarını synchronized olduğundan dolayı çalıştıramayacaktır ta ki thread-1’in işi bitene kadar(Coarse-Grained Strategy). Böylece tüm paylaşılan kaynaklar seviyesinde bir kilitleme mekanizması ortaya koymuş olduk.
- Example-2'yi incelediğimizde mantıksal olarak Example-1 ile tamamen aynıdır. Burada “synchronized” yapısını method içine taşıdığımızda thread-1 “getTasksFromDB” methoduna erişim sağlarken aynı anda thread-2 “addTAsksToqueue” methoduda erişim sağlayabilecek. Burada aynı anda birden fazla thread’e erişim yetkisi vererek coarse-grained yapısını bozduğumuzu düşünebilirsiniz ama bozmuyoruz. Çünkü “synchronized” YAPISI OBJE SEVİYESİNDE UYGULANAN BİR YAPIDIR yani thread-1 sekiz numaralı satıra girdiğinde(ki burası critical section olarak geçer bu konudan bir önceki yazıda bahsetmiştik) thread-2 on dört numaralı satırda thread-1'in işinin bitmesini bekleyecektir çünkü lock mekanizmasını “this” keywordünü kullanarak aynı obje üzerine attık ve ilk giren thread işini bitirene kadar diğer thread’ler bekleyecektir(sleeping).
- Peki farklı objeler üzerinde lock atarsak ki Example-3 tam olarak bununla alakalı; tam olarak fine grained stratejisini uygulamış olacağız. Thread-1 sekiz numaralı satırda işlemini yaparken aynı anda thread-2 on beş numaralı satıra(critical section) ‘ a beklemeden girebilecek ve tam anlamıyla paylaşılan kaynaklarımız üzerinde concurrent erişime izin vermiş olacağız.
Reentrant ve Locks API
Reentrant mantıksal olarak “synchronized” yapısıyla aynı çalışan fakat daha fazla yeteneklere sahip olan sınıftır. Yine örnekler üzerinden buradaki kilitleme mekanizmasını da incelemeye çalışalım.
- Herhangi bir paylaşılan kaynağımızı concurrent access’e karşı korumak istiyorsak Example-4'te de görüleceği üzere Reentrantlock yapısını kullanabiliriz. Burada kaynağımıza birden fazla thread erişmek istediğinde eğer critical section üzerinde lock yoksa(burada 14. satırdan başlayıp 16. satırda bitiyor) thread’lerden birisi critical section üzerine lock koyarak işini yapıyor ve işi bittikten sonra critical section bloğu üzerindeki lock’ı “finally” bloğunda kaldırıyor böylece bir başka thread paylaşılan kaynağı kullanabiliyor. Bu yapıda bize paylaşılan kaynak üzerinde thread-safe yapıda işlem yapmamıza olanak sağlıyor. Ayrıca buradaki tryLock methodu bize eğer critical sectionda çalışan bir thread varsa lock’ı isteyen thread’leri 10 saniyeliğine bekletip tekrar lock’ı istemelerini sağlıyor.
- Reentrant bize gelişmiş şekilde lock mekanizmaları da sunar. Example-5'te de görüldüğü üzere read ve write lockları bulunmaktadır. Örneğin read lock kullanıyorsak; buradaki critical section bloğuna birden fazla thread’in aynı anda erişmesinde sorun yoktur çünkü read operasyonları shared resource üzerinde değişiklik yapılmadığından concurrent olarak gerçekleşebilir.Fakat aynı durum write lock için geçerli değildir çünkü paylaşılan kaynak üzerinde write veya remove işlemleri aynı anda tek bir thread tarafından yapılmalıdır.(Race condition’a sebep olmamak için) Bu yüzden write lock’ta critical section bloğuna aynı anda sadece tek bir thread bulunabilir.
- Yukarıdaki örneklerin yanı sıra reentrant bize birçok sorgulama methodlarıda sağlar. Bunlardan bazıları aşağıdaki gibidir.
tryLock(): Verilen bekleme süresi içinde başka bir thread tarafından tutulmazsa ve mevcut thread kesintiye uğramadıysa lock’ı alır.
isLocked(): Lock herhangi bir thread tarafından tutulup tutulmadığını sorgular.
isHeldByCurrentThread(): Mevcut thread tarafından lock’ın tutulup tutulmadığını sorgular.
getOwner(): İlgili lock’a sahip olan threadi geriye döndürür eğer sahip değilse null değerini döndürür.
getQueuedThreads(): Critical section’a girmek için ilgili lock’ı almayı bekleyen thread koleksiyonunu döndürür.
hasQueuedThreads(): Critical section’a girmek için bekleyen thread var mı diye sorgular.
Reentrant Vs Synchronized
Son olarak Reentrant ve synchronized lock mekanizmalarını kıyaslayalım.
- Synchronized fairness’ı desteklemezken Reenrant desteklemektedir. Synchronized mekanizmasında herhangi bir thread lock’ı alabilir ve bekleyen threadler arasında herhangi bir tercih belirtilmez fakat ReentrantLockta ise lock’ı oluştururken fairness özelliğini belirtelek çekişme durumunda en uzun bekleyen thread’e lock’ı verebiliriz.
- Diğer bir fark ise reentrant lock’ta bulunan tryLock() yöntemidir. ReentrantLock yalnızca mevcutsa veya başka bir thread tarafından lock tutulmuyorsa; lock’ı alan tryLock() yöntemini sağlar.
- Önemli farklardan biriside lock’ı almayı beklerken thread’i kesme yeteneğidir. Syncronized keyword’u kullanarak oluşturulan lock mekanizmasında bekleyen thread’ler üzerinde bir kontrolümüz yok iken ReentrantLockta lock’ı beklerken thread’i lockInterruptably() yöntemi ile kesebiliriz. Benzer şekilde tryLock yapısı ilede thread’leri belirli bir timeout ile bekletebiliriz.(Example-4)
- Reentrant ayrıca bize yukarıda örneklerini verdiğimiz birçok sorgu methodlarını sunar.
Bir sonraki yazımda ise semaphore, inter-thread communication, producer-consumer yapısından bahsediyor olacağım. Takipte kalınız :)