Java’da Multithreading – Bölüm 3: “synchronized” Anahtar Kelimesi

İkinci bölümde threadlerin senkronizasyonu sırasında karşılaştığımız zorlukların birincisinden bahsetmiştik. Bu bölümde bu zorlukların ikincisini ve onu aşmak için Java’nın bize sağladığı “synchronized” anahtar kelimesini kullanmayı örnekleyeceğiz. Önce zorluğun/problemin ne olduğunu yine bir örnek ile açıklamaya çalışalım:

public class Application {

    private int count = 0;

    public static void main(String[] args) {
        Application application = new Application();
        application.doCount();
    }

    private void doCount() {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            }
        });

        thread1.start();
        thread2.start();

        System.out.println("Sayaç: " + count);
    }

}

“Application” sınıfının “count” (sayaç) adlı, başlangıç değeri 0 olan tam sayı tipinde bir örnek değişkeni vardır. “main” metodu içinde bir “Application” nesnesi oluşturarak “doCount()” çağrılır. “doCount()” metodu içinde, “run()” metodu içerisinde “count” değişkenini 10000 kere arttıracak birer döngü içeren iki thread oluşturulur ve başlatılır. Metodun sonunda ise “count” değişkeninin son değeri yazdırılır. Buraya kadar her şey açık. Ancak bu programı çalıştırdığımızda bizi bir sürpriz beklemektedir. Program sonlandığında konsolda 20000 görmeyi bekleyenlerimiz olacaktır. Evet bazen 20000 görebiliriz de. Ancak 511 gibi, 10987 gibi saçma sayılar gördüğünüzde siz de benim gibi şaşırmışsınızdır. Sebebi şudur: “thread1” ve “thread2” ayrı threadler oldukları için “start()” metotlarıyla başladıktan sonra ana thread, yani bu iki threadi başlatan o anki geçerli thread işleyişine devam eder ve “count” değişkeninin anlık değerini konsola yazdırır. Bu sırada “thread1” ve “thread2” sonlanmamış olabilecekleri için sayacın son değeri bazı durumlar için 20000 olamayabilir. Bu durumu çözmek için bu iki threadin “join()” metodunu çağırarak threadlere, çalışmaları bitene kadar “geçerli threadi” bekletmelerini söyleyebiliriz:

private void doCount() {

    ...

    thread1.start();
    thread2.start();

    try {
        thread1.join();
        thread2.join();
    } catch (InterruptedException e) {
    }

    System.out.println("Sayaç: " + count);
}

Programı çalıştırdığımızda ikinci sürpriz karşımıza çıkar. Programı her çalıştırdığımızda 20000 görmeyi beklerken, yine bazen 20000 görmekle birlikte, 14180 gibi, 12460 gibi, 19422 gibi sonuçlar da görürüz. Fakat bu sefer 511 gibi küçük rakamlar göremezsiniz. “join()” çağrısı ile geçerli threadi bekletmek o kadar da yavan bir çözüm değildir. Ancak sorunlarımızdan yalnızca birini çözmektedir. Sorunu anlamak için biraz daha yukarılara bakmamız gerekiyor; “run()” metodunun içine… Sorun threadlerin eş zamanlı çalışmasından kaynaklanmakta. Ama nasıl olur, amacımız threadleri eş zamanlı çalıştırmak ve böylece performans artışı sağlamak değil miydi zaten. Threadlerin bu güçlü yanı neden şimdi karşımıza “sorun” olarak çıktı? Çünkü “kontrolsüz güç güç değildir.”

“run()” metoduna, “for” döngüsünün gövdesine baktığımızda şu satırı görmekteyiz:

count++;

Bildiğiniz üzere bu kod parçası bir söz diziminden ibarettir. Asıl karşılığı ise şu şekildedir:

count = count + 1;

Burada yapılan işlem iki adımlıdır: 1) “count” değerini oku ve bir ekle, 2) sonucu “count” değişkenine ata. Bu işlem çok çok hızlı gerçekleşmektedir. Yine de eş zamanlı çalışan threadler söz konusu olduğunda bu hız yetersiz kalabilir. Örneğin:

  1. “thread1” “count”un değerini 100 olarak okur ve ona 1 ekler. Yani 101 sonucuna ulaşır.
  2. “thread2” de aynı anda “count”un değerini 100 olarak okur ve ona 1 ekler. Yani o da 101 sonucuna ulaşır.
  3. “thread1” bulduğu sonucu, yani 101’i “count” değişkenine yazar.
  4. “thread2” bulduğu sonucu, yani 101’i “count” değişkenine yazar.
  5. Sonuçta değeri 2 artması gereken “count” değişkeninin son değeri “101” olmuş olur, yani yalnızca 1 artmış olur.

Bu gibi nedenlerle “count” değişkeninin değerini arttırma işlemi aynı zamanda yalnızca bir threadin yapabileceği şekilde gerçekleştirilmelidir… Java’nın imdadımıza koşan anahtar kelimesi bu defa “synchronized” oluyor. Birinci adım olarak “count” değişkeninin değerini arttırma işlemini “Application” sınıfının bir metodu olarak ifade etmekle başlayalım ve anonim “Runnable” gerçekleştirimlerimiz içindeki “count++” satırlarımızı bu metodu çağıracak şekilde düzenleyelim:

public class Application {

    private int count = 0;

    ...

    public void increment() {
        count++;
    }

    private void doCount() {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increment();
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increment();
                }
            }
        });

        ...
    }

}

Sonra da bunu bir adım daha ileri götürerek, “synchronized” anahtar kelimesi yardımıyla, “increment()” metodunun aynı anda yalnızca bir thread tarafından çağrılabileceğini garanti edelim:

public synchronized void increment() {
    count++;
}

Böylece “count” değişkeninin değerini arttırma işlemini aynı anda yalnızca bir threadin gerçekleştirebilmesi kısıtıyla gerçekleştirmiş olduk. Bir thread “increment()” metodunu çağırdığında, o thread için işlem bitene kadar “increment()” metodunu çağırmak isteyen diğer tüm threadler bekler. Buna İngilizce terim olarak “intrinsic lock” yani yerleşik/içsel kilit mekanizması denmektedir. Artık programı her çalıştırdığımızda istisnasız olarak 20000 değerini görürüz.

Ayrıca, “count” değişkeni üzerinde işlem yapan metot “synchronized” olduğu için “count” değişkenini önceki bölümde gördüğümüz gibi “volatile” anahtar kelimesiyle belirtmedik. Bunun nedeni “synchronized” anahtar kelimesinin “count” değişkeni için “volatile” anahtar kelimesinin işlevselliğini garanti etmesidir.

Bir dip not olarak; burada gerçekleştirdiğimiz eşzamanlı threadler ile sayma gibi bir işleve ihtiyaç duyduğumuzda Java’nın dahili kütüphanelerinin içerisinde gelen AtomicInteger’ı kullanabiliriz. AtomicInteger yukarıda gerçekleştiriğimiz “thread-safe” sayma işlemini gerçekleştiren kullanıma hazır bir yardımcı sınıftır.

Reklamlar

Java’da Multithreading – Bölüm 3: “synchronized” Anahtar Kelimesi” üzerine 2 yorum

  1. Guzel bir yazi olmus, eline saglik.

    Belirtmek istedigim bir nokta var. Bu koddaki “synchronized”, aslinda volatile’in sagladigi happens-before iliskisinin yaninda, count = count + 1’in atomic bir sekilde yapilmasini sagliyor. Eger update eden threadlerden 2.sini thread’i baslatmasan, synchronized veya volatile olmadan da bu kod dogru calisir. Cunku ana thread’le count’u update eden thread arasindaki happens-before iliskisini thread.start() ve thread.join() cagrilari sagliyor. Fakat 2 thread count’u guncellediginde, hem birbirlerinin yaptiklari update’leri gormeleri, hem de increment islemini atomic bir sekilde yapmalari gerekiyor. Bu ikisini de bu kodda saglayan, senin de dedigin gibi “synchronized” keyword’u.

    Kolay gelsin.

    • Yorum için çok çok teşekkür ederim Basri. Multithreading konusunda oldukça yeniyim. Öğrendikçe bir nevi kenara not almak maksadıyla yazıyorum buraya da işte.

Bir Yorum Yazın

Aşağıya bilgilerinizi girin veya oturum açmak için bir simgeye tıklayın:

WordPress.com Logosu

WordPress.com hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Twitter resmi

Twitter hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Facebook fotoğrafı

Facebook hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Google+ fotoğrafı

Google+ hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Connecting to %s