Takip et

Java’da Gelişmiş Senkronizasyon: ReentrantLock Detaylı İncelemesi

Java’da Gelişmiş Senkronizasyon: ReentrantLock Detaylı İncelemesi Çoklu iş parçacığı (multi-threading) programlama, modern uygulamaların p

Java’da Gelişmiş Senkronizasyon: ReentrantLock Detaylı İncelemesi

Çoklu iş parçacığı (multi-threading) programlama, modern uygulamaların performansını ve yanıt verebilirliğini artırmak için vazgeçilmez bir araçtır. Ancak, birden fazla iş parçacığı aynı anda paylaşılan kaynaklara erişmeye çalıştığında, veri tutarsızlıkları ve yarış koşulları (race conditions) gibi ciddi sorunlar ortaya çıkabilir. Bu sorunları önlemek ve iş parçacığı güvenliğini (thread safety) sağlamak için senkronizasyon mekanizmalarına ihtiyaç duyarız. Java, bu amaçla synchronized anahtar kelimesini sunar, ancak bazen daha esnek ve kontrol edilebilir senkronizasyon araçlarına ihtiyaç duyulur. İşte bu noktada java.util.concurrent.locks paketi ve özellikle de ReentrantLock sınıfı devreye girer. Bu makalede, ReentrantLock‘ın ne olduğunu, nasıl çalıştığını, synchronized anahtar kelimesinden farklarını ve çeşitli kullanım senaryolarını ayrıntılı bir şekilde inceleyeceğiz.

Giriş: Çoklu İş Parçacığı Programlamada Senkronizasyon İhtiyacı

Java’da çoklu iş parçacığı uygulamaları geliştirirken, birden fazla iş parçacığının aynı anda bir nesnenin durumunu değiştirmeye çalıştığı durumlarla sıkça karşılaşılır. Örneğin, birden fazla iş parçacığı ortak bir sayaç değişkenini artırıyor veya ortak bir veri yapısına (liste, harita vb.) öğe ekleyip çıkarıyorsa, bu işlemler atomik (bölünmez) olmadığında beklenmedik sonuçlar ortaya çıkabilir. Bu duruma yarış koşulu denir. Yarış koşulları, programın davranışını öngörülemez hale getirir ve hata ayıklamayı son derece zorlaştırır.

Java’nın synchronized anahtar kelimesi, bu tür sorunları çözmek için temel bir mekanizma sunar. Bir metodu veya kod bloğunu synchronized olarak işaretlediğinizde, aynı anda yalnızca bir iş parçacığının o senkronize edilmiş koda erişmesine izin verilir. Diğer iş parçacıkları, kilit serbest bırakılana kadar beklemeye alınır. Bu, basit senkronizasyon ihtiyaçları için oldukça etkili ve kullanımı kolay bir çözümdür. Ancak synchronized‘ın bazı sınırlamaları vardır:

* Esneklik Eksikliği: Kilidin ne zaman alınacağını ve ne zaman serbest bırakılacağını açıkça kontrol edemezsiniz. Kilit, senkronize edilmiş bloğun sonuna ulaşıldığında veya bir istisna fırlatıldığında otomatik olarak serbest bırakılır.
* Kilidi Deneme Yeteneği Yok: Bir iş parçacığı, kilidi hemen alamıyorsa, sonsuza kadar beklemek yerine başka bir iş yapmaya devam edemez. synchronized ile kilidi almaya çalışmak bloke edici bir işlemdir.
* Kesintiye Uğrayabilen Kilit Beklemesi Yok: Bir iş parçacığı synchronized bir bloğa girmek için beklerken, dışarıdan gelen bir kesinti sinyali ile bu bekleme durumundan çıkamaz.
* Çoklu Koşul Değişkeni Yok: Object.wait(), notify() ve notifyAll() metotları, belirli bir nesneyle ilişkilidir ve yalnızca tek bir bekleme kümesini yönetebilir. Daha karmaşık senaryolarda birden fazla bekleme koşuluna ihtiyaç duyulduğunda bu yetersiz kalabilir.
* Adillik (Fairness) Yok: synchronized kilitlerin adil olup olmadığı garanti edilmez; yani, kilidi bekleyen iş parçacıkları arasında bir öncelik sırası yoktur.

Bu sınırlamalar, özellikle yüksek performanslı ve karmaşık eşzamanlı uygulamalar geliştirirken geliştiricilere ek zorluklar çıkarır. java.util.concurrent.locks paketi, bu eksiklikleri gidermek ve daha gelişmiş senkronizasyon kontrolü sağlamak amacıyla tasarlanmıştır.

Lock Arayüzü: synchronized‘a Alternatif ve Daha Fazlası

java.util.concurrent.locks.Lock arayüzü, synchronized anahtar kelimesine kıyasla daha esnek ve güçlü bir kilit mekanizması sunar. Bu arayüz, geliştiricilere kilit edinme ve serbest bırakma süreçleri üzerinde daha fazla kontrol sağlar. Lock arayüzünün temel metotları şunlardır:

* void lock(): Kilidi edinir. Kilit başka bir iş parçacığı tarafından tutuluyorsa, bu metodu çağıran iş parçacığı kilit serbest bırakılana kadar bloke olur.
* void lockInterruptibly() throws InterruptedException: Kilidi edinir, ancak kilidi beklerken iş parçacığı kesintiye uğrayabilir. Eğer iş parçacığı kesintiye uğrarsa, InterruptedException fırlatır.
* boolean tryLock(): Kilidi edinmeye çalışır. Kilit müsaitse hemen edinir ve true döndürür. Kilit müsait değilse, bloke olmadan hemen false döndürür.
* boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException: Belirtilen zaman aşımı süresi boyunca kilidi edinmeye çalışır. Eğer kilit bu süre içinde edinilirse true, edinilemezse false döndürür. Kilit beklerken iş parçacığı kesintiye uğrayabilir.
* void unlock(): Kilidi serbest bırakır. Bu metot, kilidi edinmiş olan iş parçacığı tarafından çağrılmalıdır. Aksi takdirde IllegalMonitorStateException fırlatılabilir.

Lock arayüzünü kullanırken en kritik noktalardan biri, kilidi her zaman bir finally bloğu içinde serbest bırakmaktır. Bu, senkronize edilmiş kod bloğunda bir istisna oluşsa bile kilidin serbest bırakılmasını garanti eder ve diğer iş parçacıklarının kilitlenmesini (deadlock) önler.

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

public class LockOrnegi {
    private final Lock kilit = new ReentrantLock();
    private int sayac = 0;

    public void artir() {
        kilit.lock(); // Kilidi edin
        try {
            // Senkronize edilmiş kod bloğu
            sayac++;
            System.out.println(Thread.currentThread().getName() + " sayacı artırdı: " + sayac);
        } finally {
            kilit.unlock(); // Kilidi serbest bırak
        }
    }

    public int getSayac() {
        return sayac;
    }

    public static void main(String[] args) throws InterruptedException {
        LockOrnegi ornek = new LockOrnegi();

        Runnable gorev = () -> {
            for (int i = 0; i < 1000; i++) {
                ornek.artir();
            }
        };

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

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

        t1.join();
        t2.join();

        System.out.println("Nihai Sayaç Değeri: " + ornek.getSayac());
    }
}

Yukarıdaki örnekte, ReentrantLock sınıfı Lock arayüzünün bir implementasyonudur. artir() metodu, lock() ile kilidi edinir ve finally bloğu içinde unlock() ile serbest bırakır. Bu yapı, synchronized anahtar kelimesinin sağladığı güvenliği, daha açık bir kontrol mekanizmasıyla sunar.

ReentrantLock Nedir? Detaylı Bakış

ReentrantLock, Lock arayüzünün en yaygın kullanılan implementasyonlarından biridir. Adından da anlaşılacağı gibi, "reentrant" (yeniden girişli) bir kilittir. Peki, bu ne anlama geliyor?

Yeniden Girişlilik (Reentrancy) Kavramı

Bir iş parçacığı, zaten sahip olduğu bir ReentrantLock kilidini tekrar edinebilir. Örneğin, bir iş parçacığı lock() metodunu çağırarak bir kilidi edindikten sonra, aynı iş parçacığı aynı kilit üzerinde tekrar lock() metodunu çağırabilir ve kilit başarılı bir şekilde edinilir. Kilidin "tutma sayısı" (hold count) artırılır. Kilidin tamamen serbest bırakılması için, lock() çağrısı kadar unlock() çağrısının yapılması gerekir.

Bu özellik, iç içe metot çağrılarında veya özyinelemeli (recursive) metotlarda oldukça kullanışlıdır. Eğer bir iş parçacığı, bir kilit altında çalışan bir metottan, yine aynı kilidi gerektiren başka bir metodu çağırırsa, yeniden girişlilik sayesinde iş parçacığı kendisini bloke etmez.

İç Çalışma Prensibi (Kavramsal)

ReentrantLock, içsel olarak kilidin hangi iş parçacığı tarafından tutulduğunu ve bu iş parçacığının kilidi kaç kez edindiğini takip eden bir sayaç (hold count) tutar.

* Bir iş parçacığı lock() metodunu çağırdığında:
* Eğer kilit serbestse, iş parçacığı kilidin sahibi olur ve tutma sayısı 1 olarak ayarlanır.
* Eğer kilit zaten bu iş parçacığı tarafından tutuluyorsa, tutma sayısı artırılır.
* Eğer kilit başka bir iş parçacığı tarafından tutuluyorsa, çağıran iş parçacığı kilit serbest bırakılana kadar bloke olur.
* Bir iş parçacığı unlock() metodunu çağırdığında:
* Eğer çağıran iş parçacığı kilidin sahibi değilse, IllegalMonitorStateException fırlatılır.
* Eğer çağıran iş parçacığı kilidin sahibi ise, tutma sayısı azaltılır.
* Eğer tutma sayısı sıfıra düşerse, kilit serbest bırakılır ve bekleyen diğer iş parçacıklarından biri kilidi edinebilir.

Kurucular ve Adillik (Fairness)

ReentrantLock sınıfının iki kurucusu vardır:

1. ReentrantLock(): Varsayılan olarak "adil olmayan" (non-fair) bir kilit oluşturur.
2. ReentrantLock(boolean fair): fair parametresi true ise adil bir kilit, false ise adil olmayan bir kilit oluşturur.

Adil Kilit (Fair Lock)

Adil bir kilit, kilidi bekleyen iş parçacıklarına, kilit için beklemeye başladıkları sıraya göre erişim garantisi verir. Yani, en uzun süre bekleyen iş parçacığı, kilit serbest bırakıldığında ilk olarak kilidi edinecektir. Bu, iş parçacığı açlığını (starvation) önler.

Avantajları: İş parçacığı açlığını engeller, daha öngörülebilir davranış sağlar.
Dezavantajları: Genellikle adil olmayan kilitlerden daha düşük performansa sahiptir. Bunun nedeni, kilitleme ve kilit açma işlemleri sırasında daha fazla işletim sistemi çağrısı ve iş parçacığı bağlam anahtarlaması (context switching) gerektirmesidir.

Adil Olmayan Kilit (Non-Fair Lock)

Adil olmayan bir kilit, kilidi bekleyen iş parçacıkları arasında belirli bir sıra garantisi vermez. Kilit serbest bırakıldığında, bekleyen iş parçacıklarından herhangi biri (genellikle işletim sistemi zamanlayıcısına bağlı olarak) kilidi edinebilir. Hatta, yeni gelen bir iş parçacığı, uzun süredir bekleyen diğer iş parçacıklarından önce kilidi edinebilir. Bu durum "yığılma" (barging) olarak da adlandırılır.

Avantajları: Genellikle adil kilitlerden daha yüksek performansa sahiptir, çünkü iş parçacığı bağlam anahtarlaması daha azdır ve CPU önbellekleri daha verimli kullanılabilir.
Dezavantajları: İş parçacığı açlığına neden olabilir; bazı iş parçacıkları kilidi edinmek için çok uzun süre bekleyebilir.

Çoğu durumda, varsayılan olan adil olmayan kilitler tercih edilir çünkü performans avantajları genellikle küçük bir açlık riski için kabul edilebilir. Ancak, uygulamanızda kesinlikle belirli bir sıranın korunması gerekiyorsa veya iş parçacığı açlığının kritik bir sorun olduğu durumlarda adil kilitler kullanılmalıdır.

Koşul Nesneleri (Condition Objects) ile Gelişmiş İş Parçacığı İletişimi

ReentrantLock ile birlikte gelen en güçlü özelliklerden biri de Condition arayüzüdür. Condition nesneleri, Object.wait(), notify() ve notifyAll() metotlarının Lock arayüzü karşılığıdır. Ancak Condition nesneleri, synchronized anahtar kelimesine bağlı olan Object metotlarına göre daha esnek ve güçlüdür.

Object metotları, yalnızca bir nesne monitörü (kilit) ile ilişkilidir ve yalnızca tek bir bekleme kümesi vardır. Yani, bir nesne üzerinde wait() çağrısı yapan tüm iş parçacıkları aynı bekleme kümesine girer ve notify() veya notifyAll() çağrısı yapıldığında bu kümedeki iş parçacıkları uyandırılır. Bu, bazen istenmeyen uyandırmalara (spurious wakeups) veya daha az verimli iş parçacığı yönetimine yol açabilir.

Condition arayüzü, bir Lock nesnesiyle ilişkilendirilir ve bir Lock nesnesi birden fazla Condition nesnesi oluşturabilir. Her Condition nesnesi kendi bekleme kümesini yönetir. Bu, geliştiricilere daha granüler (ince taneli) bir kontrol sağlar; farklı koşullar altında bekleyen iş parçacıklarını ayrı ayrı uyandırabiliriz.

Condition arayüzünün temel metotları şunlardır:

* void await() throws InterruptedException: Kilidi serbest bırakır ve iş parçacığını bu koşul nesnesi üzerinde beklemeye alır. İş parçacığı, başka bir iş parçacığı signal() veya signalAll() çağırana kadar veya kesintiye uğrayana kadar bloke olur. Geri döndüğünde, kilidi tekrar edinir.
* void signal(): Bu koşul nesnesi üzerinde bekleyen iş parçacıklarından birini (rastgele) uyandırır.
* void signalAll(): Bu koşul nesnesi üzerinde bekleyen tüm iş parçacıklarını uyandırır.

Bir Condition nesnesi, Lock nesnesi üzerinden lock.newCondition() metodu ile oluşturulur.

Üretici-Tüketici Örneği ile Condition Kullanımı

Üretici-tüketici problemi, eşzamanlı programlamadaki klasik bir senaryodur ve Condition nesnelerinin gücünü göstermek için harika bir örnektir.

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class UreticiTuketiciCondition {
    private final ReentrantLock kilit = new ReentrantLock();
    private final Condition bosAlanVar = kilit.newCondition(); // Tüketicilerin beklemesi için
    private final Condition doluAlanVar = kilit.newCondition(); // Üreticilerin beklemesi için
    private final Queue tampon = new LinkedList<>();
    private final int KAPASITE = 5;

    public void uret(int deger) throws InterruptedException {
        kilit.lock();
        try {
            while (tampon.size() == KAPASITE) {
                System.out.println("Tampon dolu, üretici bekliyor: " + Thread.currentThread().getName());
                doluAlanVar.await(); // Tampon doluysa üretici bekler
            }
            tampon.add(deger);
            System.out.println("Üretildi: " + deger + " (Tampon boyutu: " + tampon.size() + ")");
            bosAlanVar.signalAll(); // Tüketicilere boş alan olduğunu bildir
        } finally {
            kilit.unlock();
        }
    }

    public int tuket() throws InterruptedException {
        kilit.lock();
        try {
            while (tampon.isEmpty()) {
                System.out.println("Tampon boş, tüketici bekliyor: " + Thread.currentThread().getName());
                bosAlanVar.await(); // Tampon boşsa tüketici bekler
            }
            int deger = tampon.remove();
            System.out.println("Tüketildi: " + deger + " (Tampon boyutu: " + tampon.size() + ")");
            doluAlanVar.signalAll(); // Üreticilere dolu alan olduğunu bildir
            return deger;
        } finally {
            kilit.unlock();
        }
    }

    public static void main(String[] args) {
        UreticiTuketiciCondition problem = new UreticiTuketiciCondition();

        Runnable ureticiGorevi = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    problem.uret(i);
                    Thread.sleep((long) (Math.random() * 100));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };

        Runnable tuketiciGorevi = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    problem.tuket();
                    Thread.sleep((long) (Math.random() * 150));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };

        Thread uretici1 = new Thread(ureticiGorevi, "Üretici-1");
        Thread tuketici1 = new Thread(tuketiciGorevi, "Tüketici-1");
        Thread uretici2 = new Thread(ureticiGorevi, "Üretici-2"); // Birden fazla üretici/tüketici için
        Thread tuketici2 = new Thread(tuketiciGorevi, "Tüketici-2");

        uretici1.start();
        tuketici1.start();
        uretici2.start();
        tuketici2.start();

        // main thread'in bitmesini beklemek için join kullanılabilir,
        // ancak bu örnekte output'u izlemek için gerek yok.
    }
}

Bu örnekte, bosAlanVar ve doluAlanVar olmak üzere iki farklı Condition nesnesi kullanıyoruz.
* uret() metodu, tampon doluysa doluAlanVar.await() çağırarak üretici iş parçacığını beklemeye alır ve tampon boşaldığında bosAlanVar.signalAll() ile tüketicileri uyarır.
* tuket() metodu, tampon boşsa bosAlanVar.await() çağırarak tüketici iş parçacığını beklemeye alır ve tampon dolduğunda doluAlanVar.signalAll() ile üreticileri uyarır.

Bu ayrım, synchronized ile Object.wait()/notify() kullanılsaydı mümkün olmazdı ve tüm bekleyen iş parçacıkları tek bir notifyAll() ile uyandırılmak zorunda kalırdı, bu da gereksiz bağlam anahtarlamalarına yol açabilirdi.

ReentrantLock Kullanım Örnekleri

Daha önce bahsedilen temel sayaç ve üretici-tüketici örneklerinin yanı sıra, ReentrantLock'ın diğer güçlü özelliklerini gösteren örnekler de inceleyelim.

Yeniden Girişlilik (Reentrancy) Örneği

Aşağıdaki örnek, bir iş parçacığının zaten sahip olduğu bir kilidi tekrar nasıl edinebileceğini gösterir.

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockReentrancyOrnegi {
    private final ReentrantLock kilit = new ReentrantLock();

    public void disMetot() {
        kilit.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " dış metotta kilidi edindi.");
            icMetot(); // Aynı kilit tekrar ediniliyor
        } finally {
            kilit.unlock();
            System.out.println(Thread.currentThread().getName() + " dış metotta kilidi serbest bıraktı.");
        }
    }

    public void icMetot() {
        kilit.lock(); // Yeniden girişli kilit, aynı iş parçacığı tarafından tekrar edinilebilir
        try {
            System.out.println(Thread.currentThread().getName() + " iç metotta kilidi edindi.");
        } finally {
            kilit.unlock();
            System.out.println(Thread.currentThread().getName() + " iç metotta kilidi serbest bıraktı.");
        }
    }

    public static void main(String[] args) {
        ReentrantLockReentrancyOrnegi ornek = new ReentrantLockReentrancyOrnegi();

        Runnable gorev = () -> {
            ornek.disMetot();
        };

        Thread t1 = new Thread(gorev, "Thread-1");
        t1.start();
    }
}

Çıktıda görüleceği üzere, Thread-1 dış metotta kilidi edindikten sonra, iç metodu çağırdığında aynı kilidi tekrar edinebilir ve herhangi bir kilitlenme yaşanmaz. unlock() çağrıları da doğru sırayla yapıldığında kilit serbest bırakılır.

tryLock() ve Zaman Aşımı Örneği

tryLock() metodu, bir iş parçacığının kilidi hemen alamıyorsa bloke olmak yerine başka bir işlem yapmasına olanak tanır. Zaman aşımı parametresiyle birlikte kullanıldığında, belirli bir süre bekledikten sonra kilidi alamıyorsa iş parçacığına devam etme esnekliği sağlar.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTryLockOrnegi {
    private final ReentrantLock kilit = new ReentrantLock();

    public void islemYap(int id) {
        try {
            if (kilit.tryLock(1, TimeUnit.SECONDS)) { // 1 saniye bekle, alamıyorsan devam et
                try {
                    System.out.println("İş Parçacığı " + id + ": Kilidi edindi ve işleme başlıyor.");
                    Thread.sleep(2000); // 2 saniye boyunca kilidi tut
                    System.out.println("İş Parçacığı " + id + ": İşlemi tamamladı.");
                } finally {
                    kilit.unlock();
                    System.out.println("İş Parçacığı " + id + ": Kilidi serbest bıraktı.");
                }
            } else {
                System.out.println("İş Parçacığı " + id + ": Kilidi edinemedi, başka iş yapıyor.");
                // Kilidi alamadığı durumda yapılacak alternatif işler
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("İş Parçacığı " + id + ": Kesintiye uğradı.");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTryLockOrnegi ornek = new ReentrantLockTryLockOrnegi();

        Runnable gorev1 = () -> ornek.islemYap(1);
        Runnable gorev2 = () -> ornek.islemYap(2);

        Thread t1 = new Thread(gorev1);
        Thread t2 = new Thread(gorev2);

        t1.start();
        Thread.sleep(100); // t1'in kilidi önce almasını sağlamak için küçük bir gecikme
        t2.start();
    }
}

Bu örnekte, Thread-1 kilidi edinecek ve 2 saniye boyunca tutacaktır. Thread-2 ise 1 saniye bekleyecek, kilidi alamadığı için "Kilidi edinemedi, başka iş yapıyor." mesajını yazdırıp devam edecektir. Bu, synchronized ile mümkün olmayan bir esnekliktir.

lockInterruptibly() Örneği

lockInterruptibly() metodu, bir iş parçacığının kilit beklerken kesintiye uğramasına ve bekleme durumundan çıkmasına olanak tanır.

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockInterruptiblyOrnegi {
    private final ReentrantLock kilit = new ReentrantLock();

    public void islemYap() {
        try {
            kilit.lockInterruptibly(); // Kesintiye uğrayabilen kilit edinme
            try {
                System.out.println(Thread.currentThread().getName() + ": Kilidi edindi ve işleme başlıyor.");
                // Uzun süreli bir işlem simülasyonu
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + ": İşlemi tamamladı.");
            } finally {
                kilit.unlock();
                System.out.println(Thread.currentThread().getName() + ": Kilidi serbest bıraktı.");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + ": Kilit beklerken kesintiye uğradı.");
            Thread.currentThread().interrupt(); // Kesinti durumunu koru
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockInterruptiblyOrnegi ornek = new ReentrantLockInterruptiblyOrnegi();

        Thread t1 = new Thread(ornek::islemYap, "Thread-1");
        Thread t2 = new Thread(ornek::islemYap, "Thread-2");

        t1.start();
        Thread.sleep(100); // t1'in kilidi önce almasını sağla
        t2.start();

        Thread.sleep(1000); // t2'nin kilit beklemesini sağla
        t2.interrupt(); // t2'yi kesintiye uğrat
    }
}

Bu örnekte, Thread-1 kilidi edinir ve 5 saniye boyunca tutar. Thread-2 ise kilidi edinmek için lockInterruptibly() ile beklemeye başlar. 1 saniye sonra main iş parçacığı Thread-2'yi kesintiye uğratır. Thread-2, kilit beklerken kesintiye uğradığı için InterruptedException yakalar ve bekleme durumundan çıkar. synchronized anahtar kelimesiyle bu tür bir kesintiye uğrama mümkün değildir.

ReentrantLock ve synchronized Karşılaştırması

ReentrantLock ve synchronized anahtar kelimesi, Java'da iş parçacığı güvenliğini sağlamak için kullanılan iki temel mekanizmadır. Her ikisi de karşılıklı dışlamayı (mutual exclusion) sağlar, yani aynı anda yalnızca bir iş parçacığının kritik bir bölüme erişmesine izin verir. Ancak aralarında önemli farklar bulunur:

Benzerlikler

* Karşılıklı Dışlama: Her ikisi de paylaşılan kaynaklara aynı anda birden fazla iş parçacığının erişmesini engelleyerek veri tutarsızlıklarını önler.
* Yeniden Girişlilik: Her ikisi de yeniden girişlidir. Yani, bir iş parçacığı zaten sahip olduğu bir synchronized bloğa veya ReentrantLock'a tekrar girebilir.

Farklılıklar

1. Açıklık ve Kontrol:
* synchronized: Implicit (örtük) bir mekanizmadır. Kilitleme ve kilit açma işlemleri JVM tarafından otomatik olarak yönetilir. Geliştiricinin doğrudan kontrolü yoktur.
* ReentrantLock: Explicit (açık) bir mekanizmadır. Kilitleme (lock()) ve kilit açma (unlock()) metotlarını açıkça çağırmanız gerekir. Bu, daha fazla kontrol ve esneklik sağlar.

2. Esneklik ve Ek Özellikler:
* synchronized: Sınırlı esnekliğe sahiptir. Kilidi deneme (tryLock()), kesintiye uğrayabilen kilit bekleme (lockInterruptibly()) veya zaman aşımı ile kilit bekleme gibi özellikler sunmaz.
* ReentrantLock: tryLock(), lockInterruptibly(), tryLock(timeout, unit) gibi metotlarla daha gelişmiş kilit edinme stratejileri sunar. Ayrıca adil (fair) kilitler oluşturma seçeneği de vardır.

3. Hata Yönetimi ve Kilit Bırakma:
* synchronized: Kilit, senkronize edilmiş bloğun sonuna ulaşıldığında veya bir istisna fırlatıldığında otomatik olarak serbest bırakılır. Bu, geliştiricinin kilit serbest bırakmayı unutma riskini ortadan kaldırır.
* ReentrantLock: Kilidi finally bloğu içinde açıkça unlock() ile serbest bırakmak geliştiricinin sorumluluğundadır. Aksi takdirde, bir istisna durumunda kilit serbest bırakılamaz ve diğer iş parçacıkları kilitli kalır (deadlock).

4. Koşul Değişkenleri (Condition Variables):
* synchronized: Object.wait(), notify(), notifyAll() metotlarını kullanarak iş parçacığı iletişimini sağlar. Bu metotlar belirli bir nesnenin monitörüne bağlıdır ve tek bir bekleme kümesini yönetir.
* ReentrantLock: Condition arayüzünü kullanır. Bir Lock nesnesi birden fazla Condition nesnesi oluşturabilir, bu da daha granüler iş parçacığı iletişimine olanak tanır. Farklı koşullar altında bekleyen iş parçacıkları ayrı ayrı yönetilebilir.

5. Performans:
* Performans açısından, eski Java sürümlerinde ReentrantLock genellikle synchronized'dan daha iyi performans gösteriyordu. Ancak modern JVM'ler, synchronized anahtar kelimesi için önemli optimizasyonlar (örneğin, lock elision, biased locking) içermektedir. Bu nedenle, çoğu durumda performans farkı ihmal edilebilir düzeydedir veya synchronized daha iyi performans gösterebilir.
* Ancak, yüksek çekişme (high contention) durumlarında veya adil kilitler kullanıldığında ReentrantLock'ın performansı daha öngörülebilir olabilir.

Ne Zaman Hangisini Kullanmalı?

* synchronized kullanın:
* Basit senkronizasyon ihtiyaçlarınız varsa.
* Kilitleme ve kilit açma üzerinde ekstra kontrol gerekmiyorsa.
* Object.wait()/notify() ile iş parçacığı iletişimi yeterliyse.
* Kodun daha az karmaşık olmasını istiyorsanız.
* Performans kritik bir endişe değilse ve varsayılan JVM optimizasyonlarına güveniyorsanız.

* ReentrantLock kullanın:
* Kilidi edinme ve serbest bırakma üzerinde daha fazla kontrol gerekiyorsa.
* Kilidi deneme (tryLock()) veya zaman aşımı ile edinme ihtiyacınız varsa.
* Kilit beklerken iş parçacığının kesintiye uğramasını sağlamak istiyorsanız (lockInterruptibly()).
* Birden fazla koşul değişkenine (Condition nesneleri) ihtiyacınız varsa (örneğin, karmaşık üretici-tüketici senaryoları).
* Adil kilit davranışına ihtiyacınız varsa.
* Çok yüksek çekişme altında performans optimizasyonları arıyorsanız.

Genel bir kural olarak, synchronized anahtar kelimesi çoğu senaryo için yeterli ve tercih edilebilir bir çözümdür. Ancak ReentrantLock, synchronized'ın yetersiz kaldığı daha karmaşık ve özel eşzamanlılık ihtiyaçları için güçlü bir alternatiftir.

En İyi Uygulamalar ve Dikkat Edilmesi Gerekenler

ReentrantLock kullanırken aşağıdaki en iyi uygulamaları ve dikkat edilmesi gereken noktaları göz önünde bulundurmak, sağlam ve hatasız eşzamanlı uygulamalar geliştirmenize yardımcı olacaktır:

* finally Bloğu Kullanımı: Her zaman lock() çağrısından sonra unlock() metodunu bir finally bloğu içinde çağırın. Bu, kodunuzda bir istisna oluşsa bile kilidin serbest bırakılmasını garanti eder ve kilitlenmeleri (deadlock) önler.

kilit.lock();
    try {
        // Kritik bölüm
    } finally {
        kilit.unlock();
    }

* Deadlock'lardan Kaçınma: Birden fazla kilit kullanırken, iş parçacıklarının kilitleri her zaman aynı sırada edinmesini sağlayın. Farklı iş parçacıklarının kilitleri farklı sıralarda edinmeye çalışması deadlock'a yol açabilir.
* Adillik Seçimi: Uygulamanızın gereksinimlerine göre adil veya adil olmayan kilit arasında bilinçli bir seçim yapın. Performans genellikle adil olmayan kilitleri desteklerken, iş parçacığı açlığından kesinlikle kaçınmak gerekiyorsa adil kilitler tercih edilmelidir.
* Performans Değerlendirmesi: ReentrantLock veya synchronized arasında seçim yaparken, uygulamanızın gerçek dünya senaryolarında performans testleri yapın. Varsayımlara dayanmak yerine ölçümlere güvenin.
* Condition Nesnelerinin Doğru Kullanımı: await() çağrısını her zaman bir while döngüsü içinde yapın (while (koşul doğru değilse) { condition.await(); }). Bu, istenmeyen uyandırmalara (spurious wakeups) karşı koruma sağlar.
* Kesinti Yönetimi: lockInterruptibly() veya await() gibi kesintiye uğrayabilen metotları kullanırken, InterruptedException'ı doğru bir şekilde yakalayın ve işleyin. Genellikle, iş parçacığının kesinti durumunu tekrar ayarlamak (Thread.currentThread().interrupt();) ve üst katmanlara yaymak iyi bir uygulamadır.
* Kilit Kapsamı: Kilidi mümkün olan en dar kapsamda tutmaya çalışın. Gereksiz yere büyük kod bloklarını kilitlemek, eşzamanlılığı azaltır ve performans düşüşüne neden olabilir.

Sonuç

ReentrantLock, Java'nın eşzamanlılık API'sinde önemli bir yer tutan, güçlü ve esnek bir kilit mekanizmasıdır. synchronized anahtar kelimesinin sunduğu temel işlevselliğin ötesine geçerek, geliştiricilere kilit edinme ve serbest bırakma süreçleri üzerinde daha ince taneli kontrol, kesintiye uğrayabilen kilit bekleme, zaman aşımı ile kilit deneme ve birden fazla koşul değişkeni gibi gelişmiş özellikler sunar.

Her ne kadar synchronized çoğu senaryo için yeterli olsa ve modern JVM optimizasyonları sayesinde performans açısından rekabetçi olsa da, ReentrantLock karmaşık eşzamanlı uygulamalarda daha fazla esneklik ve kontrol gerektiren durumlar için vazgeçilmezdir. Özellikle üretici-tüketici gibi karmaşık iş parçacığı iletişim senaryolarında Condition nesneleriyle birlikte kullanıldığında, ReentrantLock kodunuzun daha temiz, daha verimli ve daha yönetilebilir olmasını sağlar. Ancak, bu gücün sorumlulukla geldiğini unutmamak ve finally bloğu içinde kilit serbest bırakma gibi en iyi uygulamalara sıkıca bağlı kalmak hayati önem taşır. Doğru kullanıldığında, ReentrantLock Java uygulamalarınızın çoklu iş parçacığı ortamında güvenilirliğini ve performansını önemli ölçüde artırabilir.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Mobil sürümden çık