17.Threadlerin Senkronizasyonu

Threadlerin Senkronizasyonu

Bilindiği gibi Thread akışının ayrı bir stack alanı ve ortak bir heap alanı bulunmaktadır. Buna göre bir thread akışı içerisinde bulunan yerel değişken farklı 2 akış tarafından ortak olamaz, yani bir metod için yerel değişkenler her thread için farklı yaratılmaktadır.

Threadlerdeki tipik problem ortak kullanılan alanın senkronize edilmesidir. Bir akış içerisinde senkronize edilmesi gereken bölgeye genel olarak "Critical Section" denilmektedir veya örneğin 2 tane akış bir veri yapısı üzerinde okuma ve yazma yapıyor olabilir. Bir akış bir thread yazma yaparken diğer Thread'in aynı veri üzerinde okuma yapması problem oluşturabilir. İşte senkronizasyon bu tip problemlerin çözümünde faydalı olan kavramdır. Daha önce anlatılan join() ve get() gibi metodlarda çok basit senkronizasyon işlemlerinde kullanılabilir. Ancak her türlü problemi çözemez.

Bir metodun çağrılması noktasında bir daha başka bir akışın o metodu çağramaması durumuna Thread-Safe durum denmektedir. Örneğin ++ gibi bir işlem birden fazla makina komutu ile yapılmaktadır. Öyleyse bir değişkeni birden fazla Thread aynı anda kullanıyor ise bu durumda ++ işlemi tamamlanmadan diğer Thread araya girebilir ve değerler karışabilir. Böylesi durumlara "Race Condition" denilmektedir. Öyleyse böyle bir işlemin atomik yani bir akış bitirmeden diğerinin işlem yapamaması şeklinde bitirilmesi gerekir.

/*----------------------------------------------------------------------------------------------------------------------
    Threadlerin senkronizasyonu
----------------------------------------------------------------------------------------------------------------------*/
package org.csystem;

import java.util.Random;

class Sample {
    public int a;     
}

public class App {


    public static void main(String[] args)  
    {            
        Sample s = new Sample();

        Runnable r = () -> {
            try {
                for (int i = 0; i < 3; ++i) {
                    Thread.sleep(1000);
                    s.a++;
                    System.out.printf("%s:%d%n", Thread.currentThread().getName(), s.a);
                }
            }
            catch (InterruptedException ex) {

            }
        };

        Thread t1 = new Thread(r, "Thread-1");
        Thread t2 = new Thread(r, "Thread-2");

        t1.start();
        t2.start();            
    }    
}

Bir kritik bölgenin bir Object ile senkronize edilebilmesi için synchronized bloğuna alınması gerekebilir. synchronized bloğu bir object ile senkronize edilir. Bu objectin türünün önemi yoktur. Akış synchronized bloğuna geldiğinde synchronized bloğu içerisindeki objecti kullanan diğer akış bir önce giren akış bitirmeden bu bloğa giremez. synchronized bloğu içerisindeki object aslında bu bloğun aynı objecti kullanan akışlar tarafından senkronize bir biçimde kullanılmasını sağlar.

 /*----------------------------------------------------------------------------------------------------------------------
    syncronized bloğu
----------------------------------------------------------------------------------------------------------------------*/
package org.csystem;

class Sample {
    public int a;     
}

public class App {


    public static void main(String[] args)  
    {            
        Sample s = new Sample();

        Runnable r = () -> {
            try {
                for (int i = 0; i < 3; ++i) {
                    Thread.sleep(1);
                    synchronized (s) {
                        s.a++;
                        System.out.printf("%s:%d%n", Thread.currentThread().getName(), s.a);
                    }                    
                }
            }
            catch (InterruptedException ex) {

            }
        };

        Thread t1 = new Thread(r, "Thread-1");
        Thread t2 = new Thread(r, "Thread-2");

        t1.start();
        t2.start();            
    }    
}
/*----------------------------------------------------------------------------------------------------------------------
    syncronized bloğu
----------------------------------------------------------------------------------------------------------------------*/
package org.csystem;

class Sample {
    public int a;     
}

public class App {

    public static void main(String[] args)  
    {            
        Sample s = new Sample();

        Runnable r = () -> {
            try {
                for (int i = 0; i < 3; ++i) {
                    Thread.sleep(1);
                    synchronized (s) {
                        s.a++;
                        System.out.printf("%s:%d%n", Thread.currentThread().getName(), s.a);
                    }        
                }
            }
            catch (InterruptedException ex) {

            }
        };

        for (int i = 0; i < 10; ++i) {
            new Thread(r, "Thread-" + (i +1)).start();
        }        
    }    
}

Kritik kod oluşturmak için Lock isimli bir arayüzden da faydalanılabilir. Bu sınıf Java'ya 1.5 versiyonu ile eklenmiştir. Bu arayüzü ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock isimli sınıflar implemente etmiştir. ReenteredWRLock sınıfı ayrıca ReadWriteLock arayüzünü de implemente etmektedir. Buna göre kitleme işlemi temel olarak aşağıdaki adımlardan geçilerek yapılabilir.

  1. Lock arayüzü referansına ilişkin bir nesne elde edilir. Bu Lock nesnesi tüm threadler için ortak olan nesne olmalıdır.
  2. Akış kritik koda gelmeden hemen önce lock() metodu ile kritik koda başlanmalı ve unlock() metodu ile kritik kod sonlandırılmalıdır. Bu işlem tipik olarak try bloğunda lock() çağrılarak ve finally bloğunda unlock() çağrılarak yapılabilir.

Kilitleme işlemi yazma ve okuma olmak üzere 2 şekilde yapılabilir. Yazma için istenen kilitleme nesnesi ReadWriteLock arayüzünün read() metoduyla elde edilebilir. Benzer şekilde write yazma için olan WriteLock nesnesi ise write() metoduyla elde edilebilir. Okumayı kitleme işlemi eğer tüm kritik koda giren Threadler okuma olarak giriyor ise aynı anda ele alınabilir ancak yazma ile kilitleme işleminde kritik kod mutlaka bir tanesinin çalışması şeklinde olmaktadır. Programcı bunlara dikkat ederek kod yazmalıdır. Lock sınıfı bir çok işletim sınıfının da desteklediği mutex(mutual exlusion) çekirdek nesnelerine kullanımı anlatımında benzetilebilir. Lock arayüzü ve kullanılan diğer arayüzler ve sınıflar java.util.concurrent.locks isimli paket altındadır.

/*----------------------------------------------------------------------------------------------------------------------
    Lock arayüzü ve ReentrantReadWriteLock
----------------------------------------------------------------------------------------------------------------------*/
package org.csystem;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Sample {
    public int a;     
}

public class App {

    public static void main(String[] args)  
    {            
        Sample s = new Sample();

        Lock lock = new ReentrantReadWriteLock().writeLock();

        Runnable r = () -> {

            try {                
                for (int i = 0; i < 3; ++i) {
                    Thread.sleep(1);
                    try {
                        lock.lock();
                        s.a++;
                        System.out.printf("%s:%d%n", Thread.currentThread().getName(), s.a);
                    }
                    finally {
                        lock.unlock();
                    }

                }
            }
            catch (InterruptedException ex) {

            }

        };

        for (int i = 0; i < 10; ++i) {
            new Thread(r, "Thread-" + (i +1)).start();
        }        
    }    
}

Lock arayüzünün tryLock() isimli metodu hem kilitleme işlemini yapar hem de akış bu noktada giremezse ise false değeri ile geri döner, girerse true değeri geri döner. Arayüzün tryLock() metodu bir akış writeLock olarak alınmamışsa onu ReadLock olarak kitleme amacıyla kullanılır. tryLock() metodunun parametreli versiyonu belirli bir süre beklemek için kullanılmaktadır. tryLock() metodu bekleme birimi için Executor sınıflarda da kullanılan TimeUnit isimli enum parametre almaktadır. Senkronizasyon işlemi için Object sınıfının wait() ve notifyXXX() metodları da kullanılabilmektedir. Fakat bu metodlar java.util.concurrent paketinden sonra yani Java 1.5'ten sonra çok tercih edilmemektedir.

Bazen akışısın bir kısmı yazma için bir kısmı da okuma için kitleniyor olabilir, bu durumda örneğin readLock.lock(), writeLock().lock gibi bir kilitler açması ters sırada kapatılmalıdır, yani önce readLock.unlock() denilecek sonra writeLock.unlock() denilecek.

Anahtar Notlar: Lock arayüzü kullanılarak, oluşturulan akışlarda bazı Threadler okuma bazı Threadler yazma yapıyorsa (üretici-tükeci thread) bu durumda okuma yapan kod mutlaka okuma kilitlenmesiyle kilitlenmelidir, çünkü okuma kilitlenmesi eğer kritik kodda o an bir okuma kilitlemesiyle işlem yapılıyorsa, başka bir okuma kilitlemesi akışa girebilir, bu durumda performans biraz daha yükseltilebilir.

Collection Sınıflarının Senkronizasyonu

Anımsanacağı gibi Collection sınıflar colleciton sınıfları thead güvnli olmaması sınıfa göre değişebilmektedir. Genel olarak Collection sınıflar Thread güvenli değildir. Örneğin liste tarzı bir Collection'da ekleme yapan bir metod Thread güvenli değilse bu durumda ekleme işlemi birden fazla akış için aynı Collection nesnesine problem oluşturabilecektir. Bunun için en ilkel yöntem ekleme sırasında oluşan kodu senkronize etmektir. Ancak Thread-safe olmayan Collectionlar için Collection sınıfının syncrohnizedXXX metodları ile senkronize edilmiş bir Collection sınıfı elde edilebilmektedir. Java'da ilk çıkan Collection sınıflar senkronize olarak çıkartılmıştır, daha sonrakilerin ise geneli Thread güvenli değildir.

Aşağıdaki kodda bir ArrayList'in senkronize olmuş versiyonu elde edilmektedir.

/*----------------------------------------------------------------------------------------------------------------------
    Collections sınıfının synronizedXXX metotları
----------------------------------------------------------------------------------------------------------------------*/
package org.csystem;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Sample {
    public static int a;
}

public class App {

    public static void main(String[] args)  throws InterruptedException
    {        
        List<String> list =  Collections.synchronizedList(new ArrayList<String>());


        Runnable r = () -> {    
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            for (int i = 0; i < 3; ++i) {                
                synchronized (list) {
                    list.add(Thread.currentThread().getName() + "-" + Sample.a);

                    ++Sample.a;
                }
            }

        };

        Thread [] threads = new Thread[3];

        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread(r, "Thread-" + (i + 1));
            threads[i].start();            
        }        

        for (int i = 0; i < threads.length; ++i)
            threads[i].join();

        for (String s : list)
            System.out.println(s);
    }    
}

Anahtar Notlar: Programcı kritik bölgeleri oluştururken özellikle çok fazla Threadli uygulamalarda kritik bölgedeki kilitlemelerin hemen açılacak şekilde tasarımını yapmalıdır. Çünkü bu durumda eğer çok fazla uzatılırsa diğer Threadler beklemek zorunda kalır ve işlem gecikir.

results matching ""

    No results matching ""