İçeriğe geç
academia.sh

Ders 07 / 11

Senkronizasyon

İzleyici kilidine yeniden girmek tutma sayısını 0'dan 2'ye çıkarıp geri indiriyor. Aynı senkronize yöntem aynı örnekte çağrıldığında tek kilit, iki ayrı örnekte ya da bir örnek yöntemiyle bir sınıf yönteminde çağrıldığında iki ayrı kilit devreye giriyor; üç kurulumun ikisinde kilit sayısı ikiye çıkıyor.

İçindekiler

Önceki ders synchronized bloğunu tabloya tek bir satır olarak koydu ve okuyanla yazanın aynı kilidi kullanması gerektiğini söyledi — ama o derste kilit tek bir statik alandı, iki iş parçacığının aynı nesneyi paylaştığı baştan verilmişti. Bu ders o varsayımı kaldırıyor. synchronized anahtar sözcüğü kaynakta hangi kilidin kullanılacağını açıkça yazmaz; bir örnek yöntemi çağrıldığında kullanılan kilit o çağrıdaki nesneye bağlıdır ve aynı kaynak satırı, hangi nesne üzerinden çağrıldığına göre iki ayrı kilide karşılık gelebilir.

Bu belirsizlik iki ayrı soruyu birbirinden ayırmayı gerektiriyor. Birincisi: bir iş parçacığı şu an bir kilidi tutuyor mu, ve tutuyorsa kaç kez? Bu, tek bir iş parçacığının kendi durumuyla ilgili bir sorudur ve bu dersin ilk bölümü bunu ölçüyor. İkincisi: iki ayrı çağrı aynı kilidi mi kullanıyor? Bu, kaynak metne bakarak değil, iki çağrının hangi nesne üzerinden yapıldığına bakarak yanıtlanabilir bir sorudur ve dersin ikinci bölümü bunu üç kurulumda sayıyor.

İzleyici Kilidini Kim Tutuyor

  • EZ9Thread.holdsLock(nesne) yalnız çağıran iş parçacığının o nesnenin kilidini o an tutup tutmadığını sorar; başka bir iş parçacığının tutup tutmadığını sormaz, bu bilgi API ile hiç sunulmaz. Bir kilidin kaç kez tutulduğu ise ReentrantLock.getHoldCount() ile ölçülür — izleyici kilidinin kendisi bu sayıyı dışarı vermez, bu yüzden ölçüm açık kilitle yapılıyor.
// KilitTutma.java — izleyici kilidini kim tutuyor holdsLock ile, yeniden girmenin tutma sayisi ReentrantLock ile olculur
import java.util.concurrent.locks.ReentrantLock;

public class KilitTutma {
    static final Object izleyici = new Object();
    static final ReentrantLock acikKilit = new ReentrantLock();

    public static void main(String[] args) {
        System.out.println("kilit tutuluyor mu (baslamadan once) : " + Thread.holdsLock(izleyici));
        synchronized (izleyici) {
            System.out.println("kilit tutuluyor mu (blok icinde)      : " + Thread.holdsLock(izleyici));
        }
        System.out.println("kilit tutuluyor mu (bloktan sonra)    : " + Thread.holdsLock(izleyici));

        System.out.println();
        System.out.println("baslamadan once     tutmaSayisi=" + acikKilit.getHoldCount());
        acikKilit.lock();
        try {
            System.out.println("1. giristen sonra   tutmaSayisi=" + acikKilit.getHoldCount());
            acikKilit.lock(); // ayni is parcacigi ayni kilide yeniden giriyor
            try {
                System.out.println("2. giristen sonra   tutmaSayisi=" + acikKilit.getHoldCount());
            } finally {
                acikKilit.unlock();
                System.out.println("ic cikistan sonra   tutmaSayisi=" + acikKilit.getHoldCount());
            }
        } finally {
            acikKilit.unlock();
            System.out.println("dis cikistan sonra  tutmaSayisi=" + acikKilit.getHoldCount());
        }
    }
}
kilit tutuluyor mu (baslamadan once) : false
kilit tutuluyor mu (blok icinde)      : true
kilit tutuluyor mu (bloktan sonra)    : false

baslamadan once     tutmaSayisi=0
1. giristen sonra   tutmaSayisi=1
2. giristen sonra   tutmaSayisi=2
ic cikistan sonra   tutmaSayisi=1
dis cikistan sonra  tutmaSayisi=0

İlk üç satır kilidin kimliğini ölçüyor: synchronized (izleyici) bloğuna girildiği an holdsLock true’ya dönüyor, bloktan çıkıldığı an false’a geri düşüyor — kilit tutmanın kendisi blok sınırına bağlı, ayrıca bir bırakma çağrısı gerektirmiyor. Alttaki beş satır ise yeniden girilebilirliği ölçüyor. ReentrantLock, adını da buradan alıyor: aynı iş parçacığı zaten tuttuğu kilide ikinci kez girdiğinde engellenmiyor, tutma sayısı 1’den 2’ye çıkıyor. İç bloktan çıkıldığında kilit hâlâ tutuluyor — sayı 1’e düşüyor, sıfıra değil — ve ancak dış bloktan da çıkıldığında sayı 0’a iniyor. Bu, izleyici kilidinin de (built-in synchronized) taşıdığı ama dışarı sayı olarak vermediği bir özelliktir: bir yöntem kendi çağırdığı başka bir senkronize yöntemde aynı kilide yeniden girdiğinde kilitlenmiyor, çünkü kilit iş parçacığına aittir, çağrıya değil — aynı iş parçacığı kendi tuttuğu kilidi tekrar isteyebiliyor.

Yeniden girilebilirlik olmasaydı, senkronize bir yöntemin kendi sınıfındaki başka bir senkronize yöntemi çağırması kendi kendini kilitlemek anlamına gelirdi — bir iş parçacığı, zaten sahip olduğu bir kilidi ikinci kez istediği için sonsuza kadar beklerdi. Bu, tek bir iş parçacığı içinde oluşabilecek en küçük kilitlenme biçimidir ve tutma sayısının varlığı tam olarak bunu önlüyor: çerçeve her lock() çağrısında “bu kilit zaten benim mi” diye soruyor, yanıt evetse engellemiyor, sayıyı artırıp geçiyor. holdsLock’ın kendisi bu sayıyı dışarı vermiyor, çünkü sorduğu soru farklı — yalnız “tutuyor muyum” diye soruyor, “kaç kez tutuyorum” diye sormuyor; iki soru da bu derste ayrı araçlarla yanıtlanıyor.

Kilit Nesneye Aittir, Koda Değil

  • EZ10 — Üç kurulumun üçü de aynı iki satırlık günlük desenini kullanıyor: bir iş parçacığı girdiğinde "X girdi" yazıyor, çıkarken "X cikti" yazıyor. Günlüğün sırası, hangi kilidin paylaşıldığını kanıtlıyor — süre hiçbir yerde ölçülmüyor.
// Kutu.java — kilit nesneye aittir, koda degil: senkronize yontem ornek kilidini, static yontem sinif kilidini kullanir
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class Kutu {
    synchronized void gir(List<String> gunluk, String etiket, CountDownLatch girdi, CountDownLatch devamEt)
            throws InterruptedException {
        gunluk.add(etiket + " girdi");
        if (girdi != null) girdi.countDown();
        if (devamEt != null) devamEt.await();
        gunluk.add(etiket + " cikti");
    }

    static synchronized void sinifGir(List<String> gunluk, String etiket) {
        gunluk.add(etiket + " girdi (sinif kilidi)");
        gunluk.add(etiket + " cikti (sinif kilidi)");
    }
}

gir, kendini çağıran iş parçacığını isteğe bağlı olarak durdurabiliyor: girdi verilmişse girer girmez haber veriyor, devamEt verilmişse serbest bırakılana kadar kilidi elinde tutuyor. Üç kurulum bu iki kolu farklı biçimde kullanıyor.

// KacAyriKilit.java — uc kurulumda kac ayri kilit devreye giriyor
import java.util.*;
import java.util.concurrent.CountDownLatch;

public class KacAyriKilit {
    static List<String> kurulum1AyniOrnek() throws InterruptedException {
        Kutu kutu = new Kutu();
        List<String> gunluk = Collections.synchronizedList(new ArrayList<>());
        CountDownLatch girdiA = new CountDownLatch(1);
        CountDownLatch bekleA = new CountDownLatch(1);

        Thread a = new Thread(() -> { try { kutu.gir(gunluk, "A", girdiA, bekleA); } catch (InterruptedException e) { } });
        a.start();
        girdiA.await(); // A kilidi aldi ve tutuyor

        Thread b = new Thread(() -> { try { kutu.gir(gunluk, "B", null, null); } catch (InterruptedException e) { } });
        b.start(); // B ayni kilidi istiyor, A birakmadikca giremez

        bekleA.countDown(); // A'yi serbest birak
        a.join();
        b.join();
        return gunluk;
    }

    static List<String> kurulum2FarkliOrnek() throws InterruptedException {
        Kutu kutu1 = new Kutu();
        Kutu kutu2 = new Kutu();
        List<String> gunluk = Collections.synchronizedList(new ArrayList<>());
        CountDownLatch girdiA = new CountDownLatch(1);
        CountDownLatch bekleA = new CountDownLatch(1);
        CountDownLatch bittiB = new CountDownLatch(1);

        Thread a = new Thread(() -> { try { kutu1.gir(gunluk, "A", girdiA, bekleA); } catch (InterruptedException e) { } });
        a.start();
        girdiA.await(); // A, kutu1'in kilidini tutuyor

        Thread b = new Thread(() -> {
            try { kutu2.gir(gunluk, "B", null, null); bittiB.countDown(); }
            catch (InterruptedException e) { }
        });
        b.start();
        bittiB.await(); // B, A hala beklerken tamamlandi -- ayri bir kilit kullaniyor demektir

        bekleA.countDown();
        a.join();
        b.join();
        return gunluk;
    }

    static List<String> kurulum3OrnekVeSinif() throws InterruptedException {
        Kutu kutu = new Kutu();
        List<String> gunluk = Collections.synchronizedList(new ArrayList<>());
        CountDownLatch girdiA = new CountDownLatch(1);
        CountDownLatch bekleA = new CountDownLatch(1);
        CountDownLatch bittiB = new CountDownLatch(1);

        Thread a = new Thread(() -> { try { kutu.gir(gunluk, "A", girdiA, bekleA); } catch (InterruptedException e) { } });
        a.start();
        girdiA.await(); // A, ornek kilidini tutuyor

        Thread b = new Thread(() -> { Kutu.sinifGir(gunluk, "B"); bittiB.countDown(); });
        b.start();
        bittiB.await(); // B, A hala beklerken tamamlandi -- sinif kilidi ayri

        bekleA.countDown();
        a.join();
        b.join();
        return gunluk;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1. ayni ornek uzerinde iki cagri:");
        System.out.println("   " + kurulum1AyniOrnek());
        System.out.println("2. iki ayri ornek uzerinde iki cagri:");
        System.out.println("   " + kurulum2FarkliOrnek());
        System.out.println("3. ornek yontemi ve sinif yontemi:");
        System.out.println("   " + kurulum3OrnekVeSinif());
    }
}
1. ayni ornek uzerinde iki cagri:
   [A girdi, A cikti, B girdi, B cikti]
2. iki ayri ornek uzerinde iki cagri:
   [A girdi, B girdi, B cikti, A cikti]
3. ornek yontemi ve sinif yontemi:
   [A girdi, B girdi (sinif kilidi), B cikti (sinif kilidi), A cikti]

Her üç kurulumda da A, bekleA.countDown() çağrılana kadar kilidi bırakmıyor. Birinci kurulumda B’nin günlüğü A tamamen çıktıktan sonra başlıyor — B, gir yönteminin ilk satırına bile ulaşamıyor, çünkü senkronize yöntemin gövdesine girmek için önce aynı kilidi alması gerekiyor ve o kilit A’da. İkinci ve üçüncü kurulumlarda ise B’nin hem girişi hem çıkışı, A henüz kilidi bırakmadan günlüğe düşüyor — B’nin tamamlanması A’nın bırakmasını hiç beklemiyor. Bu, iki ayrı kilidin devrede olduğunun doğrudan kanıtı: eğer aynı kilit paylaşılıyor olsaydı, B de birinci kurulumdaki gibi A’nın çıkışını beklemek zorunda kalırdı.

Kanıtın gücü tam olarak burada yatıyor: hiçbir satır “iki ayrı kilit var” diye iddia etmiyor, yalnız bir sıralama gözleniyor ve o sıralama başka türlü açıklanamıyor. B’nin bittiB.countDown() çağırabilmiş olması, gir ya da sinifGir yönteminin gövdesini tamamen çalıştırmış olduğu anlamına geliyor; bu da o gövdeye giren senkronize bloğun kilidini almış ve bırakmış olduğu anlamına geliyor. A ise aynı anda kendi kilidini hâlâ tutuyor — bekleA henüz düşmedi. İki iş parçacığı da bir kilit tutarken üçüncü kurulumda kilitlenme oluşmuyor, çünkü iki kilit ayrı nesnelere ait; birinci kurulumda ise B’nin girdi parametresi null olduğu için B’nin ne zaman kilide girdiğine dair bağımsız bir sinyal yok — ama günlükteki sıra zaten tek olasılığı bırakıyor.

Kurulum Çağrılar Ayrı kilit sayısı
1. aynı örnek kutu.gir × 2 1
2. iki ayrı örnek kutu1.gir, kutu2.gir 2
3. örnek + sınıf yöntemi kutu.gir, Kutu.sinifGir 2

Üç kurulumun ikisinde ayrı kilit sayısı ikiye çıkıyor. Örnek kilidi çağrıldığı nesneye bağlıyken, sınıf kilidi tek bir Class nesnesine bağlıdır ve bütün örnekler arasında paylaşılır — ama örnek kilidiyle aynı kilit değildir. Bir sınıfın hem örnek hem statik senkronize yöntemi varsa, aynı anda bir iş parçacığı örnek yöntemindeyken başka bir iş parçacığı statik yöntemi çağırabilir; ikisi birbirini hiç engellemez.

Bu ayrım, synchronized kullanırken hangi nesnenin kilit olarak seçildiğinin tasarım kararı olduğunu gösteriyor. Kutu.gir örtük olarak this üzerinde kilitleniyor — bu, bir sınıfın senkronize yöntemleri için varsayılan davranış. Aynı kilit, sınıfın dışından erişilen bir referanssa (this genel bir API’nin parçasıysa), sınıfın hiç bilmediği bir çağıran da synchronized (kutu) { ... } yazarak aynı kilide talip olabilir ve içeriden beklenmeyen bir çekişme yaratabilir. Kutu.gir bu dersin ölçümü için this’i kullanıyor, ama üçüncü kurulum zaten bunun tersini de gösteriyor: örnek kilidi ile sınıf kilidi birbirinden bağımsız olduğu gibi, bir sınıf kendi özel bir kilit nesnesini de kullanabilir — o zaman dışarıdan hiçbir çağıran o kilide synchronized ile ulaşamaz, çünkü referansın kendisi hiç dışarı çıkmaz.

Sınırlayıcı Ölçüm: Kilit Sıra Vermez

  • EZ11 — Bu bölümde bekleyen iş parçacıklarının hangi sırayla kilide gireceği koşturularak ölçülmüyor; ölçülseydi sonucu çizelgeleyiciye bağlı olurdu ve bu, kursun kurallarına aykırı bir yarış koşulu koşturmak olurdu.

Yukarıdaki üç kurulumun hepsinde yalnız iki iş parçacığı vardı ve hangisinin bekleyeceği baştan biliniyordu. Aynı kilidi isteyen üçüncü, dördüncü bir iş parçacığı eklendiğinde, kilit serbest kalınca bekleyenlerden hangisinin gireceği synchronized’ın kendisi tarafından belirtilmez — karşılıklı dışlamayı garanti eder, sırayı garanti etmez. ReentrantLock’ın kurucusuna true verilerek talep edilen adil kilit seçeneği bekleme sırasına yaklaşmayı hedefler, ama bu ders o sırayı bir koşumdan ölçmüyor; seçeneğin varlığı ve amacı burada yalnız modelleniyor. Aynı kilidi isteyen çok sayıda iş parçacığının birbirini süresiz bekletmesi ayrı bir kusurdur ve M01/K05 İşletim Sistemi Kavramları kursunda kavram olarak zaten kuruldu; bu ders onu yeniden kurmuyor, yalnız kilidin sıra vermediğini ölçüm dışı bırakarak hatırlatıyor.

Adil kilidin neden varsayılan olmadığı da modellenen bir gerekçedir, koşulan bir sayı değil: sırayı zorlamak, her lock() çağrısında bekleme kuyruğunun başını kontrol etmeyi gerektiriyor, ve bu kontrol yalnız bekleyen varsa değil her seferinde işliyor. synchronized ve ReentrantLock’ın adil olmayan biçimi, boşta bir kilide yeni gelen bir çağrının kuyruğu hiç yoklamadan doğrudan girmesine izin veriyor. Bu ders bu iki seçenek arasındaki maliyet farkını ölçmüyor, çünkü ölçmek demek gerçek bir sürüm karşılaştırması yapmak demek olurdu; tek belirttiği, sıranın isteğe bağlı bir garanti olduğu ve varsayılanın bunu vermediğidir. Bir kilidi bekleyen iş parçacıklarının sayısı arttıkça hangi tasarımın seçileceği bu dersin sorusu değil — bu ders yalnız, seçim yapılmadığı sürece hangi garantinin olmadığını sabitliyor.

Üçüncü kurulumdaki sınıf kilidi de kendi içinde tekildir: Kutu sınıfının bütün statik senkronize yöntemleri aynı Class nesnesinin kilidini paylaşır, tıpkı bir örneğin bütün örnek senkronize yöntemlerinin o örneğin kilidini paylaşması gibi. Bir sınıfın iki ayrı statik senkronize yöntemi aynı anda iki ayrı iş parçacığından çağrılsaydı, bu kurulum onları da birinci kurulumdaki gibi sıraya sokardı — orada ölçülmedi, çünkü bu dersin sorusu zaten örnek kilidiyle sınıf kilidinin birbirinden ayrı olup olmadığıydı, sınıf kilidinin kendi içinde tekil olup olmadığı değil.

Özet

  • Thread.holdsLock bir kilidin çağıran tarafından o an tutulup tutulmadığını belirlenimci biçimde söyler; ReentrantLock.getHoldCount() yeniden girme derinliğini sayar ve iç içe iki girişte 0’dan 2’ye çıkıp geri 0’a iner.
  • Bir kilit iş parçacığına aittir, çağrıya değil: aynı iş parçacığı zaten tuttuğu kilide yeniden girdiğinde engellenmez.
  • Örnek kilidi çağrıldığı nesneye bağlıdır: aynı senkronize yöntem iki ayrı örnekte çağrıldığında iki ayrı kilit kullanılır; sınıf kilidi bunlardan ayrı üçüncü bir kilittir.
  • Üç kurulumun yalnız birinde (aynı örnek üzerindeki iki çağrı) tek kilit paylaşılıyor; kalan ikisinde ayrı kilit sayısı ikiye çıkıyor.
  • Kilit karşılıklı dışlama ve görünürlük sağlar, bekleyenler arasında sıra vermez; sıra isteniyorsa adil kilit seçeneği modellenir, koşumdan ölçülmez.

Sonraki Adım

Bu üç kurulumda da kilidi elle almak ve bırakmak çağıranın işiydi — synchronized bloğu ya da acikKilit.lock()/unlock() çifti her seferinde elle yazıldı, ve her yazımda kilidin serbest bırakılmasını garanti etmek finally bloğuna düştü. KacAyriKilit’teki her üç kurulum da bu deseni tekrarladı; gerçek bir programda bu tekrar, unutulan bir unlock() çağrısının kilidi süresiz tuttuğu bir kusura dönüşebilir. Sıradaki ders bu elle tutma işini standart kitaplığın hazır tiplerine devrediyor: bir sayaç kendi kilidini kendi yönetip yönetmediğini, bir mandal bekleyeni ne zaman serbest bıraktığını, bir liste yazarken kimin ödediğini ölçüyor — kilit artık kaynak metinde görünmüyor, ama iş hâlâ birinin üzerine düşüyor; sıradaki soru bu yükün hangi tarafa yüklendiği.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat