Mengapa harus menunggu () selalu dalam blok disinkronkan

257

Kita semua tahu bahwa untuk memanggil Object.wait(), panggilan ini harus ditempatkan di blok yang disinkronkan, jika tidak IllegalMonitorStateExceptiondilemparkan. Tapi apa alasan untuk membuat batasan ini? Saya tahu itu wait()melepaskan monitor, tetapi mengapa kita perlu secara eksplisit memperoleh monitor dengan membuat blok tertentu disinkronkan dan kemudian melepaskan monitor dengan menelepon wait()?

Apa potensi kerusakan jika dimungkinkan untuk menjalankan di wait()luar blok yang disinkronkan, mempertahankan semantiknya - menangguhkan utas penelepon?

diy
sumber

Jawaban:

232

A wait()hanya masuk akal ketika ada juga notify(), jadi selalu tentang komunikasi antara utas, dan yang membutuhkan sinkronisasi agar berfungsi dengan benar. Orang dapat berargumen bahwa ini harusnya implisit, tetapi itu tidak akan benar-benar membantu, karena alasan berikut:

Secara semantik, Anda tidak pernah adil wait(). Anda perlu beberapa kondisi untuk dipuaskan, dan jika tidak, Anda menunggu sampai kondisi itu dipenuhi. Jadi apa yang sebenarnya Anda lakukan adalah

if(!condition){
    wait();
}

Tetapi kondisinya sedang diatur oleh utas terpisah, jadi untuk menjalankannya dengan benar, Anda perlu sinkronisasi.

Beberapa hal lagi yang salah dengan itu, di mana hanya karena utas Anda berhenti menunggu bukan berarti kondisi yang Anda cari benar:

  • Anda bisa mendapatkan bangun palsu (artinya utas dapat bangun dari menunggu tanpa pernah menerima pemberitahuan), atau

  • Kondisi dapat diatur, tetapi utas ketiga membuat kondisi salah lagi pada saat utas menunggu bangun (dan meminta kembali monitor).

Untuk menangani kasus-kasus ini apa yang benar-benar Anda butuhkan adalah selalu beberapa variasi dari ini:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

Lebih baik lagi, jangan main-main dengan sinkronisasi primitif sama sekali dan bekerja dengan abstraksi yang ditawarkan dalam java.util.concurrentpaket.

Michael Borgwardt
sumber
3
Ada diskusi terperinci di sini juga, mengatakan pada dasarnya hal yang sama. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…
1
btw, jika Anda tidak mengabaikan bendera yang terputus, loop juga harus memeriksa Thread.interrupted().
bestsss
2
Saya masih dapat melakukan sesuatu seperti: while (! Kondisi) {disinkronkan (ini) {tunggu ();}} yang berarti masih ada perlombaan antara memeriksa kondisi dan menunggu bahkan jika menunggu () dipanggil dengan benar dalam blok yang disinkronkan. Jadi apakah ada alasan lain di balik pembatasan ini, mungkin karena cara itu diterapkan di Jawa?
shrini1000
9
Skenario buruk lainnya: kondisinya salah, kami akan menunggu () dan kemudian utas lain mengubah kondisi dan memanggil notifikasi (). Karena kami belum menunggu (), kami akan kehilangan pemberitahuan ini (). Dengan kata lain, uji dan tunggu, serta perubahan dan beri tahu harus berupa atom .
1
@ Nullpointer: Jika itu adalah tipe yang dapat ditulis secara atom (seperti boolean yang tersirat dengan menggunakannya secara langsung dalam klausa if) dan tidak ada saling ketergantungan dengan data bersama lainnya, Anda bisa lolos dengan menyatakannya volatile. Tetapi Anda perlu itu atau sinkronisasi untuk memastikan bahwa pembaruan akan segera terlihat oleh utas lainnya.
Michael Borgwardt
283

Apa potensi kerusakan jika dimungkinkan untuk menjalankan di wait()luar blok yang disinkronkan, mempertahankan semantiknya - menangguhkan utas penelepon?

Mari kita ilustrasikan masalah apa yang akan kita hadapi jika wait()bisa disebut di luar blok yang disinkronkan dengan contoh nyata .

Misalkan kita harus mengimplementasikan antrian pemblokiran (saya tahu, sudah ada satu di API :)

Upaya pertama (tanpa sinkronisasi) dapat melihat sesuatu di sepanjang garis di bawah ini

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

Inilah yang berpotensi terjadi:

  1. Seorang konsumen menelepon take()dan melihat bahwa buffer.isEmpty().

  2. Sebelum utas konsumen meneruskan ke panggilan wait(), utas produsen datang dan memanggil penuh give(), yaitu,buffer.add(data); notify();

  3. Benang konsumen sekarang akan memanggil wait()(dan kehilangan yang notify()yang hanya disebut).

  4. Jika sial, utas produsen tidak akan menghasilkan lebih banyak give()karena fakta bahwa utas konsumen tidak pernah bangun, dan kami memiliki dead-lock.

Setelah Anda memahami masalahnya, solusinya sudah jelas: Gunakan synchronizeduntuk memastikan notifytidak pernah dipanggil antara isEmptydanwait .

Tanpa merinci: Masalah sinkronisasi ini bersifat universal. Seperti yang ditunjukkan oleh Michael Borgwardt, tunggu / beri tahu tentang komunikasi antar utas, sehingga Anda akan selalu berakhir dengan kondisi balapan yang serupa dengan yang dijelaskan di atas. Inilah sebabnya mengapa aturan "hanya tunggu di dalam sinkronisasi" diberlakukan.


Sebuah paragraf dari tautan yang diposting oleh @ Willie merangkumnya dengan cukup baik:

Anda memerlukan jaminan mutlak bahwa pelayan dan pemberi tahu setuju tentang keadaan predikat. Pelayan memeriksa keadaan predikat pada titik tertentu sedikit SEBELUM itu tertidur, tetapi tergantung pada kebenaran pada predikat yang benar KETIKA tidur. Ada periode kerentanan antara kedua peristiwa itu, yang dapat merusak program.

Predikat yang harus disetujui oleh produsen dan konsumen ada dalam contoh di atas buffer.isEmpty(). Dan perjanjian diselesaikan dengan memastikan bahwa menunggu dan memberitahukan dilakukan dalam synchronizedblok.


Posting ini telah ditulis ulang sebagai artikel di sini: Java: Mengapa menunggu harus dipanggil dalam blok yang disinkronkan

aioobe
sumber
Selain itu juga untuk memastikan perubahan yang dilakukan pada kondisi terlihat segera setelah menunggu () selesai, saya kira. Jika tidak, juga kunci mati karena notify () telah dipanggil.
Surya Wijaya Madjid
Menarik, tetapi perhatikan bahwa hanya memanggil yang disinkronkan sebenarnya tidak akan selalu menyelesaikan masalah seperti itu karena sifat "tidak dapat diandalkan" dari wait () dan notify (). Baca lebih lanjut di sini: stackoverflow.com/questions/21439355/… . Alasan mengapa sinkronisasi diperlukan terletak dalam arsitektur perangkat keras (lihat jawaban saya di bawah).
Marcus
tetapi jika menambahkan return buffer.remove();blok sementara tetapi setelah wait();, itu berhasil?
BobJiang
@ BobJiang, tidak, utas dapat dibangunkan untuk alasan lain selain seseorang yang menelepon memberi. Dengan kata lain, buffer mungkin kosong bahkan setelah waitkembali.
aioobe
Aku hanya Thread.currentThread().wait();di mainfungsi dikelilingi oleh try-catch untuk InterruptedException. Tanpa synchronizedblok, itu memberi saya pengecualian yang sama IllegalMonitorStateException. Apa yang membuatnya mencapai keadaan ilegal sekarang? Ini bekerja di dalam synchronizedblok.
Shashwat
12

@ Ballollall benar. The wait()disebut, sehingga benang dapat menunggu untuk beberapa kondisi terjadi ketika hal ini wait()panggilan terjadi, benang dipaksa menyerah kuncinya.
Untuk melepaskan sesuatu, Anda harus memilikinya terlebih dahulu. Utas perlu memiliki kunci terlebih dahulu. Oleh karena itu kebutuhan untuk memanggilnya di dalam synchronizedmetode / blok.

Ya, saya setuju dengan semua jawaban di atas mengenai potensi kerusakan / inkonsistensi jika Anda tidak memeriksa kondisi dalam synchronizedmetode / blok. Namun seperti yang ditunjukkan @ shrini1000, hanya dengan memanggil wait()blok yang disinkronkan tidak akan mencegah terjadinya ketidakkonsistenan ini.

Ini bacaan yang bagus ..

ashoka.devanampriya
sumber
5
@Popeye Jelaskan 'dengan benar' dengan benar. Komentar Anda tidak berguna bagi siapa pun.
Marquis of Lorne
4

Masalah yang mungkin ditimbulkannya jika Anda tidak melakukan sinkronisasi sebelumnya wait()adalah sebagai berikut:

  1. Jika benang 1 masuk ke makeChangeOnX()dan cek kondisi sementara, dan itu true( x.metCondition()pengembalian false, berarti x.conditionadalah false) sehingga akan mendapatkan di dalamnya. Kemudian tepat sebelum wait()metode, utas lain pergi ke setConditionToTrue()dan mengatur x.conditionke truedan notifyAll().
  2. Kemudian hanya setelah itu, utas pertama akan memasuki wait()metodenya (tidak terpengaruh oleh notifyAll()yang terjadi beberapa saat sebelumnya). Dalam hal ini, utas pertama akan tetap menunggu utas lainnya tampil setConditionToTrue(), tetapi itu mungkin tidak terjadi lagi.

Tetapi jika Anda meletakkan synchronizedsebelum metode yang mengubah keadaan objek, ini tidak akan terjadi.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}
Shay.G
sumber
2

Kita semua tahu bahwa metode wait (), notify () dan notifyAll () digunakan untuk komunikasi antar-thread. Untuk menghilangkan sinyal yang tidak terjawab dan masalah bangun palsu, thread menunggu selalu menunggu pada beberapa kondisi. misalnya-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Kemudian memberitahukan set thread adalah Variabel terverifikasi ke true dan memberitahukan.

Setiap utas memiliki cache lokal mereka sehingga semua perubahan pertama kali ditulis di sana dan kemudian dipromosikan ke memori utama secara bertahap.

Seandainya metode ini tidak dipanggil dalam blok yang disinkronkan, variabel wasNotified tidak akan masuk ke memori utama dan akan ada di cache lokal thread sehingga thread yang menunggu akan tetap menunggu sinyal meskipun sudah diatur ulang dengan memberi tahu thread.

Untuk memperbaiki masalah jenis ini, metode ini selalu dipanggil di dalam blok yang disinkronkan yang memastikan bahwa ketika blok yang disinkronkan dimulai maka semuanya akan dibaca dari memori utama dan akan dimasukkan ke dalam memori utama sebelum keluar dari blok yang disinkronkan.

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Terima kasih, harap ini menjelaskan.

Aavesh Yadav
sumber
1

Ini pada dasarnya ada hubungannya dengan arsitektur perangkat keras (yaitu RAM dan cache ).

Jika Anda tidak menggunakannya synchronizedbersama-sama dengan wait()atau notify(), utas lain dapat memasuki blok yang sama alih-alih menunggu monitor untuk memasukkannya. Selain itu, ketika misalnya mengakses array tanpa blok yang disinkronkan, utas lain mungkin tidak melihat perubahannya ... sebenarnya utas lain tidak akan melihat perubahan apa pun ketika sudah memiliki salinan array dalam cache tingkat-x ( alias cache level 1/2/3) dari thread yang menangani inti CPU.

Tetapi blok yang disinkronkan hanya satu sisi dari medali: Jika Anda benar-benar mengakses suatu objek dalam konteks yang disinkronkan dari konteks yang tidak disinkronkan, objek itu masih tidak akan disinkronkan bahkan di dalam blok yang disinkronkan, karena itu memegang salinan sendiri dari objek dalam cache-nya. Saya menulis tentang masalah ini di sini: https://stackoverflow.com/a/21462631 dan Ketika kunci memegang objek non-final, dapatkah referensi objek masih diubah oleh utas lain?

Selain itu, saya yakin bahwa cache tingkat-x bertanggung jawab atas sebagian besar kesalahan runtime yang tidak dapat direproduksi. Itu karena pengembang biasanya tidak mempelajari hal-hal tingkat rendah, seperti bagaimana CPU bekerja atau bagaimana hirarki memori mempengaruhi jalannya aplikasi: http://en.wikipedia.org/wiki/Memory_hierarchy

Tetap menjadi teka-teki mengapa kelas pemrograman tidak dimulai dengan hierarki memori dan arsitektur CPU terlebih dahulu. "Halo dunia" tidak akan membantu di sini. ;)

Marcus
sumber
1
Baru saja menemukan sebuah situs web yang menjelaskannya dengan sempurna dan mendalam: javamex.com/tutorials/…
Marcus
Hmm .. tidak yakin saya ikuti. Jika caching adalah satu-satunya alasan untuk menempatkan wait dan notifikasi di dalam disinkronkan, mengapa sinkronisasi tidak dimasukkan ke dalam implementasi wait / notify?
aioobe
Pertanyaan bagus, karena menunggu / memberi tahu metode sinkronisasi yang sangat baik ... mungkin mantan pengembang Java Sun tahu jawabannya? Lihat tautan di atas, atau mungkin ini juga akan membantu Anda: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus
Alasannya mungkin: Pada hari-hari awal Jawa tidak ada kesalahan kompilasi ketika tidak memanggil disinkronkan sebelum melakukan operasi multithreading ini. Sebaliknya hanya ada kesalahan runtime (misalnya coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Mungkin mereka benar-benar berpikir @SUN bahwa ketika programmer mendapatkan kesalahan ini mereka dihubungi, yang mungkin memberi mereka kesempatan untuk menjual lebih banyak server mereka. Kapan itu berubah? Mungkin Java 5.0 atau 6.0, tetapi sebenarnya saya tidak ingat untuk jujur ​​...
Marcus
TBH Saya melihat beberapa masalah dengan jawaban Anda 1) Kalimat kedua Anda tidak masuk akal: Tidak masalah objek mana yang dikunci. Apa pun objek yang disinkronkan oleh dua utas, semua perubahan dibuat terlihat. 2) Anda mengatakan utas lain "tidak akan" melihat perubahan. Ini harus "mungkin tidak" . 3) Saya tidak tahu mengapa Anda memunculkan cache tingkat 1/2/3 ... Yang penting di sini adalah apa yang dikatakan oleh Java Memory Model dan yang ditentukan di JLS. Sementara arsitektur perangkat keras dapat membantu dalam memahami mengapa JLS mengatakan apa yang dilakukannya, itu benar-benar berbicara tidak relevan dalam konteks ini.
aioobe
0

langsung dari ini java oracle tutorial:

Ketika sebuah thread memanggil d.wait, ia harus memiliki kunci intrinsik untuk d - jika tidak, akan terjadi kesalahan. Meminta tunggu di dalam metode yang disinkronkan adalah cara sederhana untuk mendapatkan kunci intrinsik.

Rollerball
sumber
Dari pertanyaan yang penulis buat, sepertinya pertanyaan penulis tidak memiliki pemahaman yang jelas tentang apa yang saya kutip dari tutorial. Dan terlebih lagi, jawaban saya, menjelaskan "Mengapa".
Rollerball
0

Saat Anda memanggil notify () dari objek t, java memberi tahu metode t.wait () tertentu. Tapi, bagaimana pencarian java dan memberitahukan metode menunggu tertentu.

java hanya melihat ke blok kode yang disinkronkan yang dikunci oleh objek t. java tidak dapat mencari seluruh kode untuk memberi tahu t.wait () tertentu.

lakshman reddy
sumber
0

sesuai dokumen:

Utas saat ini harus memiliki monitor objek ini. Utas melepaskan kepemilikan monitor ini.

wait()Metode hanya berarti melepaskan kunci pada objek. Jadi objek hanya akan dikunci dalam blok / metode yang disinkronkan. Jika utas di luar blok sinkronisasi berarti tidak dikunci, jika tidak dikunci maka apa yang akan Anda lepaskan pada objek?

Arun Raaj
sumber
0

Utas menunggu pada objek pemantauan (objek yang digunakan oleh blok sinkronisasi), Dapat ada n jumlah objek pemantauan dalam seluruh perjalanan satu utas. Jika Thread menunggu di luar blok sinkronisasi maka tidak ada objek pemantauan dan juga utas lainnya memberi tahu untuk mengakses objek pemantauan, jadi bagaimana utas di luar blok sinkronisasi akan tahu bahwa ia telah diberitahu. Ini juga salah satu alasan bahwa wait (), notify () dan notifyAll () berada di kelas objek daripada kelas thread.

Pada dasarnya objek pemantauan adalah sumber umum di sini untuk semua utas, dan objek pemantauan hanya dapat tersedia di blok sinkronisasi.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
aditya bilah
sumber