Java Thread - 2

Merhaba, Thread ile ilgili ilk yazıda ne olduğundan, nasıl oluşturulduğundan ve yaşam döngüsünden bahsetmiştik. Bu yazıda da multithread bir uygulama yazarken karşılaşabileceğimiz durumlar ve kavramlara değineceğiz. Umarım faydalı bir yazı olur, keyifli okumalar 🙋‍♀️

💡 Birden fazla thread in eş zamanlı çalıştığı durumda çalışma sıralamasına göre farklı sonuçların çıkabileceği kod bölümü Critical Section olarak adlandırlır. Critical Section; verinin tutarlılığını korumak için senkronize edilmesi gereken değişken ve/veya kaynakları içerir.

💡 Critical Section üzerinde threadlerin farklı sıra ile çalışmasından kaynaklanan bu eşzamanlılık (concurrency) problemine Race Conditions denir. Eş zamanlı programlamada bir thread verinin değerini değiştirdiği anda başka bir thread okumaya çalıştığı durumda elde edilen sonuç tutarsız ve güvenilmezdir.

Programlama dünyasında 2 çeşit race condition türü vardır;

  • Read-modify-write; 1 veya daha fazla thread aynı anda bir veriyi alıp okuduğu ve arından veriyi güncelleyip tekrar yazdığı durumda ortaya çıkar. Sıra ile yapılan işlemler; her thread için memory den değişken değerini alır register alanına kaydeder, veri üzerinde işlem yapar, registerdan memory e tekrar gönderme işlemini yapar. Bu adımlar sırasında iki thread aynı anda değişken değerini okumaya çalıştığında ikiside aynı değeri görür ve onun üstüne ekleme yaptığında sonuç tutarsız olur ve beklediğimiz doğru değeri göremeyiz. Aşağıdaki resimde bu koşulu göstermektedir.
https://www.rapitasystems.com/blog/race-condition-testing

Aşağıdaki kod üzerinden örnek verecek olursak; ilk thread amount değerini 100 e güncellendi, ardından diğer thread aynı değeri 300 e çekti fakat üçüncü thread 400 değerini toplama eklemek için geldiğinde bu değeri görmedi ve 100 üzerine ekleme yaptı sonuçda 100+400 = 500 değerini görürüz fakat bu beklediğimiz sonuç değildir.

public class Account {
long amount = 0;

public void add(long value){
this.amount += value;
}
}
  • Check-then-act; Birden fazla thread in aynı anda koşulu kontrol edip program adımlarına devam edildiği durumda ortaya çıkar. Örneğin bir boolean değer kontrolü olduğunu varsayalım iki thread in aynı anda true değerini okuduğunu ve birinin işlem yaptığını düşünelim, bu durum diğer thread in o koşul üzerinde yanlış hareket etmesine neden olabilir.

Rase Condition durumunun nasıl önüne geçebiliriz, alabileceğimiz önemler nelerdir ❓

Bu durumu tespit etmek oldukça zordur, herhangi bir hata çıktısı üretilmez, kod içerisindeki bir çok noktadan kaynaklanabilecek mantıksal bir durumdur. Bu yüzden en baştan kodu tasarlarken bunun önüne geçebilecek şekilde ilerlenmelidir.

💥 Thread ler arasında paylaşılan objelerin değiştirilmez olmasını sağlayarak yani hiç bir thread in o objeyi değiştirmeyeceğini garantilersek thread safe hale getirebiliriz. Bu yapı Immutable olarak adlandırlır. Örneğin Java da String sınıfı immutable dır. Bu yüzden değeri değiştirilemez ve multi thread yapılarda güvenilirdir. Her thread tek bir string objesine erişir. Aşağıda String sınıfında yer alan concat fonksiyonuna bakarsak işlemleri yaptıktan sonra döndürdüğü aslında yeni bir String objesidir.

public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}

Kendi oluşturduğumuz sınıflarda da bunu set fonksiyonu olmadan obje ilk oluştuğundaki değeri hep saklayacak şekilde yapabiliriz. Yada işlem yapmamız gerekiyorsa String sınıfında olduğu gibi yeni bir obje return ederek sağlayabiliriz. Mümkün olduğunca her zaman değişmez objeleri kullanmak tavsiye edilir.

💥 Thread lerın hangi değerleri paylaştığından bir önceki yazıda bahsetmiştik, bu şekilde paylaşılan objelerden kaçınılarak rase condition önüne geçebiliriz. Örnek verecek olursak sınıf değişkenleri thread ler arasında paylaşılan verilerdir o yüzden mümkün oldukça multithread çalışacak yapılarda bunlardan kaçınabiliriz.

💥Critical section içerisin de atomik işlemler yapıldığından emin olmamız gerekiyor. Bir thread bu alanda çalışırken başka thread ilk gelen thread işini bitirine kadar onu çalıştıramaz. Bu süreç aslında thread senkronizasyonu olarakda adlandırılıyor. Kod parçasını aynı zamanda sadece tek bir thread in çalıştırmasına izin veren yapıyı kurarak da race condition dan kaçınabiliriz.

Threadleri senkronize etmek için kullanılan yapılardan bahsedelim Java da bunu sağlayan ilk mekanizma Synchronized keyword ile gelmiştir. Bu keyword fonksiyonlarda, statik fonksiyonlarda ve kod blokları üzerinde kullanılabilmektedir.

public class Account {
long amount = 0;

public synchronized void add(long value){
this.amount += value;
}
}

Bu şekilde add fonksiyonunun aynı anda sadece bir thread tarafından çalışmasını garantileriz.

💥 Bazı durumlarda bütün metodu senkronize etmek istemeyebiliriz fonksiyon içindeki sadece ilgili kısmı senkronize etmek içinde aşağıdaki gibi yapı kullanılıyor. synchronized içerisine this parametresi geçiliyor bunun anlamı izleyeceği objeyi referans etmesidir. Yani izleyeceği obje üzerinde aynı anda yalnızca bir thread çalışabilir. Eğer static bir fonksiyon ise this yerine class değeri verilebilir.

public synchronized void add(long value){
synchronized (this){
this.amount += value;
}

}

Synchronized kullanıldığında thread bu bloğa girerken kilitler yani başka threadler buraya giremez, sonunda da kilidi kaldırır. Bu şekilde aynı anda bir kod/veri üzerinde tek bir thread çalışmış olur ve race condition durumuna yakalanmayız.

Synchronized keywordunu fonksiyonlar üzerinde kullanmaktansa bloklar için kullanmayı tercih etmeliyiz. Fonksiyonlar üzerinde kullandığımızda bütün objeye kilit koyulur yani bir thread bu metodu çalıştırmaya başlandığında diğer threadler obje içerisindeki diğer synchronized metodları da kullanamaz haldedir. Fakat blok senkronize edildiğinde thread sadece o blokta iken kilit koyulur. Yani objenin kalan kısımları diğer threadler tarafından kullanılabilir. Bu yüzden blok üzerinde synchronized kullanmak threadlerin daha az birbirini beklemesini sağlar.

Benzer bir durumda paylaşılan değişkenler için bulunmaktadır. Main memory den alınan değer CPU cache üzerinde saklanır, thread lerde buradan ilgili değeri okur ve ardından değiştirilen değer tekrar main memory e aktarılır. Bu durumda birden fazla thread çalışırken CPU cache üzerinden alınan değerin doğruluğu garantilenemez. Bu problem için değişkenler üzerinde volatile keyword kullanılır. Volatile olarak tanımlanmış bir değişkenin okunmadan önce main memory den getirilmesi garantilenmiş olur.

public class Account {
public volatile long amount = 0;
}

Volatile kullanmak veriyi her seferinde cache den değil main memoryden getireceği için performans açısından oldukça maliyetlidir bu yüzden sadece gerçekten ihtiyaç olduğu durumda kullanmak tavsiye edilir.

--

--