Java: beri tahu () vs. notifyAll () lagi-lagi

377

Jika satu Googles untuk "perbedaan antara notify()dan notifyAll()" maka banyak penjelasan akan muncul (meninggalkan paragraf javadoc). Semuanya bermuara pada jumlah utas menunggu yang terbangun: satu masuk notify()dan semua masuk notifyAll().

Namun (jika saya mengerti perbedaan antara metode ini dengan benar), hanya satu utas yang selalu dipilih untuk akuisisi monitor selanjutnya; dalam kasus pertama yang dipilih oleh VM, dalam kasus kedua yang dipilih oleh penjadwal thread sistem. Prosedur pemilihan yang tepat untuk keduanya (dalam kasus umum) tidak diketahui oleh programmer.

Apa perbedaan yang bermanfaat antara notify () dan notifyAll () ? Apakah saya melewatkan sesuatu?

Sergey Mikhanov
sumber
6
Perpustakaan yang berguna untuk digunakan untuk konkurensi ada di perpustakaan konkurensi. Saya menyarankan ini adalah pilihan yang lebih baik di hampir setiap kasus. Perpustakaan Concurency pra tanggal Java 5.0 (di mana mereka ditambahkan sebagai standar pada tahun 2004)
Peter Lawrey
4
Saya tidak setuju dengan Peter. Pustaka konkurensi diterapkan di Jawa, dan ada banyak kode Java yang dieksekusi setiap kali Anda memanggil kunci (), membuka kunci (), dll. Anda dapat menembak diri sendiri dengan menggunakan pustaka konkurensi alih-alih yang lama synchronized, kecuali untuk kepastian tertentu. , kasus penggunaan agak jarang.
Alexander Ryzhov
2
Kesalahpahaman utama tampaknya adalah ini: ... hanya satu utas yang selalu dipilih untuk akuisisi monitor lebih lanjut; dalam kasus pertama yang dipilih oleh VM, dalam kasus kedua yang dipilih oleh penjadwal thread sistem. Implikasinya adalah bahwa pada dasarnya sama. Sementara perilaku seperti yang dijelaskan adalah benar, yang hilang adalah bahwa dalam notifyAll()kasus ini, _ utas lainnya setelah yang pertama tetap terjaga dan akan mendapatkan monitor, satu per satu. Dalam notifykasus ini, tidak ada utas lainnya yang terbangun. Jadi secara fungsional mereka sangat berbeda!
BeeOnRope
1) Jika banyak utas menunggu pada suatu objek, dan beri tahu () dipanggil hanya sekali pada objek tersebut. Kecuali salah satu utas menunggu utas yang tersisa menunggu selamanya? 2) Jika notify () digunakan hanya satu dari banyak utas tunggu yang mulai dijalankan. Jika notifyall () digunakan semua utas tunggu diberi tahu tetapi hanya satu dari mereka yang mulai mengeksekusi, jadi apa gunanya notifyall () di sini?
Chetan Gowda
@ChetanGowda Memberitahu semua utas vs Memberitahu persis hanya satu utas yang semena-mena yang benar-benar memiliki perbedaan signifikan hingga perbedaan yang tampaknya halus namun penting itu menerpa kita. Ketika Anda memberi tahu () hanya 1 utas, semua utas lainnya akan berada dalam status menunggu hingga menerima pemberitahuan eksplisit /sinyal. Memberitahu semua, semua utas akan dieksekusi dan diselesaikan dalam beberapa urutan satu demi satu tanpa pemberitahuan lebih lanjut - di sini kita harus mengatakan utas itu blockeddan tidak waiting. Ketika blockedpelakunya ditangguhkan sementara sampai utas lain berada di dalam syncblok.
user104309

Jawaban:

248

Namun (jika saya mengerti perbedaan antara metode ini dengan benar), hanya satu utas yang selalu dipilih untuk akuisisi monitor selanjutnya.

Itu tidak benar. o.notifyAll()membangunkan semua utas yang diblokir dalam o.wait()panggilan. Utas hanya diizinkan untuk kembali dari o.wait()satu per satu, tetapi masing-masing akan mendapat giliran.


Sederhananya, itu tergantung pada mengapa utas Anda menunggu untuk diberitahu. Apakah Anda ingin memberi tahu salah satu utas menunggu bahwa sesuatu terjadi, atau Anda ingin menceritakan semuanya pada saat yang sama?

Dalam beberapa kasus, semua utas tunggu dapat mengambil tindakan bermanfaat setelah menunggu selesai. Contohnya adalah serangkaian utas menunggu tugas tertentu selesai; setelah tugas selesai, semua utas menunggu dapat melanjutkan bisnis mereka. Dalam kasus seperti itu, Anda akan menggunakan notifyAll () untuk membangunkan semua utas secara bersamaan.

Kasus lain, misalnya penguncian yang saling eksklusif, hanya satu dari utas tunggu yang dapat melakukan sesuatu yang berguna setelah diberi tahu (dalam hal ini memperoleh kunci). Dalam kasus seperti itu, Anda lebih suka menggunakan notify () . Diimplementasikan dengan benar, Anda dapat menggunakan notifyAll () dalam situasi ini juga, tetapi Anda tidak perlu membangunkan utas yang tidak dapat melakukan apa pun.


Dalam banyak kasus, kode untuk menunggu suatu kondisi akan ditulis sebagai loop:

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

Dengan begitu, jika sebuah o.notifyAll()panggilan membangunkan lebih dari satu utas menunggu, dan yang pertama kembali dari o.wait()make meninggalkan kondisi dalam keadaan salah, maka utas lainnya yang terbangun akan kembali ke menunggu.

Liedman
sumber
29
jika Anda memberi tahu hanya satu utas tetapi banyak yang menunggu pada suatu objek, bagaimana VM menentukan mana yang akan diberitahukan?
amfibi
6
Saya tidak bisa mengatakan dengan pasti tentang spesifikasi Java, tetapi umumnya Anda harus menghindari membuat asumsi tentang detail seperti itu. Saya pikir Anda dapat mengasumsikan bahwa VM akan melakukannya dengan cara yang waras dan kebanyakan adil.
Liedman
15
Liedman sangat salah, spesifikasi Java secara eksplisit menyatakan bahwa notifikasi () tidak dijamin adil. yaitu setiap panggilan untuk memberi tahu akan membangun kembali utas yang sama (antrian utas pada monitor BUKAN ADIL atau FIFO). Namun penjadwal dijamin adil. Itulah sebabnya dalam sebagian besar kasus di mana Anda memiliki lebih dari 2 utas, Anda sebaiknya memberi tahu All.
Yann TM
45
@YannTM Saya semua untuk kritik konstruktif, tapi saya pikir nada suara Anda agak tidak adil. Saya secara eksplisit mengatakan "tidak bisa mengatakan dengan pasti" dan "Saya pikir". Tenang, apakah Anda pernah menulis sesuatu tujuh tahun lalu yang tidak 100% benar?
Liedman
10
Masalahnya adalah bahwa ini adalah jawaban yang diterima, ini bukan pertanyaan tentang kebanggaan pribadi. Jika Anda tahu Anda salah sekarang, silakan edit jawaban Anda untuk mengatakannya, dan arahkan ke misalnya xagyg pedagogik dan jawab yang benar di bawah ini.
Yann TM
330

Jelas, notifybangun (ada) satu utas di set tunggu, notifyAllbangun semua utas di set tunggu. Diskusi berikut harus menjernihkan keraguan. notifyAllharus digunakan sebagian besar waktu. Jika Anda tidak yakin mana yang akan digunakan, maka gunakan. notifyAllSilakan lihat penjelasan yang berikut.

Baca dengan sangat hati-hati dan mengerti. Silakan kirim saya email jika Anda memiliki pertanyaan.

Lihatlah produsen / konsumen (asumsi adalah kelas ProduserConsumer dengan dua metode). ITU RUSAK (karena menggunakan notify) - ya itu MUNGKIN bekerja - bahkan sebagian besar waktu, tetapi juga dapat menyebabkan kebuntuan - kita akan melihat mengapa:

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;
}

PERTAMA,

Mengapa kita perlu loop sementara di sekitar menunggu?

Kami membutuhkan whileloop jika kami mendapatkan situasi ini:

Konsumen 1 (C1) memasuki blok yang disinkronkan dan buffer kosong, sehingga C1 dimasukkan ke dalam set tunggu (melalui waitpanggilan). Konsumen 2 (C2) akan memasuki metode yang disinkronkan (pada titik Y di atas), tetapi Produser P1 menempatkan objek di buffer, dan kemudian memanggil notify. Satu-satunya utas menunggu adalah C1, sehingga dibangunkan dan sekarang mencoba untuk mendapatkan kembali kunci objek pada titik X (di atas).

Sekarang C1 dan C2 berusaha mendapatkan kunci sinkronisasi. Salah satunya (tanpa syarat) dipilih dan memasuki metode, yang lain diblokir (tidak menunggu - tetapi diblokir, mencoba untuk mendapatkan kunci pada metode). Katakanlah C2 mendapatkan kunci terlebih dahulu. C1 masih memblokir (berusaha mendapatkan kunci di X). C2 menyelesaikan metode dan melepaskan kunci. Sekarang, C1 mendapatkan kunci. Coba tebak, beruntung kita memiliki whileloop, karena, C1 melakukan loop check (guard) dan dicegah untuk menghapus elemen yang tidak ada dari buffer (C2 sudah mendapatkannya!). Jika kita tidak memiliki while, kita akan mendapatkan IndexArrayOutOfBoundsExceptionketika C1 mencoba untuk menghapus elemen pertama dari buffer!

SEKARANG,

Oke, sekarang mengapa kita perlu memberi tahu SEMUA?

Dalam contoh produsen / konsumen di atas, sepertinya kita bisa lolos notify. Kelihatannya seperti ini, karena kita dapat membuktikan bahwa penjaga di loop menunggu untuk produsen dan konsumen saling eksklusif. Artinya, sepertinya kita tidak dapat memiliki utas menunggu dalam putmetode maupun getmetode, karena, agar itu benar, maka yang berikut ini harus benar:

buf.size() == 0 AND buf.size() == MAX_SIZE (anggap MAX_SIZE bukan 0)

NAMUN, ini tidak cukup baik, kami PERLU untuk menggunakannya notifyAll. Mari kita lihat mengapa ...

Asumsikan kita memiliki buffer ukuran 1 (untuk membuat contoh mudah diikuti). Langkah-langkah berikut membawa kita ke jalan buntu. Perhatikan bahwa KAPAN SAJA utas dibangunkan dengan memberi tahu, utas dapat dipilih secara non-deterministik oleh JVM - yaitu utas tunggu mana pun dapat dibangunkan. Perhatikan juga bahwa ketika banyak utas menghalangi saat masuk ke metode (yaitu mencoba memperoleh kunci), urutan akuisisi dapat menjadi non-deterministik. Ingat juga bahwa utas hanya dapat berada dalam salah satu metode pada satu waktu - metode yang disinkronkan hanya memungkinkan satu utas untuk mengeksekusi (yaitu memegang kunci) metode apa saja (disinkronkan) di kelas. Jika urutan peristiwa berikut terjadi - hasil kebuntuan:

LANGKAH 1:
- P1 menempatkan 1 char ke buffer

LANGKAH 2:
- Upaya P2 put- memeriksa lingkaran tunggu - sudah menjadi tanda tunggu

LANGKAH 3:
- Upaya P3 put- memeriksa lingkaran tunggu - sudah menjadi tanda tunggu

LANGKAH 4:
- C1 berupaya mendapatkan 1 karakter
- C2 mencoba mendapatkan 1 karakter - blok saat masuk ke getmetode
- C3 berupaya mendapatkan 1 karakter - blok saat masuk ke getmetode

LANGKAH 5:
- C1 mengeksekusi getmetode - mendapatkan char, panggilan notify, metode keluar
- notifyBangun P2
- TETAPI, C2 memasukkan metode sebelum P2 dapat (P2 harus mendapatkan kembali kunci), jadi blok P2 pada saat masuk ke putmetode
- C2 memeriksa loop menunggu, tidak ada lagi karakter dalam buffer, jadi tunggu
- C3 memasuki metode setelah C2, tetapi sebelum P2, memeriksa loop menunggu, tidak ada lagi karakter dalam buffer, jadi tunggu

LANGKAH 6:
- SEKARANG: ada P3, C2, dan C3 menunggu!
- Akhirnya P2 mendapatkan kunci, menempatkan char di buffer, panggilan notify, keluar dari metode

LANGKAH 7:
- Pemberitahuan P2 membangunkan P3 (ingat utas apa pun dapat dibangunkan)
- P3 memeriksa kondisi loop menunggu, sudah ada arang di buffer, jadi tunggu.
- TIDAK ADA LEBIH BANYAK BENANG UNTUK PEMBERITAHUAN dan TIGA THREAD YANG DITANGGUHKAN!

SOLUSI: Ganti notifydengan notifyAllkode produsen / konsumen (di atas).

xagyg
sumber
1
finnw - P3 harus memeriksa kembali kondisi karena notifypenyebab P3 (utas yang dipilih dalam contoh ini) untuk melanjutkan dari titik itu sedang menunggu (yaitu dalam whileloop). Ada contoh lain yang tidak menyebabkan kebuntuan, namun, dalam hal ini penggunaan notifytidak menjamin kode bebas dari kebuntuan. Penggunaan notifyAlltidak.
xagyg
4
@marcus Sangat dekat. Dengan notifyAll, setiap utas akan mendapatkan kembali kunci (satu per satu), tetapi perhatikan bahwa setelah satu utas mendapatkan kembali kunci dan menjalankan metode (dan kemudian keluar) ... utas berikutnya mendapatkan kembali kunci, periksa "sementara" dan akan kembali ke "wait" (tergantung kondisi apa yang tentunya). Jadi, beri tahu bangun satu utas - saat Anda menyatakan dengan benar. notifyAll membangunkan semua utas dan setiap utas mendapatkan kembali kunci satu per satu - memeriksa kondisi "sementara" dan menjalankan metode atau "menunggu" lagi.
xagyg
1
@xagyg, apakah Anda berbicara tentang skenario di mana setiap produsen hanya memiliki satu char untuk disimpan? Jika demikian, contoh Anda benar, tetapi IMO tidak terlalu menarik. Dengan langkah-langkah tambahan yang saya sarankan, Anda bisa menemui jalan buntu pada sistem yang sama, tetapi dengan jumlah input yang tidak terbatas - itulah yang biasanya digunakan dalam pola seperti itu.
eran
3
@codeObserver Anda bertanya: "Apakah memanggil notifyAll () mengarah ke beberapa utas menunggu yang memeriksa kondisi while () pada saat yang sama .. dan karenanya ada kemungkinan bahwa sebelum sementara saitsfied disimpan, 2 utas sudah keluar sehingga menyebabkan outOfBound pengecualian? " Tidak, ini tidak mungkin, karena meskipun beberapa utas akan bangun, mereka tidak dapat memeriksa kondisi sementara pada saat yang sama. Mereka masing-masing diminta untuk mendapatkan kembali kunci (segera setelah menunggu) sebelum mereka dapat memasukkan kembali bagian kode dan memeriksa kembali saat. Karena itu, satu per satu.
xagyg
4
@ xagyg contoh yang bagus. Ini di luar topik dari pertanyaan awal; hanya untuk diskusi. Kebuntuan adalah masalah desain imo (koreksi saya jika saya salah). Karena Anda memiliki satu kunci yang dibagikan oleh put dan get. Dan JVM tidak cukup pintar untuk memanggil put setelah mendapatkan melepaskan kunci dan sebaliknya. Kunci mati terjadi karena put membangunkan put lain, yang menempatkan dirinya kembali untuk menunggu () karena sementara (). Apakah membuat dua kelas (dan dua kunci) berfungsi? Jadi cantumkan {synchonized (get)}, get {(synchonized (put)}. Dengan kata lain, get will wake wake only, dan will wake wake only
Jay
43

Perbedaan yang berguna:

  • Gunakan beri tahu () jika semua utas menunggu Anda dapat dipertukarkan (urutan mereka bangun tidak masalah), atau jika Anda hanya memiliki satu utas tunggu. Contoh umum adalah kumpulan utas yang digunakan untuk menjalankan pekerjaan dari antrian - ketika suatu pekerjaan ditambahkan, salah satu utas diberitahu untuk bangun, melaksanakan pekerjaan berikutnya dan kembali tidur.

  • Gunakan notifyAll () untuk kasus lain di mana utas tunggu mungkin memiliki tujuan yang berbeda dan harus dapat berjalan secara bersamaan. Contohnya adalah operasi pemeliharaan pada sumber daya bersama, di mana banyak utas menunggu untuk menyelesaikan operasi sebelum mengakses sumber daya.

David Crow
sumber
19

Saya pikir itu tergantung pada bagaimana sumber daya diproduksi dan dikonsumsi. Jika 5 objek kerja tersedia sekaligus dan Anda memiliki 5 objek konsumen, masuk akal untuk membangunkan semua utas menggunakan notifyAll () sehingga masing-masing dapat memproses 1 objek kerja.

Jika Anda hanya memiliki satu objek kerja yang tersedia, apa gunanya membangunkan semua objek konsumen untuk berlomba untuk satu objek itu? Yang pertama memeriksa untuk pekerjaan yang tersedia akan mendapatkannya dan semua utas lainnya akan memeriksa dan menemukan mereka tidak ada hubungannya.

Saya menemukan penjelasan yang bagus di sini . Pendeknya:

Metode notify () umumnya digunakan untuk kumpulan sumber daya , di mana ada sejumlah "konsumen" atau "pekerja" sewenang-wenang yang mengambil sumber daya, tetapi ketika sumber daya ditambahkan ke kumpulan, hanya satu dari konsumen atau pekerja yang menunggu yang dapat menangani dengan itu. Metode notifyAll () sebenarnya digunakan dalam kebanyakan kasus lain. Secara ketat, diperlukan untuk memberi tahu pelayan tentang suatu kondisi yang dapat memungkinkan beberapa pelayan untuk melanjutkan. Tapi ini sering sulit diketahui. Jadi sebagai aturan umum, jika Anda tidak memiliki logika khusus untuk menggunakan notify (), maka Anda mungkin harus menggunakan notifyAll () , karena seringkali sulit untuk mengetahui dengan tepat utas apa yang akan menunggu pada objek tertentu dan mengapa.

andyuk
sumber
11

Perhatikan bahwa dengan utilitas concurrency Anda juga memiliki pilihan antara signal()dan signalAll()karena metode ini disebut di sana. Jadi pertanyaannya tetap valid bahkan dengan java.util.concurrent.

Doug Lea memunculkan poin menarik dalam bukunya yang terkenal : jika a notify()dan Thread.interrupt()terjadi pada saat yang sama, pemberitahuan itu mungkin akan hilang. Jika ini dapat terjadi dan memiliki implikasi dramatis notifyAll()adalah pilihan yang lebih aman meskipun Anda harus membayar biaya overhead (membangunkan terlalu banyak utas sepanjang waktu).

Abu-abu
sumber
10

Ringkasan singkat:

Selalu lebih suka beri tahuAll () daripada notify () kecuali Anda memiliki aplikasi paralel masif di mana sejumlah besar utas melakukan hal yang sama.

Penjelasan:

beri tahu () [...] bangun satu utas. Karena notify () tidak memungkinkan Anda menentukan utas yang dibangunkan, ini berguna hanya dalam aplikasi paralel masif - yaitu, program dengan sejumlah besar utas, semua melakukan tugas yang sama. Dalam aplikasi seperti itu, Anda tidak peduli utas mana yang terbangun.

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

Bandingkan notify () dengan notifyAll () dalam situasi yang dijelaskan di atas: aplikasi paralel besar-besaran di mana utas melakukan hal yang sama. Jika Anda memanggil notifyAll () dalam kasus itu, notifyAll () akan menginduksi bangun (yaitu penjadwalan) sejumlah besar utas, banyak dari mereka tidak perlu (karena hanya satu utas yang benar-benar dapat melanjutkan, yaitu utas yang akan diberikan kepada monitor untuk objek wait () , notify () , atau notifyAll () dipanggil), karena itu menyia-nyiakan sumber daya komputasi.

Jadi, jika Anda tidak memiliki aplikasi tempat sejumlah besar utas melakukan hal yang sama secara bersamaan, lebih baik beri tahuAll () daripada notify () . Mengapa? Karena, seperti yang sudah dijawab oleh pengguna lain di forum ini, beri tahu ()

bangun satu utas yang menunggu di monitor objek ini. [...] Pilihannya arbitrer dan terjadi atas kebijakan implementasi.

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

Bayangkan Anda memiliki aplikasi konsumen produsen di mana konsumen siap (yaitu menunggu () ing) untuk mengkonsumsi, produsen siap (yaitu menunggu () ing) untuk memproduksi dan antrian barang (untuk diproduksi / dikonsumsi) kosong. Dalam hal ini, beri tahu () mungkin hanya membangunkan konsumen dan tidak pernah memproduksi karena pilihan yang bangun adalah sewenang - wenang . Siklus konsumen produsen tidak akan membuat kemajuan meskipun produsen dan konsumen masing-masing siap untuk memproduksi dan mengkonsumsi. Sebagai gantinya, seorang konsumen dibangunkan (yaitu meninggalkan status menunggu () ), tidak mengambil item dari antrian karena kosong, dan memberi tahu () konsumen lain untuk melanjutkan.

Sebaliknya, notifyAll () membangkitkan produsen dan konsumen. Pilihan siapa yang dijadwalkan tergantung pada penjadwal. Tentu saja, tergantung pada implementasi penjadwal, penjadwal mungkin juga hanya menjadwalkan konsumen (misalnya jika Anda memberikan prioritas yang sangat tinggi kepada konsumen). Namun, asumsi di sini adalah bahwa bahaya penjadwalan penjadwalan hanya konsumen lebih rendah daripada bahaya JVM hanya membangunkan konsumen karena penjadwal yang diimplementasikan secara wajar tidak hanya membuat keputusan sewenang - wenang . Sebaliknya, sebagian besar implementasi penjadwal membuat setidaknya beberapa upaya untuk mencegah kelaparan.

ansd
sumber
9

Berikut ini sebuah contoh. Menjalankannya. Kemudian ubah salah satu dari notifyAll () menjadi notify () dan lihat apa yang terjadi.

Kelas ProducerConsumerExample

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();
    }
}

Kelas dropbox

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();
    }
}

Kelas konsumen

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) { }
        }
    }
}

Kelas produsen

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) { }
        }
    }
}
Erik
sumber
8

Dari Joshua Bloch, Guru Jawa sendiri dalam Java Edisi ke-2 yang efektif:

"Butir 69: Memilih utilitas konkurensi untuk menunggu dan memberi tahu".

Steve McLeod
sumber
16
The mengapa lebih penting daripada sumber.
Pacerier
2
@Pacerier Well berkata. Saya akan lebih tertarik untuk mencari tahu alasannya juga. Salah satu alasan yang mungkin bisa menjadi bahwa menunggu dan memberi tahu di kelas objek didasarkan pada variabel kondisi implisit. Jadi dalam contoh standar produsen dan konsumen ..... baik produsen dan konsumen akan menunggu pada kondisi yang sama yang dapat menyebabkan kebuntuan seperti yang dijelaskan oleh xagyg dalam jawabannya. Jadi pendekatan yang lebih baik adalah dengan menggunakan 2 variabel kondisi seperti yang dijelaskan dalam docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
rahul
6

Saya harap ini akan menghapus beberapa keraguan.

notify () : Metode notify () bangun satu utas menunggu kunci (utas pertama yang disebut wait () pada kunci itu).

notifyAll () : Metode notifyAll () membangunkan semua utas menunggu kunci; JVM memilih salah satu utas dari daftar utas menunggu kunci dan membangunkan utas tersebut.

Dalam kasus utas tunggal menunggu kunci, tidak ada perbedaan yang signifikan antara notify () dan notifyAll (). Namun, ketika ada lebih dari satu utas yang menunggu kunci, baik di notify () dan notifyAll (), utas yang tepat dibangunkan di bawah kendali JVM dan Anda tidak dapat secara terprogram mengontrol bangunkan utas tertentu.

Pada pandangan pertama, tampaknya itu adalah ide yang baik untuk memanggil notify () untuk membangunkan satu utas; sepertinya tidak perlu membangunkan semua utas. Namun, masalah dengan memberi tahu () adalah bahwa utas yang dibangunkan mungkin bukan yang cocok untuk dibangunkan (utas mungkin menunggu beberapa kondisi lain, atau kondisinya masih belum terpenuhi untuk utas tersebut dll). Dalam hal ini , notifikasi () mungkin hilang dan tidak ada utas lain yang akan bangun yang berpotensi mengarah ke jenis kebuntuan (notifikasi hilang dan semua utas lainnya menunggu notifikasi — selamanya).

Untuk menghindari masalah ini , selalu lebih baik untuk memanggil notifyAll () ketika ada lebih dari satu utas menunggu kunci (atau lebih dari satu kondisi di mana menunggu dilakukan). Metode notifyAll () membangun semua utas, sehingga tidak sangat efisien. Namun, kehilangan kinerja ini dapat diabaikan dalam aplikasi dunia nyata.

pardeep131085
sumber
6

Ada tiga negara bagian untuk utas.

  1. TUNGGU - Utas tidak menggunakan siklus CPU apa pun
  2. BLOKED - Utas diblokir saat mencoba mendapatkan monitor. Mungkin masih menggunakan siklus CPU
  3. RUNNING - Utas sedang berjalan.

Sekarang, ketika notify () dipanggil, JVM mengambil satu utas dan memindahkannya ke status BLOCKED dan karenanya ke status RUNNING karena tidak ada persaingan untuk objek monitor.

Ketika notifyAll () dipanggil, JVM mengambil semua utas dan memindahkan semuanya ke Status BLOCKED. Semua utas ini akan mendapatkan kunci objek berdasarkan prioritas. Benang yang dapat memperoleh monitor terlebih dahulu akan dapat pergi ke status MENJALANKAN terlebih dahulu dan seterusnya.

S_R
sumber
Penjelasan yang luar biasa.
royatirek
5

Saya sangat terkejut bahwa tidak ada yang menyebutkan masalah "bangun yang hilang" (google it).

Pada dasarnya:

  1. jika Anda memiliki beberapa utas menunggu pada kondisi yang sama dan,
  2. beberapa utas yang dapat membuat Anda beralih dari negara A ke negara B dan,
  3. banyak utas yang dapat membuat Anda beralih dari keadaan B ke keadaan A (biasanya utas yang sama seperti pada 1.) dan,
  4. transisi dari keadaan A ke B harus memberi tahu utas di 1.

KEMUDIAN Anda harus menggunakan notifyAll kecuali Anda memiliki jaminan yang terbukti bahwa kehilangan bangun tidak mungkin.

Contoh umum adalah antrian FIFO bersamaan di mana: beberapa enqueuers (1. dan 3. di atas) dapat mentransisikan antrian Anda dari kosong ke beberapa dequeuers kosong (2. di atas) dapat menunggu kondisi "antrian tidak kosong" kosong -> non-kosong harus memberi tahu dequeuers

Anda dapat dengan mudah menulis interleaving operasi di mana, mulai dari antrian kosong, 2 enqueuers dan 2 dequeuers berinteraksi dan 1 enqueuer akan tetap tidur.

Ini adalah masalah yang bisa dibandingkan dengan masalah kebuntuan.

NickV
sumber
Maafkan saya, xagyg menjelaskannya secara rinci. Nama untuk masalah ini adalah "wakeup hilang"
NickV
@Abhay Bansal: Saya pikir Anda kehilangan fakta bahwa condition.wait () melepaskan kunci dan akan diperoleh kembali oleh utas yang bangun.
NickV
4

notify()akan bangun satu utas sementara notifyAll()akan bangun semua. Sejauh yang saya tahu tidak ada jalan tengah. Tetapi jika Anda tidak yakin apa yang notify()akan dilakukan pada utas Anda, gunakan notifyAll(). Bekerja seperti pesona setiap saat.

Spoike
sumber
4

Semua jawaban di atas benar, sejauh yang saya tahu, jadi saya akan memberi tahu Anda sesuatu yang lain. Untuk kode produksi, Anda harus menggunakan kelas-kelas di java.util.concurrent. Ada sangat sedikit yang tidak bisa mereka lakukan untuk Anda, di bidang konkurensi di java.


sumber
4

notify()memungkinkan Anda menulis kode yang lebih efisien daripada notifyAll().

Pertimbangkan potongan kode berikut yang dijalankan dari beberapa utas paralel:

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

Itu dapat dibuat lebih efisien dengan menggunakan notify():

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

Dalam kasus ini jika Anda memiliki banyak utas, atau jika kondisi loop menunggu mahal untuk dievaluasi, notify()akan jauh lebih cepat daripada notifyAll(). Misalnya, jika Anda memiliki 1000 utas, maka 999 utas akan dibangunkan dan dievaluasi setelah yang pertama notifyAll(), kemudian 998, lalu 997, dan seterusnya. Sebaliknya, dengan notify()solusinya, hanya satu utas yang akan dibangunkan.

Gunakan notifyAll()saat Anda perlu memilih utas mana yang akan melakukan pekerjaan selanjutnya:

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

Akhirnya, penting untuk memahami bahwa dalam kasus notifyAll(), kode di dalam synchronizedblok yang telah dibangunkan akan dieksekusi secara berurutan, tidak semuanya sekaligus. Katakanlah ada tiga utas menunggu dalam contoh di atas, dan utas keempat memanggil notifyAll(). Ketiga utas akan dibangunkan tetapi hanya satu yang akan memulai eksekusi dan memeriksa kondisi whileloop. Jika kondisinya true, ia akan memanggil wait()lagi, dan hanya kemudian utas kedua akan mulai mengeksekusi dan akan memeriksa whilekondisi loop -nya , dan seterusnya.

Alexander Ryzhov
sumber
4

Berikut ini penjelasan yang lebih sederhana:

Anda benar bahwa apakah Anda menggunakan notify () atau notifyAll (), hasil langsungnya adalah bahwa satu utas lainnya akan mendapatkan monitor dan mulai mengeksekusi. (Dengan asumsi beberapa utas sebenarnya diblokir saat menunggu () untuk objek ini, utas lain yang tidak terkait tidak menyerap semua inti yang tersedia, dll.) Dampaknya muncul kemudian.

Misalkan utas A, B, dan C sedang menunggu objek ini, dan utas A mendapat monitor. Perbedaannya terletak pada apa yang terjadi setelah A melepaskan monitor. Jika Anda menggunakan notify (), maka B dan C masih diblokir dalam wait (): mereka tidak menunggu di monitor, mereka sedang menunggu untuk diberitahu. Ketika A melepaskan monitor, B dan C masih akan duduk di sana, menunggu pemberitahuan ().

Jika Anda menggunakan notifyAll (), maka B dan C telah maju melewati status "tunggu pemberitahuan" dan keduanya menunggu untuk mendapatkan monitor. Ketika A melepaskan monitor, B atau C akan mendapatkannya (dengan asumsi tidak ada utas lain yang bersaing untuk monitor itu) dan mulai mengeksekusi.

Steve
sumber
Penjelasannya sangat jelas. Hasil dari perilaku notify () ini dapat menyebabkan "Sinyal Terlewatkan" / "Pemberitahuan Terlewatkan" yang mengarah ke kebuntuan / tidak ada situasi kemajuan dari status aplikasi. P-Produser, C-Konsumen P1, P2 dan C2 sedang menunggu C1. Panggilan C1 memberitahukan () dan dimaksudkan untuk produsen tetapi C2 dapat dibangunkan sehingga P1 dan P2 melewatkan pemberitahuan dan akan menunggu "pemberitahuan" eksplisit lebih lanjut (panggilan notifikasi () panggilan).
user104309
4

Jawaban ini adalah penulisan ulang grafis dan penyederhanaan jawaban yang sangat baik oleh xagyg , termasuk komentar oleh eran .

Mengapa menggunakan notifyAll, bahkan ketika setiap produk ditujukan untuk satu konsumen?

Pertimbangkan produsen dan konsumen, disederhanakan sebagai berikut.

Produsen:

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

Konsumen:

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

Asumsikan 2 produsen dan 2 konsumen, berbagi penyangga ukuran 1. Gambar berikut menggambarkan skenario yang mengarah ke jalan buntu , yang akan dihindari jika semua utas menggunakan notifyAll .

Setiap pemberitahuan ditandai dengan utas yang dibangunkan.

kebuntuan karena memberi tahu

Marco
sumber
3

Saya ingin menyebutkan apa yang dijelaskan dalam Java Concurrency in Practice:

Poin pertama, apakah Memberitahu atau Memberitahu Semua?

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

Jika dua utas A dan B sedang menunggu predikat kondisi yang berbeda dari antrian kondisi yang sama dan beri tahu disebut, maka terserah JVM di mana utas JVM akan memberi tahu.

Sekarang jika pemberitahuan dimaksudkan untuk utas A dan JVM yang diberi tahu utas B, maka utas B akan bangun dan melihat bahwa pemberitahuan ini tidak berguna sehingga akan menunggu lagi. Dan Thread A tidak akan pernah tahu tentang sinyal yang terlewat ini dan seseorang membajak notifikasi itu.

Jadi, memanggil notifyAll akan menyelesaikan masalah ini, tetapi sekali lagi ini akan berdampak kinerja karena akan memberi tahu semua utas dan semua utas akan bersaing untuk kunci yang sama dan itu akan melibatkan pengalih konteks dan karenanya memuat pada CPU. Tetapi kita harus memperhatikan kinerja hanya jika berperilaku dengan benar, jika perilakunya sendiri tidak benar maka kinerja tidak ada gunanya.

Masalah ini dapat diselesaikan dengan menggunakan objek Kondisi kunci penguncian eksplisit, disediakan dalam jdk 5, karena menyediakan menunggu berbeda untuk setiap kondisi predikat. Di sini ia akan berperilaku dengan benar dan tidak akan ada masalah kinerja karena akan memanggil sinyal dan memastikan bahwa hanya satu utas menunggu kondisi itu

AKS
sumber
3

notify()- Memilih utas acak dari set tunggu objek dan menempatkannya dalam BLOCKEDstatus. Sisa utas dalam set tunggu objek masih dalam WAITINGstatus.

notifyAll()- Memindahkan semua utas dari set tunggu objek ke BLOCKEDstatus. Setelah Anda gunakan notifyAll(), tidak ada utas yang tersisa dalam set menunggu objek yang dibagikan karena semuanya sekarang dalam BLOCKEDkeadaan dan tidak dalam WAITINGkeadaan.

BLOCKED- Diblokir untuk akuisisi kunci. WAITING- menunggu pemberitahuan (atau diblokir untuk penyelesaian gabungan).

fireboy91
sumber
3

Diambil dari blog di Java Efektif:

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

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

Jadi, yang saya pahami adalah (dari blog yang disebutkan di atas, komentar oleh "Yann TM" pada jawaban yang diterima dan dokumen Java ):

  • notify (): JVM membangkitkan salah satu utas menunggu pada objek ini. Pemilihan utas dibuat secara sewenang-wenang tanpa keadilan. Jadi utas yang sama dapat dibangunkan berulang kali. Jadi keadaan sistem berubah tetapi tidak ada kemajuan nyata yang dibuat. Sehingga menciptakan livelock .
  • notifyAll (): JVM membangkitkan semua utas dan lalu semua utas berpacu untuk mengunci pada objek ini. Sekarang, penjadwal CPU memilih utas yang mendapatkan kunci pada objek ini. Proses seleksi ini akan jauh lebih baik daripada seleksi oleh JVM. Dengan demikian, memastikan lives.
rajya vardhan
sumber
2

Lihatlah kode yang diposting oleh @xagyg.

Misalkan dua benang yang berbeda menunggu dua kondisi yang berbeda:
The Thread pertama sedang menunggu buf.size() != MAX_SIZE, dan thread kedua sedang menunggubuf.size() != 0 .

Misalkan pada titik tertentu buf.size() tidak sama dengan 0 . JVM memanggil notify()alih-alih notifyAll(), dan utas pertama diberi tahu (bukan yang kedua).

Utas pertama dibangunkan, memeriksa buf.size()yang mungkin kembali MAX_SIZE, dan kembali menunggu. Utas kedua tidak dibangunkan, terus menunggu dan tidak menelepon get().

alxlevin
sumber
1

notify()bangun utas pertama yang memanggil wait()objek yang sama.

notifyAll()membangunkan semua utas yang memanggil wait()objek yang sama.

Utas prioritas tertinggi akan berjalan terlebih dahulu.

Kshitij Banerjee
sumber
13
Dalam hal notify()itu bukan " utas pertama ".
Bhesh Gurung
6
Anda tidak dapat memprediksi mana yang akan dipilih oleh VM. Hanya Tuhan yang tahu.
Sergii Shevchyk
Tidak ada jaminan siapa yang akan menjadi yang pertama (tidak ada keadilan)
Ivan Voroshilin
Itu hanya akan membangun utas pertama jika OS menjamin itu, dan kemungkinan tidak. Ini benar-benar mengacu pada OS (dan penjadwalnya) untuk menentukan utas yang membangunkan.
Paul Stelian
1

beri tahu akan memberi tahu hanya satu utas yang dalam kondisi menunggu, sedangkan beri tahu semua akan memberi tahu semua utas dalam status tunggu sekarang semua utas yang diberitahukan dan semua utas yang diblokir memenuhi syarat untuk kunci, di mana hanya satu yang akan mendapatkan kunci dan semua yang lain (termasuk mereka yang dalam keadaan menunggu lebih awal) akan dalam keadaan diblokir.

Saurabh
sumber
1

Untuk meringkas penjelasan terperinci yang sangat baik di atas, dan dengan cara paling sederhana yang dapat saya pikirkan, ini disebabkan oleh keterbatasan monitor bawaan JVM, yang 1) diperoleh pada seluruh unit sinkronisasi (blok atau objek) dan 2) tidak membeda-bedakan kondisi tertentu yang sedang menunggu / diberitahu pada / tentang.

Ini berarti bahwa jika beberapa utas menunggu pada kondisi yang berbeda dan memberitahukan () digunakan, utas yang dipilih mungkin bukan yang akan membuat kemajuan pada kondisi yang baru terpenuhi - menyebabkan utas tersebut (dan utas lainnya yang masih menunggu yang akan dapat untuk memenuhi kondisi, dll.) tidak dapat membuat kemajuan, dan akhirnya kelaparan atau hangup program.

Sebaliknya, notifyAll () memungkinkan semua utas tunggu akhirnya mendapatkan kembali kunci dan memeriksa kondisinya masing-masing, sehingga akhirnya memungkinkan kemajuan dibuat.

Jadi, beri tahu () dapat digunakan dengan aman hanya jika utas menunggu dijamin untuk memungkinkan kemajuan harus dibuat jika dipilih, yang secara umum puas ketika semua utas dalam monitor yang sama memeriksa hanya satu dan kondisi yang sama - yang cukup langka kasus dalam aplikasi dunia nyata.

KingCode
sumber
0

Ketika Anda memanggil wait () dari "objek" (mengharapkan kunci objek diperoleh), magang ini akan melepaskan kunci pada objek itu dan membantu adalah utas lain untuk mengunci pada "objek" ini, dalam skenario ini akan ada lebih dari 1 utas menunggu "sumber daya / objek" (mengingat utas lainnya juga mengeluarkan tunggu pada objek yang sama di atas dan di ujung jalan akan ada utas yang mengisi sumber daya / objek dan memanggil notify / notifyAll).

Di sini, ketika Anda mengeluarkan pemberitahuan tentang objek yang sama (dari sisi yang sama / sisi lain dari proses / kode), ini akan merilis utas tunggal yang diblokir dan menunggu (tidak semua utas tunggu - utas yang dirilis ini akan dipilih oleh JVM Thread Penjadwal dan semua proses mendapatkan kunci pada objek sama seperti biasa).

Jika Anda hanya memiliki satu utas yang akan membagikan / mengerjakan objek ini, tidak masalah untuk menggunakan metode notify () sendirian dalam implementasi wait-notify Anda.

jika Anda berada dalam situasi di mana lebih dari satu utas membaca dan menulis pada sumber daya / objek berdasarkan logika bisnis Anda, maka Anda harus mencari notifyAll ()

sekarang saya mencari bagaimana tepatnya jvm mengidentifikasi dan memutus utas menunggu ketika kami mengeluarkan notify () pada sebuah objek ...

Deepak sai
sumber
0

Meskipun ada beberapa jawaban kuat di atas, saya terkejut dengan jumlah kebingungan dan kesalahpahaman yang telah saya baca. Ini mungkin membuktikan gagasan bahwa seseorang harus menggunakan java.util.concurrent sebanyak mungkin alih-alih mencoba menulis kode konkuren yang rusak sendiri. Kembali ke pertanyaan: untuk meringkas, praktik terbaik hari ini adalah HINDARI beri tahu () dalam SEMUA situasi karena masalah bangun yang hilang. Siapa pun yang tidak memahami ini tidak boleh diizinkan untuk menulis kode konkurensi misi kritis. Jika Anda khawatir tentang masalah penggembalaan, satu cara aman untuk mencapai bangun satu utas pada satu waktu adalah dengan: 1. Membuat antrean tunggu yang eksplisit untuk utas tunggu; 2. Mintalah setiap utas dalam antrian menunggu pendahulunya; 3. Minta setiap utas panggilan notifyAll () saat selesai. Atau Anda dapat menggunakan Java.util.concurrent. *,

jzl106
sumber
dalam pengalaman saya, penggunaan wait / notify sering digunakan dalam mekanisme antrian di mana utas ( Runnableimplementasi) memproses konten antrian. Ini wait()kemudian digunakan setiap kali antrian kosong. Dan notify()disebut ketika informasi ditambahkan. -> dalam kasus seperti itu hanya ada 1 utas yang pernah memanggil wait(), maka bukankah itu terlihat agak konyol untuk digunakan notifyAll()jika Anda tahu hanya ada 1 utas menunggu.
bvdb
-2

Bangun semua tidak banyak berarti di sini. tunggu beri tahu dan beri tahu semua, semua ini dimasukkan setelah memiliki monitor objek. Jika sebuah utas sedang dalam tahap menunggu dan notifikasi dipanggil, utas ini akan mengambil kunci dan tidak ada utas lain pada saat itu yang bisa mengambil kunci itu. Jadi akses bersamaan tidak bisa terjadi sama sekali. Sejauh yang saya tahu panggilan apa pun untuk menunggu memberi tahu dan memberitahukan semua dapat dilakukan hanya setelah mengambil kunci pada objek. Perbaiki saya jika saya salah.

Abhay Bansal
sumber