Java: notify () ile notifyAll ()


377

Bir Google arasında " notify()ve arasındaki fark notifyAll()" için çok fazla açıklama açılır (javadoc paragraflarını birbirinden ayırır). Her şey uyanmakta olan bekleyen iş parçacığı sayısına kadar kaynar: biri içeri notify()ve hepsi içeri notifyAll().

Ancak (bu yöntemler arasındaki farkı doğru anlarsam), daha fazla monitör edinimi için her zaman yalnızca bir iş parçacığı seçilir; ilk durumda VM tarafından seçilen, ikinci durumda sistem iş parçacığı zamanlayıcısı tarafından seçilen. Her ikisi için (genel durumda) kesin seçim prosedürleri programcı tarafından bilinmemektedir.

Ne var kullanışlı arasındaki fark bildirmek () ve notifyAll () o zaman? Bir şey mi kaçırıyorum?


6
Eşzamanlılık için kullanılacak kullanışlı kütüphaneler eşzamanlılık kütüphanelerinde bulunur. Bunların hemen hemen her durumda daha iyi bir seçim olduğunu düşünüyorum. Eşzamanlılık kütüphanesi öncesi tarih Java 5.0 (2004'te standart olarak
eklendiler

4
Peter ile aynı fikirde değilim. Eşzamanlılık kütüphanesi Java uygulanan ve yerine iyi eski eşzamanlılık kitaplığını kullanarak kendinize tekme vb Java kodu bir çok kilit diyoruz her zaman () yürütülen kilidini () bulunmaktadır edilir synchronizedbelli haricinde , oldukça nadir kullanım durumları.
Alexander Ryzhov

2
Önemli yanlış anlama şöyledir : ... daha fazla monitör alımı için her zaman sadece bir evre seçilir; ilk durumda VM tarafından seçilen, ikinci durumda sistem iş parçacığı zamanlayıcısı tarafından seçilen. Bunun anlamı esasen aynı olmasıdır. Açıklandığı gibi davranış doğru olsa da, eksik olan şey notifyAll(), ilkinden sonraki diğer iş parçacıkları uyanık kalır ve monitörü tek tek elde eder. Bu notifydurumda, diğer ipliklerin hiçbiri uyandırılmaz. Yani işlevsel olarak çok farklılar!
BeeOnRope

1) Bir nesnede çok sayıda iş parçacığı varsa ve o nesnede yalnızca bir kez notify () çağrılır. Bekleyen iş parçacıklarından biri dışında kalan iş parçacıkları sonsuza kadar bekler mi? 2) notify () kullanılırsa, bekleyen birçok iş parçacığından yalnızca biri yürütülmeye başlar. Notifyall () kullanılırsa, bekleyen tüm iş parçacıkları bildirilir, ancak bunlardan yalnızca biri yürütülmeye başlar, bu nedenle burada notifyall () kullanımı nedir?
Chetan Gowda

@ChetanGowda Tüm konuları bildirmek vs Tam olarak sadece rastgele bir iş parçacığına haber vermek, görünüşte ince ama önemli fark bize çarpana kadar gerçekten önemli bir farka sahiptir. / sinyal. Tüm bildirilmesi, bütün ipler başka bildirimde bulunmadan birbiri ardına bazı emir birinde işleme ve tamamlanacaktır - burada ipler vardır demeliyim blockeddeğil waiting.her blockedbaşka bir iş parçacığı içinde olduğu kadar onun exec geçici olarak askıya alınır syncblokta.
user104309

Yanıtlar:


248

Ancak (bu yöntemler arasındaki farkı doğru anlarsam), daha fazla monitör edinimi için her zaman yalnızca bir iş parçacığı seçilir.

Bu doğru değil. çağrılarda engellenen tümo.notifyAll() evreleri uyandırır . İpler sadece gelen dönmelerine izin verilmesi biri-birer, ama onlar her edecektir onların dönüş olsun.o.wait()o.wait()


Basitçe söylemek gerekirse, iş parçacıklarınızın neden bildirilmeyi beklediğine bağlıdır. Bekleyen mesaj dizilerinden birine bir şey olduğunu söylemek ister misiniz, yoksa hepsini aynı anda söylemek ister misiniz?

Bazı durumlarda, bekleme bittikten sonra tüm bekleyen iş parçacıkları yararlı eylemler gerçekleştirebilir. Örnek olarak, belirli bir görevin bitmesini bekleyen bir dizi iş parçacığı gösterilebilir; görev tamamlandığında, bekleyen tüm evreler işlerine devam edebilir. Böyle bir durumda, bekleyen tüm evreleri aynı anda uyandırmak için notifyAll () yöntemini kullanırsınız .

Başka bir durum, örneğin karşılıklı olarak kilitleme, bekleyen ipliklerden sadece biri bildirildikten sonra yararlı bir şey yapabilir (bu durumda kilidi elde eder). Böyle bir durumda, notify () yöntemini kullanmayı tercih edersiniz . Düzgün sen, uygulanan olabilir kullanmak notifyAll () yanı bu durumda, ama yine de bir şey yapamaz konuları uyanmak gereksiz olacaktır.


Çoğu durumda, bir koşulu bekleyen kod bir döngü olarak yazılır:

synchronized(o) {
    while (! IsConditionTrue()) {
        o.wait();
    }
    DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}

Bu şekilde, bir o.notifyAll()çağrı birden fazla bekleyen iş parçacığını uyandırırsa ve o.wait()markalardan geri dönen ilk kişi durumu yanlış durumda bırakırsa, uyanmış olan diğer iş parçacıkları beklemeye geri döner.


29
bir nesneyi yalnızca bir iş parçacığına bildirirseniz ancak birden çok nesne üzerinde beklerse, VM hangisini bildireceğini nasıl belirler?
katılımcı

6
Java özellikleri hakkında kesin olarak söyleyemem, ancak genellikle bu tür ayrıntılar hakkında varsayımlar yapmaktan kaçınmalısınız. Yine de VM'nin bunu aklı başında ve çoğunlukla adil bir şekilde yapacağını varsayabilirsiniz.
Liedman

15
Liedman ciddi şekilde yanlıştır, Java belirtimi açık bir şekilde notify () yönteminin adil olduğunu garanti etmemektedir. yani her bildirim çağrısı aynı iş parçacığını tekrar uyandırabilir (monitördeki iş parçacığı sırası FAIR veya FIFO DEĞİLDİR). Ancak programlayıcının adil olduğu garanti edilir. Bu nedenle, 2'den fazla iş parçacığının olduğu çoğu durumda bildirmeyi tercih etmelisiniz.
Yann TM

45
@YannTM Ben hep yapıcı eleştiri içindeyim ama bence tonunuz biraz haksız. "Kesinlikle söyleyemem" ve "Sanırım" dedim. Kolaylaşın, hiç yedi yıl önce% 100 doğru olmayan bir şey yazdınız mı?
Liedman

10
Sorun şu ki, kabul edilen cevap bu, kişisel bir gurur meselesi değil. Şimdi yanıldığınızı biliyorsanız, lütfen yanıtınızı düzenleyin ve aşağıdaki xagyg pedagojik ve doğru cevabı işaret edin.
Yann TM

330

Açıkça, notifybekleme kümesindeki bir iş parçacığını uyandırır (herhangi bir), bekleme kümesindeki notifyAlltü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, notifyAlllü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?

whileBu 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 whiledö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 whileolmasaydı 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, putyö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 - getyönteme
giriş bloklar - C3 girişimleri 1 karakter için - getbir yöntem

ADIM 5:
- C1 getyöntemi uyguluyor - karakter, çağrılar notify, çıkış yöntemini alıyor
- notifyP2 uyanıyor
- BUT, C2, P2'nin yapabilmesi için yönteme giriyor (P2, kilidi yeniden almak zorundadır), bu nedenle P2, putyö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 notifyile notifyAllüretici / tüketici kodu (yukarıda).


1
finnw - P3 durumu yeniden kontrol etmelidir, çünkü notifyP3 nedenleri (bu örnekte seçilen iplik) beklediği noktadan (yani whiledöngü içinde) ilerlemesine neden olur . Kilitlenmeye neden olmayan başka örnekler de vardır, ancak bu durumda notifykilitlenmeyen kodun kullanılması garanti edilmez. Kullanımı notifyAllyapar.
xagyg

4
@marcus Çok yakın. NotifyAll ile, her iş parçacığı kilidi (her seferinde bir tane) yeniden alacaktır, ancak bir iş parçacığı kilidi yeniden aldıktan ve yöntemi uyguladıktan sonra (ve sonra çıktıktan sonra) ... bir sonraki iş parçacığı kilidi yeniden ele geçirir, "while" komutunu kontrol eder. ve "beklemek" için geri dönecektir (koşulun ne olduğuna bağlı olarak). Bu nedenle, doğru bir şekilde belirttiğiniz gibi bir iş parçacığını uyandırır. Tüm tüm iş parçacıklarını uyandırır ve her iş parçacığı her seferinde kilidi yeniden alır - "while" durumunu kontrol eder ve yöntemi uygular veya "bekler".
xagyg

1
@xagyg, her üreticinin saklayacağı tek bir karakter içeren bir senaryodan mı bahsediyorsunuz? Öyleyse, örneğiniz doğrudur, ancak çok ilginç IMO değildir. Önerdiğim ek adımlar ile aynı sistemi kilitleyebilirsiniz, ancak sınırsız miktarda girdi ile - bu tür kalıplar genellikle gerçekte bu şekilde kullanılır.
eran

3
@codeObserver Siz sormuştunuz: "notifyAll () çağrısı, while () koşulunu aynı anda kontrol eden birden fazla bekleme iş parçacığına yol açacaktır .. ve bu nedenle, bir süre önce uygun hale getirilmeden önce 2 iş parçacığının zaten dışarıda çıkmasına neden olma olasılığı vardır. istisna? " Hayır, bu mümkün değildir, çünkü birden fazla iş parçacığı uyanacak olsa da, while durumunu aynı anda kontrol edemezler. Her biri, kod bölümüne tekrar girmeden ve süreyi tekrar kontrol etmeden önce kilidi yeniden beklemelidir (beklemeden hemen sonra). Bu nedenle, her seferinde bir tane.
xagyg

4
@xagyg güzel bir örnek. Bu asıl sorudan konu dışı; sadece tartışma uğruna. Kilitlenme bir tasarım sorunu imo (yanılıyorsam beni düzelt). Çünkü hem koy hem de al tarafından paylaşılan bir kilit var. Ve JVM, kilidi açtıktan sonra ve ayetten sonra koymak için yeterince akıllı değildir. Ölü kilit, put başka bir put uyandırır, çünkü while () nedeniyle kendisini wait () öğesine geri döndürür. İki sınıf (ve iki kilit) yapmak işe yarar mı? Yani {synchonized (get)} ifadesini koyun, {(synchonized (put)} ifadesini alın. Başka bir deyişle, get yalnızca uyanacak ve uyanacak, yalnızca uyanacak.
Jay

43

Faydalı farklılıklar:

  • Tüm bekleyen evreleriniz değiştirilebilirse (uyandırma sırası önemli değil) veya yalnızca bir bekleyen evreniz varsa notify () kullanın . Yaygın bir örnek, kuyruktan işleri yürütmek için kullanılan bir iş parçacığı havuzudur - bir iş eklendiğinde, iş parçacıklarından birine uyanma, bir sonraki işi yürütme ve uyku moduna geri dönme bildirilir.

  • Bekleyen evrelerin farklı amaçlara sahip olabileceği ve aynı anda çalışabilmesi gereken diğer durumlarda notifyAll () öğesini kullanın . Bir örnek, kaynağa erişmeden önce işlemin tamamlanmasını bekleyen birden çok iş parçacığının paylaşılan bir kaynakta yapılan bakım işlemidir.


19

Kaynakların nasıl üretildiğine ve tüketildiğine bağlı olduğunu düşünüyorum. Aynı anda 5 iş nesnesi varsa ve 5 tüketici nesneniz varsa, notifyAll () kullanarak tüm iş parçacıklarını uyandırmak, böylece her biri 1 iş nesnesini işleyebilir.

Kullanılabilir tek bir çalışma nesneniz varsa, tüm tüketici nesnelerini o nesne için yarışacak şekilde uyandırmanın anlamı nedir? Mevcut iş için ilk kontrol onu alacak ve diğer tüm iş parçacığı kontrol edecek ve yapacak bir şeyleri bulmak.

Burada harika bir açıklama buldum . Kısacası:

Notify () yöntemi genellikle, kaynak alan keyfi sayıda "tüketici" veya "işçi" bulunan kaynak havuzları için kullanılır , ancak havuza bir kaynak eklendiğinde, bekleyen tüketicilerden veya çalışanlardan yalnızca biri Bununla birlikte. NotifyAll () yöntemi aslında diğer çoğu durumda kullanılır. Kesinlikle, birden fazla garsonun devam etmesine izin verebilecek bir durumu garsonlara bildirmek gerekir. Ancak bunu bilmek genellikle zordur. Genel bir kural olarak, notify () kullanmak için belirli bir mantığınız yoksa, muhtemelen notifyAll () kullanmalısınız , çünkü belirli bir nesnede tam olarak hangi iş parçacıklarının bekleyeceğini ve nedenini bilmek genellikle zordur.


11

Eşzamanlılık yardımcı programlarında , bu yöntemler arasında denilen signal()ve arasında seçim signalAll()yapabileceğinizi unutmayın. Böylece soru, ile de geçerli kalır java.util.concurrent.

Doug Lea, ünlü kitabında ilginç bir nokta getiriyor : a notify()ve Thread.interrupt()aynı zamanda gerçekleşirse, bildirim gerçekten kaybolabilir. Bu olabilir ve dramatik etkileri notifyAll()varsa, genel gider fiyatını ödemenize rağmen daha güvenli bir seçimdir (çoğu zaman çok fazla iş parçacığını uyandırmak).


10

Kısa özet:

Her zaman tercih ) notifyAll ( over ) (haber Eğer parçacığı çok sayıda hepsi aynı şeyi güçlü bir paralel uygulama yoksa.

Açıklama:

notify () [...] tek bir iş parçacığını uyandırır. Çünkü () bildirmek olduğunu parçacığı çok sayıda geleni yaptığını benzer işlerinde programlara - Eğer uyandı olan ipliği belirtmek izin vermiyor, sadece güçlü paralel uygulamalarda yararlıdır. Böyle bir uygulamada, hangi ipliğin uyandırıldığı umurumda değil.

kaynak: https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

Yukarıda açıklanan durumda notify () öğesini notifyAll () ile karşılaştırın : iş parçacıklarının aynı şeyi yaptığı büyük ölçüde paralel bir uygulama. Eğer ararsanız notifyAll () bu durumda, notifyAll () sadece bir iplik aslında verilecek, yani iplik devam edebilirsiniz beri (gereksiz birçoğu parçacığı çok büyük sayıda uyanma (yani zamanlama) neden olur nesne izlemek bekleme () , () bildirmek veya ) (notifyAll çağrıldı , böylece bilgi işlem kaynaklarını boşa .

Eğer parçacığı çok sayıda eşzamanlı aynı şeyi bir uygulama yoksa Böylece, tercih notifyAll () üzerinde bildirmek () . Neden? Çünkü, diğer kullanıcılar bu forumda daha önce yanıtlamış oldukları gibi, bildir ()

bu nesnenin monitöründe bekleyen tek bir iş parçacığını uyandırır. [...] Seçim keyfidir ve uygulamanın takdirine bağlıdır.

kaynak: Java SE8 API ( https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify-- )

Tüketicilerin tüketmeye hazır (yani bekle () ), üreticilerin üretime hazır (yani bekle ()) ve üretici sırasının (üretilecek / tüketilecek) boş olduğu bir üretici tüketici uygulamanız olduğunu düşünün . Bu durumda, notify () yalnızca tüketicileri uyandırabilir ve asla üreticiyi uyandıramaz, çünkü uyanan seçim keyfidir . Üreticiler ve tüketiciler sırasıyla üretmeye ve tüketmeye hazır olmalarına rağmen, üretici tüketici döngüsü ilerleme kaydetmez. Bunun yerine, bir tüketici uyandırılır (yani wait () durumunu bırakmak ), boş olduğu için bir öğeyi kuyruktan çıkarmaz ve bildirir () devam etmesi için başka bir tüketiciyi .

Buna karşılık, notifyAll () hem üreticileri hem de tüketicileri uyandırır. Zamanlanan seçim zamanlayıcıya bağlıdır. Tabii ki, zamanlayıcının uygulamasına bağlı olarak, zamanlayıcı yalnızca tüketicileri zamanlayabilir (örneğin, tüketici iş parçacıklarına çok yüksek bir öncelik atarsanız). Bununla birlikte, buradaki varsayım, sadece zamanlayıcı çizelgeleme tehlikesinin JVM'nin sadece tüketicileri uyandırma tehlikesinden daha düşük olmasıdır, çünkü makul olarak uygulanan herhangi bir zamanlayıcı sadece keyfi kararlar vermez . Aksine, çoğu programlayıcı uygulaması en azından açlığı önlemek için biraz çaba sarf etmektedir.


9

İşte bir örnek. Çalıştır. Sonra notify () için notifyAll () yönteminden birini değiştirin ve ne olduğunu görün.

ProducerConsumerExample sınıfı

public class ProducerConsumerExample {

    private static boolean Even = true;
    private static boolean Odd = false;

    public static void main(String[] args) {
        Dropbox dropbox = new Dropbox();
        (new Thread(new Consumer(Even, dropbox))).start();
        (new Thread(new Consumer(Odd, dropbox))).start();
        (new Thread(new Producer(dropbox))).start();
    }
}

Dropbox sınıfı

public class Dropbox {

    private int number;
    private boolean empty = true;
    private boolean evenNumber = false;

    public synchronized int take(final boolean even) {
        while (empty || evenNumber != even) {
            try {
                System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
                wait();
            } catch (InterruptedException e) { }
        }
        System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
        empty = true;
        notifyAll();

        return number;
    }

    public synchronized void put(int number) {
        while (!empty) {
            try {
                System.out.println("Producer is waiting ...");
                wait();
            } catch (InterruptedException e) { }
        }
        this.number = number;
        evenNumber = number % 2 == 0;
        System.out.format("Producer put %d.%n", number);
        empty = false;
        notifyAll();
    }
}

Tüketici sınıfı

import java.util.Random;

public class Consumer implements Runnable {

    private final Dropbox dropbox;
    private final boolean even;

    public Consumer(boolean even, Dropbox dropbox) {
        this.even = even;
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            dropbox.take(even);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) { }
        }
    }
}

Üretici sınıfı

import java.util.Random;

public class Producer implements Runnable {

    private Dropbox dropbox;

    public Producer(Dropbox dropbox) {
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            int number = random.nextInt(10);
            try {
                Thread.sleep(random.nextInt(100));
                dropbox.put(number);
            } catch (InterruptedException e) { }
        }
    }
}

8

Joshua Bloch'tan, Java Guru'nun Etkili Java 2. sürümünde kendisi:

"Madde 69: Bekleme ve bildirimde bulunmak için eşzamanlılık yardımcı programlarını tercih et".


16
Neden kaynağından daha önemlidir.
Pacerier

2
@Pacerier Peki dedi. Sebepleri bulmakla daha fazla ilgilenirim. Olası bir neden, nesne sınıfındaki bekleme ve bildirimin örtük bir koşul değişkenine dayanması olabilir. Yani standart üretici ve tüketici örneğinde ..... hem üretici hem de tüketici, cevabında xagyg tarafından açıklandığı gibi bir kilitlenmeye yol açabilecek aynı durumu bekleyeceklerdir. Dolayısıyla daha iyi bir yaklaşım, docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
rahul

6

Umarım bu bazı şüpheleri giderir.

notify () : notify () yöntemi, kilidi bekleyen bir iş parçacığını uyandırır (bu kilitte wait () olarak adlandırılan ilk iş parçacığı).

notifyAll () : notifyAll () yöntemi kilidi bekleyen tüm evreleri uyandırır; JVM, kilidi bekleyen iş parçacığı listesinden iş parçacıklarından birini seçer ve bu iş parçacığını uyandırır.

Bir kilit bekleyen tek bir iş parçacığı durumunda, notify () ve notifyAll () arasında önemli bir fark yoktur. Ancak, kilidi bekleyen birden fazla iş parçacığı olduğunda, hem notify () hem de notifyAll () 'de, uyandırılan tam iş parçacığı JVM'nin kontrolü altındadır ve belirli bir iş parçacığının uyanmasını programlı olarak denetleyemezsiniz.

İlk bakışta, sadece bir iş parçacığını uyandırmak için notify () öğesini çağırmanın iyi bir fikir olduğu görülmektedir; tüm iplikleri uyandırmak gereksiz görünebilir. Ancak, notify () ile ilgili sorun, uyandırılan iş parçacığının uyandırılması için uygun olmayabilir (iş parçacığı başka bir durum için beklemiş olabilir veya bu iş parçacığı vb. İçin durum hala karşılanmamıştır). Bu durumda , notify () kaybolabilir ve başka hiçbir iş parçacığı uyanmaz ve bir tür çıkmaza neden olabilir (bildirim kaybolur ve diğer tüm iş parçacıkları sonsuza kadar bildirim bekler).

Bu sorunu önlemek için, bir kilidi bekleyen birden fazla iş parçacığı (veya beklemenin yapıldığı birden fazla koşul) olduğunda notifyAll () öğesini çağırmak her zaman daha iyidir. NotifyAll () yöntemi tüm evreleri uyandırır, bu nedenle çok verimli değildir. ancak bu performans kaybı gerçek dünya uygulamalarında ihmal edilebilir düzeydedir.


6

Bir iş parçacığı için üç durum vardır.

  1. WAIT - İş parçacığı herhangi bir CPU döngüsü kullanmıyor
  2. BLOCKED - Bir monitör almaya çalışırken iş parçacığı engellendi. CPU döngülerini kullanmaya devam ediyor olabilir
  3. ÇALIŞIYOR - İplik çalışıyor.

Şimdi, bir notify () çağrıldığında, JVM bir iş parçacığını alır ve monitör nesnesi için rekabet olmadığından onları BLOCKED durumuna ve dolayısıyla RUNNING Durumuna taşır.

Bir notifyAll () çağrıldığında, JVM tüm evreleri alır ve hepsini BLOCKED Durumuna taşır. Tüm bu iş parçacıkları nesnenin kilidini öncelikli olarak alacaktır.İlk önce monitörü elde edebilen iplik önce ÇALIŞMA durumuna geçebilecektir.


Sadece harika bir açıklama.
royatirek

5

Kimsenin rezil "kayıp uyandırma" sorunundan (google) bahsetmediğine çok şaşırdım.

Temelde:

  1. aynı koşulda bekleyen birden fazla iş parçacığınız varsa ve,
  2. A durumundan B durumuna geçiş yapabileceğiniz birden fazla iş parçacığı ve
  3. durum B'den durum A'ya geçiş yapabileceğiniz birden çok iş parçacığı (genellikle 1. ile aynı iş parçacıkları) ve,
  4. durum A'dan B'ye geçiş, 1'deki konuları bildirmelidir.

Bundan sonra, kaybolan uyanmaların imkansız olduğu kanıtlanabilir garantileriniz yoksa notifyAll'ı kullanmalısınız.

Yaygın bir örnek eşzamanlı bir FIFO kuyruğu olup, burada: birden çok enqueüre (yukarıdaki 1. ve 3.) sıranızı boştan boş olmayan birden fazla dequeutor'a (2. yukarıda) "sıra boş değil" boş durumunu bekleyebilir -> boş olmayan dequeuers bildirmelidir

Boş bir kuyruktan başlayarak 2 enqueuer ve 2 dequeuer'in etkileşime girdiği ve 1 enqueuer'ın uyumaya devam edeceği bir harmanlama işlemleri kolayca yazabilirsiniz.

Bu, kilitlenme sorunu ile karşılaştırılabilir bir problemdir.


Özür dilerim, xagyg bunu ayrıntılı olarak açıklıyor. Sorunun adı "kayıp uyandırma"
NickV

@Abhay Bansal: Sanırım condition.wait () kilidi serbest bırakır ve uyanan iplik tarafından yeniden kazanılır.
NickV

4

notify()bir iş parçacığını notifyAll()uyandırırken, her şeyi uyandırır. Bildiğim kadarıyla orta yol yok. Ancak notify()ipliklerinize ne yapacağınızdan emin değilseniz kullanın notifyAll(). Her zaman bir cazibe gibi çalışır.


4

Yukarıdaki tüm cevaplar doğru, söyleyebildiğim kadarıyla size başka bir şey söyleyeceğim. Üretim kodu için gerçekten java.util.concurrent sınıflarını kullanmalısınız. Java'daki eşzamanlılık alanında sizin için yapamayacakları çok az şey var.


4

notify()daha verimli kod yazmanızı sağlar notifyAll().

Birden çok paralel iş parçacığından yürütülen aşağıdaki kod parçasını düşünün:

synchronized(this) {
    while(busy) // a loop is necessary here
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notifyAll();
}

Aşağıdakiler kullanılarak daha verimli hale getirilebilir notify():

synchronized(this) {
    if(busy)   // replaced the loop with a condition which is evaluated only once
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notify();
}

Çok sayıda iş parçacığınız varsa veya bekleme döngüsü koşulunu değerlendirmek pahalıysa, notify()önemli ölçüde daha hızlı olacaktır notifyAll(). Örneğin, 1000 iş parçacığınız varsa, 999 iş parçacığı ilkinden sonra uyandırılır ve değerlendirilir notifyAll(), daha sonra 998, sonra 997 vb. Aksine, notify()çözüm ile sadece bir iplik uyanacaktır.

notifyAll()Sonraki işte hangi iş parçacığının yapılacağını seçmeniz gerektiğinde kullanın :

synchronized(this) {
    while(idx != last+1)  // wait until it's my turn
        wait();
}
...
synchronized(this) {
    last = idx;
    notifyAll();
}

Son olarak, uyanmış blokların notifyAll()içindeki kodun synchronized, bir kerede değil, sırayla yürütüleceğini anlamak önemlidir . Yukarıdaki örnekte bekleyen üç iş parçacığı olduğunu ve dördüncü iş parçacığı çağırdığını varsayalım notifyAll(). Her üç iş parçacığı da uyanacak, ancak yalnızca bir tanesi yürütmeye başlayacak ve whiledöngünün durumunu kontrol edecektir . Koşul ise true, wait()tekrar arar ve ancak o zaman ikinci iş parçacığı yürütmeye başlar ve whiledöngü durumunu kontrol eder , vb.


4

İşte daha basit bir açıklama:

Eğer notify () veya notifyAll () kullansanız da, hemen sonuç başka bir iş parçacığının monitörü alması ve yürütmeye başlamasıdır. (Aslında bazı nesnelerin bu nesne için beklemede () engellendiği varsayılarak, diğer ilgisiz dişler mevcut tüm çekirdekleri vb. Islatmaz.) Etki daha sonra gelir.

A, B ve C dişlerinin bu nesne üzerinde beklediğini ve A dişinin monitörü aldığını varsayalım. Fark, A monitörü serbest bıraktığında olanlarda yatar. Notify () yöntemini kullandıysanız, B ve C beklemede () hala engellenir: monitörde beklemiyorlar, bildirilmeyi bekliyorlar. A monitörü serbest bıraktığında, B ve C hala orada oturuyor ve bir bildirim bekliyor ().

NotifyAll () yöntemini kullandıysanız, B ve C'nin her ikisi de "bildirim için bekle" durumunu geçtiler ve her ikisi de monitörü almayı bekliyor. A monitörü serbest bıraktığında, B veya C onu alır (o monitör için başka hiçbir iş parçacığının rekabet etmediği varsayılarak) ve yürütmeye başlar.


Çok açık bir açıklama. Bu notify () davranışının sonucu "Missed Signal" / "Missed Notification" / "Missed Notification" ile sonuçlanan durumun kilitlenmesine neden olur / uygulama durumunun ilerleme durumu olmaz. P-Producer, C-Consumer P1, P2 ve C2 C1'i bekliyor. C1, notify () işlevini çağırır ve C2 uyandırılabilir ve böylece hem P1 hem de P2 bildirimi kaçırır ve daha fazla açık "bildirim" (bir notify () çağrısı) bekler.
user104309

4

Bu cevap tarafından mükemmel cevap grafiksel yeniden yazma ve sadeleştirme olan xagyg yorumlarına dahil eran .

Neden her ürün tek bir tüketici için tasarlanmış olsa bile notifyAll kullanıyorsunuz?

Aşağıdaki gibi basitleştirilmiş üreticileri ve tüketicileri düşünün.

Üretici:

while (!empty) {
   wait() // on full
}
put()
notify()

Tüketici:

while (empty) {
   wait() // on empty
}
take()
notify()

1 üretici arabellek paylaşan 2 üretici ve 2 tüketici olduğunu varsayalım. Aşağıdaki resim, tüm iş parçacıklarının bildirimde bulunması durumunda kaçınılması gereken bir kilitlenmeye yol açan bir senaryoyu göstermektedir .

Her bildirim, iş parçacığı uyandırılmış olarak etiketlenir.

bildirim nedeniyle kilitlenme


3

Uygulamada Java Eşzamanlılığı'nda açıklananlardan bahsetmek istiyorum:

İlk nokta, ister Bildir ister Bildir?

It will be NotifyAll, and reason is that it will save from signall hijacking.

İki A ve B iş parçacığı aynı durum kuyruğunun farklı durum tahminlerini bekliyorsa ve bildirim çağrılırsa, JVM'nin bildireceği JVM'ye kadardır.

Eğer bildirim A iş parçacığı ve JVM onaylı iş parçacığı B için yapıldıysa, B iş parçacığı uyanacak ve bu bildirimin yararlı olmadığını görecek, böylece tekrar bekleyecektir. Ve A İş parçacığı bu kaçırılan sinyali asla tanımayacak ve biri bildirimini kaçırmıştı.

Yani, notifyAll çağrılması bu sorunu çözecektir, ancak yine de tüm iş parçacıklarını bildireceği ve tüm iş parçacıkları aynı kilit için rekabet edeceği ve bağlam anahtarı ve dolayısıyla CPU üzerindeki yükü içereceği için performans etkisi olacaktır. Ancak performansı ancak doğru davranıyorsa, davranışının kendisi doğru değilse performansın faydası olmazsa önem vermeliyiz.

Bu sorun, her koşul yüklemesi için farklı bekleme sağladığı için jdk 5'te sağlanan açık kilitleme Kilidinin Koşul nesnesi kullanılarak çözülebilir. Burada doğru davranacak ve sinyal çağıracağı ve bu durumu yalnızca bir iş parçacığının beklediğinden emin olacağı için performans sorunu olmayacak


3

notify()- Nesnenin bekleme kümesinden rastgele bir iş parçacığı seçer ve bunu BLOCKEDdurumuna koyar . Nesnenin bekleme kümesindeki diğer iş parçacıkları hala WAITINGdurumudur.

notifyAll()- Nesnenin bekleme kümesinden tüm evreleri BLOCKEDduruma geçirir. Kullandıktan sonra notifyAll(), paylaşılan nesnenin bekleme kümesinde kalan hiçbir iş parçacığı yok, çünkü hepsi şimdi BLOCKEDdurumda ve durumda değil WAITING.

BLOCKED- kilit alımı için engellendi. WAITING- bildirim bekleniyor (veya katılımın tamamlanması için engellendi).


3

Etkili Java ile ilgili blogdan alınmıştır :

The notifyAll method should generally be used in preference to notify. 

If notify is used, great care must be taken to ensure liveness.

Yani, anladığım şey (yukarıda belirtilen blogdan, "Yann TM" tarafından kabul edilen cevap ve Java belgelerine yorum yapın ):

  • notify (): JVM, bu nesne üzerinde bekleyen iş parçacıklarından birini uyandırır. İplik seçimi adilli olmadan keyfi olarak yapılır. Böylece aynı iplik tekrar tekrar uyandırılabilir. Böylece sistemin durumu değişir, ancak gerçek bir ilerleme kaydedilmez. Böylece bir livelock oluşturma .
  • notifyAll (): JVM tüm evreleri uyandırır ve ardından tüm evreleri bu nesne üzerindeki kilit için yarışır. Şimdi, CPU zamanlayıcı bu nesne üzerinde kilit alan bir iş parçacığı seçer. Bu seçim süreci JVM'nin seçiminden çok daha iyi olacaktır. Böylece canlılığı sağlamak.

2

@Xagyg tarafından gönderilen koda bir göz atın.

: Varsayalım iki farklı konu iki farklı koşullar bekliyor birinci iplik bekliyor ve ikinci iplik bekliyor .
buf.size() != MAX_SIZEbuf.size() != 0

Bir noktada buf.size() 0'a eşit olmadığını varsayalım . JVM notify()yerine çağırır notifyAll()ve ilk iş parçacığı bilgilendirilir (ikincisi değil).

İlk iş parçacığı uyandırılır, buf.size()geri dönebileceği kontrol edilir MAX_SIZEve beklemeye geri döner. İkinci iş parçacığı uyandırılmaz, beklemeye devam eder ve çağrı yapmaz get().


1

notify()wait()aynı nesnede çağrılan ilk iş parçacığını uyandırır .

notifyAll()wait()aynı nesne üzerinde çağrılan tüm evreleri uyandırır .

Önce en yüksek öncelikli iş parçacığı çalışır.


13
Durumunda notify()bunun tam olarak "var ilk iplik ".
Bhesh Gurung

6
hangisinin VM tarafından seçileceğini tahmin edemezsiniz. Sadece Tanrı bilir.
Sergii Shevchyk

İlk kimin olacağının garantisi yok (adalet yok)
Ivan Voroshilin

Yalnızca işletim sistemi bunu garanti ederse ilk iş parçacığını uyandırır ve muhtemelen başaramaz. Hangi iş parçacığının uyanacağını belirlemek için işletim sistemini (ve zamanlayıcısını) gerçekten korur.
Paul Stelian

1

bildir yalnızca bekleyen durumdaki bir iş parçacığını bildirir, tüm bildirilenler bekleme durumundaki tüm iş parçacıklarını bildirir, şimdi tüm bildirilen iş parçacıkları ve engellenen tüm iş parçacıkları kilit için uygundur, bunlardan yalnızca biri kilidi alır ve diğerlerinin tümü (daha önce bekleme durumunda olanlar dahil) engellenmiş durumda olacaktır.


1

Yukarıdaki mükemmel ayrıntılı açıklamaları özetlemek ve aklıma gelen en basit şekilde, bunun nedeni 1) tüm senkronizasyon ünitesinde (blok veya nesne) ve 2) edinilmiş olan JVM yerleşik monitörün sınırlamalarıdır. hakkında / hakkında bekletilen / bildirilen belirli bir durum hakkında ayrımcılık yapmaz.

Bu, birden fazla iş parçacığının farklı koşullarda beklemesi ve notify () kullanılması durumunda, seçilen iş parçacığının yeni yerine getirilen koşulda ilerleme gösterecek iş parçacığı olmayabileceği anlamına gelir - bu iş parçacığına neden olan (ve halen bekleyen diğer iş parçacıkları koşulu yerine getirmek vb.) ilerleme kaydedememek, ve sonunda açlık ya da programın kapatılması.

Buna karşılık, notifyAll () tüm bekleyen evrelerin sonunda kilidi yeniden almasını ve ilgili durumlarını kontrol etmesini sağlar, böylece sonunda ilerlemenin yapılmasına izin verir.

Bu nedenle notify (), yalnızca aynı monitördeki tüm iş parçacıkları yalnızca bir ve aynı koşulu kontrol ettiğinde tatmin edilen bir beklemedeki iş parçacığının ilerlemesine izin verilmesi garanti edilirse güvenle kullanılabilir - oldukça nadir gerçek dünya uygulamalarında durum.


0

"Nesne" nin wait () yöntemini çağırdığınızda (nesne kilidinin elde edilmesini beklerken), stajyer bu nesnenin üzerindeki kilidi açığa çıkarır ve diğer nesnelerin bu "nesne" üzerinde kilitlenmesine yardımcı olur, bu senaryoda "kaynak / nesne" için bekleyen 1'den fazla iş parçacığı (diğer iş parçacıkları da aynı yukarıdaki nesne üzerinde beklemeyi yayınladı ve kaynak / nesneyi dolduran ve notify / notifyAll çağırır bir iş parçacığı olacak şekilde).

Burada aynı nesnenin bildirimini (işlemin / kodun aynı / diğer tarafından) yayınladığınızda, bu engellenen ve bekleyen bir iş parçacığını serbest bırakacaktır (bekleyen tüm iş parçacıkları değil - bu serbest iş parçacığı JVM İş parçacığı tarafından seçilecektir) Zamanlayıcı ve nesne üzerindeki tüm kilit alma işlemi normal ile aynıdır).

Bu nesne üzerinde paylaşılacak / üzerinde çalışacak yalnızca bir iş parçacığınız varsa, wait-notify uygulamanızda yalnızca notify () yöntemini kullanmanız uygundur.

iş mantığınıza bağlı olarak birden fazla iş parçacığının okuduğu ve kaynaklara / nesneye yazdığı bir durumdaysanız, notifyAll ()

şimdi nasıl tam olarak jvm tanımlamak ve bir nesne üzerinde notify () sorunu zaman bekleyen iş parçasını kırma arıyorum ...


0

Yukarıda bazı sağlam cevaplar olsa da, okuduğum karışıklık ve yanlış anlamaların sayısı beni şaşırttı. Bu muhtemelen kendi kırık eşzamanlı kodunu yazmak yerine java.util.concurrent kullanmak mümkün olduğu fikrini kanıtlıyor. Soruya geri dönelim: özetlemek gerekirse, bugün en iyi uygulama kayıp uyanma sorunu nedeniyle TÜM durumlarda notify () KAÇINMAK. Bunu anlamayan herkesin kritik kritik eşzamanlılık kodu yazmasına izin verilmemelidir. Sürü sorunundan endişe ediyorsanız, her seferinde bir ipliği uyandırmanın güvenli bir yolu şudur: 1. Bekleyen iplikler için açık bir bekleme kuyruğu oluşturmak; 2. Sıradaki her bir iş parçacığının selefi beklemesini sağlayın; 3. Her iş parçacığının tamamlandığında notifyAll () çağrmasını sağlayın. Veya Java.util.concurrent kullanabilirsiniz. *,


tecrübelerime göre, bekleme / bildirim kullanımı genellikle Runnablebir kuyruğun içeriğini işleyen bir iş parçacığının ( uygulama) kuyruk mekanizmalarında kullanılır . Ardından wait(), kuyruk her boşken kullanılır. Ve notify()bilgi eklendiğinde çağrılır. -> Böyle bir durumda, sadece 1 iş parçacığı vardır wait(), o zaman notifyAll()sadece 1 bekleyen iş parçacığı olduğunu biliyorsanız , kullanmak biraz aptalca görünmüyor .
bvdb

-2

Her şeyi uyandırmak burada çok önemli değil. beklemek bildirmek ve notifyall, tüm bunlar nesnenin monitör sahibi olduktan sonra konur. Bir iş parçacığı bekleme aşamasındaysa ve bildir çağrılırsa, bu iş parçacığı kilidi alır ve o noktada başka hiçbir iş parçacığı bu kilidi alamaz. Böylece eşzamanlı erişim hiç gerçekleşemez. Bildiğim kadarıyla beklemek için herhangi bir çağrı bildirmek ve notifyall sadece nesne üzerinde kilit aldıktan sonra yapılabilir. Yanlışım varsa düzelt.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.