Java Thread -(Başlangıç)

Merhaba, Thread serisini en baştan ileri konulara doğru ilerletmek istiyorum. O yüzden ilk olarak ne olduğunu tanımlayarak başlayalım. Umarım faydalı bir yazı olur, keyifli okumalar 🙋‍♀️

💥 Process üzerinde birden fazla işin aynı anda yapılmasına olanak sağlayan yapılara thread denir. Process nedir dersek; bir uygulamayı çalıştırmaya başladığınızda temelde bir process oluşmuş olur. Somut bir örnek verecek olursak elinizde bir jar uygulaması olduğunu düşünelim bunu java -jar ile çalıştırdığınızda bir process oluşturmuş olursunuz. Eğer linux üzerinde çalışıyorsanız ps -ef komutu ile çalışan process ve id lerine erişim sağlayabilirsiniz. Default olarak bir process tek bir thread den meydana gelmektedir ve buna main thread denir. Bu senaryoda uygulamanız Single Thread çalışan bir uygulama olarak adlandırılabilir.

Alttaki resimde de görüleceği gibi threadler heap alanını ortak olarak kullanır. Yani thread lerin process ler gibi kendi bellek alanı bulunmaz. Bir process altındaki bütün thread ler aynı kod, data ve dosya kaynaklarını kullanır. Multithread bir uygulama yazarken bu bilgi sürekli göz önünde bulundurulmalıdır. Örneğin bir sınıf değişkeni oluşturduğunuzda bu heap üzerinde saklanır yani bütün thread ler aynı değişken değerini kullanılır.

https://www.backblaze.com/blog/whats-the-diff-programs-processes-and-threads/

Uygulama içerisinde sıra ile tüm işlemlerin yapılması gerekiyor ise tek bir process ve altında main thread ile süreç yürütülebilir. Fakat çoğu durumda bu yeterli olmayacaktır. Ana thread çalışırken eş zamanlı yürütülmesi gereken işler olduğu durumda ne yapılmalı? Arayüz üzerinden örneklendirirsek bir web uygulamamız var ve kullanıcı butona bastığında uzun sürecek bir işlemi tetikliyor bu durumda sadece ana thread üzerinden gidersek kullanıcı işlem bitene kadar arayüz üzerinde herhangi bir işlem yapamaz halde olacaktır. Bu genelde istenmeyen bir durumdur.

Cevap beklemeyeceğimiz asenkron durumlarda da thread kullanmak mantıklıdır, bu şekilde ana thread uzun süre meşgul edilmez. Neden thread oluşturma ihtiyacımız olacağı kısmından behsettikten sonra sırada bunu nasıl gerçekleştireceğimiz var. Java da bir thread nasıl oluşturulur ❓

  1. Thread sınıfından subclass oluşturup run fonksiyonunu override ederek; run fonksiyonu thread in başladığında hangi işlemleri yapacağını belirler. Thread start() fonksiyonu ile başlatılır, eğer run() fonksiyonu kullanılırsa ana thread diğer thread i beklemek zorunda kalır, buda çoğu zaman istemediğimiz bir durumdur.
  2. Runnable interface sini kullanarak; Runnable interface run adında tek bir metod sahiptir. Bu interface implement eden sınıf oluşturarak thread oluşturabiliriz. Yada tek bir fonksiyona sahip interfacelerde Java8 ile birlikte lambda ile implement edebiliyorduk, bu şekilde oluşturabiliriz;

Runnable rn=()->{ …….. };

Bu iki yöntem arasında hangisinin daha iyi olduğu ile ilgili bir kural yok. Kişisel tercihe ve implement edilecek modele göre uygunluğuna karar verilebilir.

Thread oluşturduktan sonra aşağıdaki gibi bir yaşam döngüsü bulunmaktadır.

http://www.btechsmartclass.com/java/java-thread-model

Bir thread ilk oluşturulduğunda New durumundadır yani henüz başlatılmamıştır. Çalışmaya hazır olduğunda Runnable durumuna gelir. Bu durumda thread çalışıyor olabilir yada zaman planı yapılmış ve çalışmaya hazır halde olabilir. Thread geçici olarak çalışmayı durduğunda Blocked duruma geçer. Bu bloklanma yada bekleme senaryoları için geçerli olabilir. Dead/Terminated durumuna ise normal olarak çalıştırması gereken kodu bitirdiğinde yada çalışma sırasında beklenmeyen bir exception alındığı durumda geçer.

Şuana kadar gördüğümüz kısımları küçük bir örnek üzerinden inceleyelim; MinusThread adında Thread sınıfından türemiş yeni bir sınıf oluşturuyoruz. Bu sınıfta yapılan işlem parametre olarak gelen sayıdan toplam sayıyı çıkarmak.

public class MinusThread extends Thread{

Number number;
int size;
String status;

public MinusThread(Number number, int size, String status) {
this.number = number;
this.size = size;
this.status = status;
}

@Override
public void run() {
number.minus(status,size);
System.out.println(" state2 : " + this.getState());
}
}

Main fonksiyonumuzun çıktısı aşağıdaki gibi olacaktır. Thread new ile oluşturulduktan sonra henüz başlatılmadığı için new olarak ilk açıklama konsola yazılıyor. Daha sonra thread üzerinde start fonksiyonu çalışıyor ve ikinci açıklama runnable olarak konsola yazıyor. Ana thread sleep fonksiyonu ile bekletiliyor ki c1 threadi işini bitirip terminated durumuna geçtiğini görüntüleyebilelim. Eğer ana thread bekletilmez ise state3 yorumunda da halen runnable olarak görüntüleriz.

public static void main(String[] args) throws InterruptedException {
Number number = new Number(1000);

MinusThread c1 = new MinusThread(number, 100," Calculate 1 ");
System.out.println( " state1 : " + c1.getState()); // NEW
c1.start();
Thread.sleep(1000);
System.out.println( " state3 : " + c1.getState()); //TERMINATED
}

state1 : NEW
Calculate 1–900
state2 : RUNNABLE
state3 : TERMINATED

Bazı zamanlarda ana thread i bekletip oluşturduğumuz thread in önce işini bitirmesini isteyebiliriz, örneğin thread den bir veri bekliyorsak yada adımlara devam etmek için işini tamamlaması gerektiği durumlar olabilir. Bu durumlarda Join fonksiyonunu kullanabiliriz. Bu fonksiyon ile önce start edilen thread işini bitirir ardından adımlara devam edilir. Yani ana process bu thread ölene kadar bekler.

Örneği incelersek toplam sayımız 1000 den çıkarma işlemi yapıyoruz ilk önce c1 thread işlemini yapıp bitiyor bunu c1.join() ile garantilemiş oluyoruz. Aksi durumda thread ler üzerinde herhangi bir ayarlama yapılmazsa hangisinin çalışacağının yani sıralamanın bir garantisi olamaz.

public static void main(String[] args) throws InterruptedException {
Number number = new Number(1000);

MinusThread c1 = new MinusThread(number, 100," Calculate 1 ");
MinusThread c2 = new MinusThread(number, 200," Calculate 2 ");
MinusThread c3 = new MinusThread(number, 300," Calculate 3 ");
c1.start();
c1.join();
c2.start();
System.out.println( "state join den önce : " + c2.getState());
c2.join();
System.out.println( "state join den sonra : " + c2.getState());
c3.start();
c3.join();
}

Çıktıya bakarsak join den önce c2 thread durum runnable iken sonrasında terminated durumuna gelmiştir. Her thread birbirini beklediği içinde 1000–100–200–300 sonucunda 400 toplam miktarı doğru bir şekilde hesaplandı.

Calculate 1–900
Calculate 2–700
state join den önce : RUNNABLE
state join den sonra : TERMINATED
Calculate 3–400

Eğer join fonksiyonu kullanmazsak thread ler rastgele çalışıp Number sınıfı üzerindeki total değişkeni güncelleyeceğinden dolayı her çalıştırığımızda çıktı farklı olacaktır fakat bir tane örnek vereceksek aşağıdaki gibi olabilir;

Calculate 1–700
Calculate 3–400
Calculate 2–700

💥 Aynı anda birden çok thread tarafından çağrılması güvenli olan kod parçası Thread Safety olarak adlandırılır. Güvenli kavramını biraz daha açalım; yukarıda ki örneğimizde birden fazla thread aynı fonksiyonu çalıştırdığında değişken üzerinde güncelleme yapan bir thread bulunuyorsa diğeri yanlış veriyi okur, işte bu durum güvensizdir. Bu yüzden thread lerin çalışırken hangi kaynakları paylaştığını bilmek önemli bir konudur.

  • Lokal Değişkenler(local variable); Her thread lokal değişkenleri kendi stack alanında tutar yani diğer threadler ile paylaşılmaz. Bu yüzden lokal değişlenler thread safe olarak tanımlanır.
  • Sınıf Değişkenleri(class member); Objelerle birlikte obje değişkenleri de heap üzerinde tutulur. Heap thread ler tarafından paylaşıldığı için sınıf değişkenleri thread safe değildir.
  • Lokal Obje Referansları(local object referance); Objeler heap alanında saklanır fakat fonksiyon içerisinde oluşturulmuş bir obje referansı thread stack alanında tutulacağından thread safe olarak tanımlanır.

Java collection yapısından örnek verecek olursak Vector and Hashtable yapıları thread safe iken diğer List, Set gibi yapılar değildir. İhtiyacınıza göre kullanacağınız sınıflarda bu özelliğin olup olmamasına dikkat edilmelidir.

Referans;

https://jenkov.com/tutorials/java-concurrency/index.html

--

--