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
- EZ9 —
Thread.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 iseReentrantLock.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.holdsLockbir 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şte0’dan2’ye çıkıp geri0’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.