Açıkça, notify
bekleme kümesindeki bir iş parçacığını uyandırır (herhangi bir), bekleme kümesindeki notifyAll
tüm iş parçacıklarını uyandırır. Aşağıdaki tartışma şüpheleri gidermelidir. notifyAll
çoğu zaman kullanılmalıdır. Hangisini kullanacağınızdan emin değilseniz, notifyAll
lütfen kullanın.Lütfen aşağıdaki açıklamaya bakın.
Çok dikkatli okuyun ve anlayın. Herhangi bir sorunuz varsa lütfen bana bir e-posta gönderin.
Üretici / tüketiciye bakın (varsayım iki yöntemle bir ProducerConsumer sınıfıdır). KIRILMIŞTIR (çünkü kullanır notify
) - evet işe yarayabilir - çoğu zaman bile, ama aynı zamanda kilitlenmeye neden olabilir - nedenini göreceğiz:
public synchronized void put(Object o) {
while (buf.size()==MAX_SIZE) {
wait(); // called if the buffer is full (try/catch removed for brevity)
}
buf.add(o);
notify(); // called in case there are any getters or putters waiting
}
public synchronized Object get() {
// Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
while (buf.size()==0) {
wait(); // called if the buffer is empty (try/catch removed for brevity)
// X: this is where C1 tries to re-acquire the lock (see below)
}
Object o = buf.remove(0);
notify(); // called if there are any getters or putters waiting
return o;
}
BİRİNCİ OLARAK,
Neden beklemeyi çevreleyen bir while döngüsüne ihtiyacımız var?
while
Bu durumu yakalayabilmemiz için bir döngüye ihtiyacımız var :
Tüketici 1 (C1) senkronize bloğa girer ve tampon boştur, bu nedenle C1 bekleme setine ( wait
çağrı yoluyla ) konur . Tüketici 2 (C2), senkronize edilmiş yönteme girmek üzere (yukarıdaki Y noktasında), ancak Üretici P1 arabelleğe bir nesne koyar ve daha sonra çağırır notify
. Bekleyen tek iş parçacığı C1'dir, bu nedenle uyandırılır ve şimdi X noktasında (yukarıda) nesne kilidini yeniden almaya çalışır.
Şimdi C1 ve C2 senkronizasyon kilidini almaya çalışıyor. Bunlardan biri (belirsiz olarak) seçilir ve yönteme girer, diğeri bloke edilir (beklemez - ancak bloke edilir, yöntem üzerindeki kilidi almaya çalışır). Diyelim ki C2 önce kilidi alıyor. C1 hala bloke oluyor (X'te kilidi almaya çalışıyor). C2 yöntemi tamamlar ve kilidi serbest bırakır. Şimdi, C1 kilidi alır. Ne oldu, bir while
döngü var şanslıyız , çünkü C1 döngü kontrolünü (koruma) gerçekleştirir ve var olmayan bir elemanı tampondan çıkarması engellenir (C2 zaten var!). Eğer bir tane while
olmasaydı IndexArrayOutOfBoundsException
, C1 ilk öğeyi tampondan kaldırmaya çalışırken bir tane alırdık!
ŞİMDİ,
Tamam, şimdi neden bildirmemiz gerekiyor?
Yukarıdaki üretici / tüketici örneğinde, kurtulabileceğimiz anlaşılıyor notify
. Öyle görünüyor, çünkü üretici ve tüketici için bekleme döngülerindeki korumaların karşılıklı olarak birbirini dışladığını kanıtlayabiliriz . Yani, put
yöntemde olduğu kadar yöntemde de bekleyen bir iş parçacığımız olamaz gibi görünüyor get
, çünkü bunun doğru olması için aşağıdakilerin doğru olması gerekir:
buf.size() == 0 AND buf.size() == MAX_SIZE
(MAX_SIZE öğesinin 0 olmadığını varsayalım)
ANCAK, bu yeterince iyi değil, kullanmamız GEREKİR notifyAll
. Bakalım neden ...
1 boyutunda bir tamponumuz olduğunu varsayın (örneği takip etmeyi kolaylaştırmak için). Aşağıdaki adımlar bizi kilitlenmeye götürür. Bir iş parçacığının bildirimle uyandığı HER ZAMAN, JVM tarafından belirleyici olmayan bir şekilde seçilebileceğini unutmayın - herhangi bir bekleyen iş parçacığı uyandırılabilir. Ayrıca, bir yönteme girişte birden fazla iş parçacığı engellendiğinde (yani bir kilit edinmeye çalışıldığında), edinme sırasının belirleyici olmayabileceğini unutmayın. Ayrıca bir iş parçacığının herhangi bir zamanda yalnızca bir iş parçacığında olabileceğini unutmayın - eşzamanlı yöntemler, sınıftaki herhangi bir (eşzamanlı) yöntemi yalnızca bir iş parçacığının yürütmesine (yani kilidini tutma) izin verir. Aşağıdaki olaylar dizisi oluşursa - kilitlenme sonuçları:
ADIM 1:
- P1 tampona 1 karakter koyar
ADIM 2:
- P2 denemeleri put
- bekleme döngüsünü kontrol eder - zaten bir karakter - bekler
ADIM 3:
- P3 denemeleri put
- bekleme döngüsünü kontrol eder - zaten bir karakter - bekler
ADIM 4:
- C1-girişimleri 1 karakter için
giriş bloklar - 1 karakter için C2-girişimleri - get
yönteme
giriş bloklar - C3 girişimleri 1 karakter için - get
bir yöntem
ADIM 5:
- C1 get
yöntemi uyguluyor - karakter, çağrılar notify
, çıkış yöntemini alıyor
- notify
P2 uyanıyor
- BUT, C2, P2'nin yapabilmesi için yönteme giriyor (P2, kilidi yeniden almak zorundadır), bu nedenle P2, put
yönteme girişte bloke eder
- C2 bekleme döngüsünü kontrol eder, arabellekte daha fazla karakter olmaz, bu yüzden bekler
- C3 C2'den sonra yönteme girer, ancak P2'den önce bekleme döngüsünü kontrol eder, arabellekte daha fazla karakter yok, bu yüzden bekler
6.ADIM:
- ŞİMDİ: P3, C2 ve C3 bekliyor!
- Son olarak P2 kilidi alır, arabelleğe bir karakter koyar, bildirim çağırır, yöntemden çıkar
ADIM 7:
- P2'nin bildirimi P3'ü uyandırır (herhangi bir ipliğin uyandırılabileceğini unutmayın)
- P3 bekleme döngüsü koşulunu kontrol eder, tamponda zaten bir karakter vardır, bu yüzden bekler.
- BİLDİRİMİ ÇAĞIRMAK İÇİN DAHA FAZLA İPLİK VE KALICI OLARAK ÜÇLÜ İPLİKLER YOK!
ÇÖZÜM yerine notify
ile notifyAll
üretici / tüketici kodu (yukarıda).