JAVA MULTITHREADING — Bolum-2: Critical Section, Race Conditions ve Atomic Operations
Merhaba, “Java Multithreading” yazı serisinin 2. bölümünde aşağıdaki konulardan bahsedeceğim.
- Race Condition nedir?
- Critical Section nedir ?
- Atomic Operations nedir?
- Locking Mekanizmaları nedir ?
Bir önceki yazıda thread ve process, thread pooling ve executors gibi konulardan bahsetmiştik. İncelemek isterseniz buradan ulaşabilirsiniz.
Race Condition Nedir ?
Aynı anda; paylaşılan(shared) bir kaynağa(resource) birden fazla threadin erişerek veri üzerinde işlem yapmasıyla oluşan durumdur.
Örneğin;bir thread bir linked list üzerinde veri okumaya çalışıyorsa ve aynı zamanda başka bir thread aynı verileri silmeye çalışıyorsa bu işlem Race Condition’a yol açar ve Run Time Error’a sebep olabilir.
Yukarıdaki örnekte oluşturduğumuz iki thread aynı obje üzerindeki shared_data değişkeninin değerini değiştirmeye çalışıyor.Bir thread shared_data değerini arttırmaya çalışırken diğer thread aynı zamanda azaltmaya çalışıyor. Normal olarak beklentimiz iterasyon sayısı aynı olduğundan shared_data değişkeninin değerinin 0 olması fakat birden fazla thread; ortak bir değişkene erişip işlem yapmak istediğinden ve yapılan işlem atomik(aşağıda atomik operasyon tanımından bahsediyor olacağım) olmadığından Race Condition’a düşerek beklediğimiz değer olan 0'ı alamayacağız.
Critical Section Nedir ?
Kritik bölüm; birden fazla threadın aynı anda kritik bölüm üzerinde çalışması sonucu, beklenilen sonuçtan farklı bir sonuç yarattığı kod bölümüdür. Bunun sebebi ise kritik bölümde ortak veriler(shared data) kullanmamız ve Thread’lerin aynı anda bu ortak veriler üzerinde işlem yapmalarıdır. Bu yüzden kritik bölüm olarak adlandırdığımız kod alanlarında aynı anda sadece tek bir thread çalışmalıdır. Race Condition örneğinde 8 ve 12. satırlar bizim için kritik bölümdür çünkü aynı anda iki thread aynı değişken üzerinde atomik olmayan bir işlem yapmak istediğinde farklı sonuçlar almıştık.
İki thread oluşturduğumuzu ve bu threadlerin “aggregateFunction” fonksiyonunu çağırdığını düşünelim. Thread’lerden biri kritik bölüme girip çalışmaya başladığı anda diğer thread kritik bölüme giremeyecek ve askıya(suspend) alınarak bekleyecektir ta ki kritik bölüme giren thread kritik bölümden çıkana kadar. Böylece kritik bölümde aynı anda tek thread çalıştırarak; ortak veriler üzerinde birden fazla thread operasyon yapamayacak ve Race Condition oluşmayacaktır.
Atomik Operations Nedir?
Atomik bir operasyon, başlatıldığında yarıda kesilemeyen ve bir defada bitirilmesi gereken operasyon tipidir. Şimdi Javada ki atomik operasyonlardan bahsedelim.
- Bütün referans atamaları atomik olarak gerçekleşir.
- Referanslar üzerindeki tüm get ve set operasyonları atomik olarak gerçekleşir.
- long ve double dışındaki tüm primitive tipler için atamalar atomiktir. (Örneğin int, byte, short, float, char, boolean) Bunun anlamı tipler üzerindeki okuma(read) ve yazma(write) operasyonları atomik olarak gerçekleşeceğinden thread safe yapıda olacaktır.
- long ve double tipleri 64bit olduğundan dolayı JVM atama yaparken bu operasyonu atomik olarak yapacağını garanti etmez.(64 bit bir makine olsa bile) Tipe atama CPU tarafından 2 operasyonda yapılacaktır.(Bir operasyon ilk 32 bite diğer operasyon diğer 32 bite yazacaktır). long ve double tipleri üzerinde atomik olarak işlem yapmak için Java bize “volatile” yöntemini sunar. Bu tipleri volatile keyword’u ile tanımladığımız zamanda atama, okuma ve yazma (assign/read/write) işlemleri atomik olarak yapılacaktır.
- Race Condition örneğindeki(Example-1) “shared_data” değişkeni üzerindeki operasyonlar(shared_data++ ve shared_data- -) ilk bakışta atomik gibi gözükebilir fakat; operasyonu CPU perspektifinden incelediğimizde 3 ayrı operasyona denk gelmektedir. Bunlar; değişkenin değerini okuma, değişkenin değerini arttırma ve yeni değeri atamak olacaktır.Bu işlemler atomik olarak gerçekleştirilmediğinden örnekteki Race Condition probleminin asıl kaynağıda budur. Aşağıda race condition oluşumunu adım adım takip edelim:
incrementThread: shared_data değişkeninin değerini 0 olarak okudu.
incrementThread: shared_data değişkeninin değerini arttırarak 1 yaptı fakat yazma işlemi hala gerçekleşmedi.
decrementThread: shared_data değişkeninin değeri 0 olarak okudu.
incrementThread: shared_data değişkenini 1 olarak kaydetti ve yazma işlemi tamamlandı.
decrementThread: shared_data değişkeninin değerini azaltarak -1 yaptı.
decrementThread: shared_data değişkeninin değerini -1 olarak kaydetti ve yazma işlemi tamamlandı.
Yukarıdaki olay akışında da anlaşılacağı üzere normalde 0 olarak okunan değer arttırma ve azaltma işlemlerinden sonra yine değerinin 0 olarak kalması gerekirken -1 olarak güncellendi.
- Multithread senaryolarda bu tür Race Conditionlardan kaçınmak için “java.util.concurrent.atomic” paketini kullanabiliriz. Bu paket tüm tiplerin(primitive ve object) atomik sınıflarını bulabilir ve kullanabiliriz. Şimdi örnek olarak AtomicInteger sınıfını inceleyelim.
Atomic Integer
Atomic Integer sınıfı, atomik olarak okunabilen ve yazılabilen (read-write) int tipi üzerinde işlemler sağlar ve birçok gelişmiş atomik operasyonlar sağlar. Atomic Integer sınıfındaki bazı önemli methodlar aşağıdaki gibidir.
- int addAndGet(int delta) : Verilen değeri mevcut değere atomik olarak ekler.
- boolean compareAndSet(int expect, int update) : Mevcut değer beklenen değere eşitse, yeni değeri bu koşula göre atar.
- int decrementAndGet() : Mevcut değeri atomik olarak bir azaltır.
- int incrementAndGet() : Mevcut değeri atomik olarak bir arttırır.
- int getAndSet(int newValue) : Atomik olarak verilen değeri setler ve eski değeri döndürür.
- int get() : Mevcut değeri döndürür.
Aşağıdaki örnekte atomic integer kullanımına ait kod örneğini bulabilirsiniz. Burada ilk örneğimizin aksine(Race Condition Example-1) AtomicInteger sınıfıyla thread safe bir yapı kurarak doğru sonuçlar elde edebiliriz. “items” değişkeni threadler arasında paylaşılan bir resource olmasına rağmen bu sefer arttırma ve azaltma işlemlerimizi atomik olarak yaptığımızdan dolayı “items” değişkenin değeri işlemlerin sonunda yine 0 olacaktır.
Bir sonraki yazımda ise locking, synchronized ve reentrantlock yapılarından bahsediyor olacağım. Takipte kalınız :)