Neden wait () her zaman senkronize blokta olmalıdır?


257

Hepimiz biliyoruz ki, çağırmak için Object.wait()bu çağrı senkronize blokta yapılmalıdır, aksi takdirde bir IllegalMonitorStateExceptionatılır. Ancak bu kısıtlamayı yapmanın nedeni nedir? wait()Monitörü serbest bıraktığını biliyorum , ancak neden belirli bir bloğu senkronize ederek monitörü açıkça almamız gerekiyor ve daha sonra arayarak monitörü serbest bırakmamız gerekiyor wait()?

Semantiğini wait()koruyarak, arayan iş parçacığını askıya alarak senkronize edilmiş bir bloğun dışına çağırmak mümkün olsaydı potansiyel hasar nedir ?

Yanıtlar:


232

A wait()yalnızca bir de olduğunda mantıklıdır notify(), bu yüzden her zaman iş parçacıkları arasındaki iletişim ile ilgilidir ve düzgün çalışması için senkronizasyon gerekir. Bu, örtük olması gerektiğini iddia edebilir, ancak aşağıdaki nedenden dolayı bu gerçekten yardımcı olmaz:

Anlamsal olarak, asla sadece wait(). Satsifiye olmak için bazı koşullara ihtiyacınız var ve eğer değilse, o kadar bekleyin. Gerçekten yaptığınız şey

if(!condition){
    wait();
}

Ancak koşul ayrı bir iş parçacığı tarafından ayarlanıyor, bu nedenle bu çalışmanın doğru bir şekilde çalışması için senkronizasyona ihtiyacınız var.

Birkaç şey daha yanlış, burada iş parçacığından beklemeniz aradığınız durumun doğru olmadığı anlamına gelmez:

  • Sahte uyanmalar alabilirsiniz (yani bir ileti dizisinin bildirim almadan beklemeden uyanabileceği anlamına gelir) veya

  • Koşul ayarlanabilir, ancak üçüncü bir iş parçacığı, bekleyen iş parçacığı uyandığında (ve monitörü yeniden aldığında) durumu yeniden yanlış yapar.

Bu vakalarla başa çıkmak için gerçekten ihtiyacınız olan şey her zaman bunun bir çeşitlemesidir:

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

Daha da iyisi, senkronizasyon ilkelleriyle hiç uğraşmayın ve java.util.concurrentpaketlerde sunulan soyutlamalarla çalışmayın .


3
Burada da aynı şeyi söyleyen ayrıntılı bir tartışma var. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
btw, kesintiye uğramış bayrağı görmezden gelmezseniz, döngü de kontrol edecektir Thread.interrupted().
bestsss

2
Yine de şöyle bir şey yapabilirim: while (! Condition) {eşzamanlı (this) {wait ();}} Bu, durumu kontrol etmek ve wait () eşitlenmiş bir blokta doğru şekilde çağrılsa bile beklemek arasında hala bir yarış olduğu anlamına gelir. Bu kısıtlamanın ardında, belki de Java'da uygulanma şekli nedeniyle başka bir neden var mı?
shrini1000

10
Başka bir kötü senaryo: koşul yanlış, wait () içine girmek üzereyiz ve daha sonra başka bir iş parçacığı durumu değiştirir ve notify () öğesini çağırır. Henüz beklemediğimiz için (), bu bildirimi () özleyeceğiz. Başka bir deyişle, test et ve bekle, değiştir ve bildir atomik olmalı .

1
@Nullpointer: Atom olarak yazılabilen bir türse (doğrudan bir if deyimiyle ima edilen boolean gibi) ve diğer paylaşılan verilerle bağımlılık yoksa, geçici olarak bildirmekten kurtulabilirsiniz. Ancak güncellemenin derhal diğer iş parçacıkları tarafından görülmesini sağlamak için buna veya senkronizasyona ihtiyacınız vardır.
Michael Borgwardt

283

Semantiğini wait()koruyarak, arayan iş parçacığını askıya alarak senkronize edilmiş bir bloğun dışına çağırmak mümkün olsaydı potansiyel hasar nedir ?

Somut bir örneklewait() senkronize edilmiş bir bloğun dışına çağrılabilirse hangi sorunlarla karşılaşacağımızı gösterelim .

Bir engelleme kuyruğu uygulayacağımızı varsayalım (Biliyorum, API'da zaten bir tane var :)

İlk denemede (senkronizasyon olmadan) aşağıdaki satırlar boyunca bir şeyler görünebilir

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

Potansiyel olarak ne olabilir:

  1. Bir tüketici iş parçacığı arar take()ve görür buffer.isEmpty().

  2. Tüketici iş parçacığı aramaya başlamadan önce wait(), bir üretici iş parçacığı gelir ve bir dolu çağırır give(), yani,buffer.add(data); notify();

  3. Tüketici iplik şimdi arayacak wait()(ve kaçırmaknotify() aradı olduğunu).

  4. Şanssızsa, üretici ipliği give(), tüketici ipliğinin asla uyanmadığı ve daha fazla kilitlememizin bir sonucu olarak daha fazla üretmez .

Sorunu anladıktan sonra çözüm açıktır: ve ile asla çağrılmadığından synchronizedemin olmak notifyiçin kullanın .isEmptywait

Ayrıntılara girmeden: Bu senkronizasyon sorunu evrenseldir. Michael Borgwardt'ın işaret ettiği gibi, bekle / bildir tamamen dişler arasındaki iletişim ile ilgilidir, bu yüzden her zaman yukarıda tarif edilene benzer bir yarış durumu elde edersiniz. Bu nedenle "yalnızca senkronize içinde bekle" kuralı uygulanır.


@Willie tarafından yayınlanan bağlantıdan bir paragraf bunu oldukça iyi özetliyor:

Garson ve bildirim yapan kişinin yüklemin durumu konusunda hemfikir olduğu konusunda mutlak bir garantiye ihtiyacınız vardır. Garson, uyumadan ÖNCE yüklemin durumunu bir noktada hafifçe kontrol eder, ancak uyumaya gittiğinde yüklemin doğruluğuna bağlıdır. Bu iki olay arasında, programı bozabilecek bir güvenlik açığı vardır.

Üreticinin ve tüketicinin üzerinde anlaşmaya varmasının gerekliliği yukarıdaki örnektir buffer.isEmpty(). Anlaşma, bekleme ve bildirimin synchronizedbloklar halinde yapılmasını sağlayarak çözülür .


Bu yazı burada bir makale olarak yeniden yazılmıştır: Java: Neden bekleme senkronize bir blokta çağrılmalıdır


Buna ek olarak, koşulda yapılan değişikliklerin wait () bittikten hemen sonra görüldüğünden emin olmak için sanırım. Aksi takdirde, notify () zaten çağrıldığından beri ölü bir kilit de vardır.
Surya Wijaya Madjid

İlginç, ama sadece senkronize çağrının aslında wait () ve notify () 'in güvenilmez doğası nedeniyle bu tür sorunları her zaman çözmeyeceğini unutmayın. Daha fazla bilgiyi buradan edinebilirsiniz: stackoverflow.com/questions/21439355/… . Eşitlemenin gerekli olmasının nedeni donanım mimarisinde yatmaktadır (aşağıdaki cevabıma bakın).
Marcus

ama eğer return buffer.remove();blok eklerseniz ama sonra wait();çalışırsa?
BobJiang

@BojJiang, hayır, iplik vermek isteyen biri dışında nedenlerle uyandırılabilir. Başka bir deyişle, arabellek waitdönüşten sonra bile boş olabilir .
aioobe

Sadece var Thread.currentThread().wait();içinde mainfonksiyon için try-catch ile çevrilidir InterruptedException. synchronizedBlok olmadan bana aynı istisnayı verir IllegalMonitorStateException. Şimdi yasadışı duruma ulaşmasını sağlayan nedir? Yine de synchronizedblok içinde çalışır .
Shashwat

12

@Rollerball haklı. wait()İplik bu ortaya bazı durum için bekleyebilir ki, denir wait()çağrı iplik onun kilidini vazgeçmek zorunda kalır, olmuyor.
Bir şeyden vazgeçmek için önce ona sahip olmanız gerekir. İş parçacığının önce kilide sahip olması gerekir. Bu nedenle bir synchronizedyöntem / blok içinde çağırma ihtiyacı .

Evet, synchronizedyöntem / blok içindeki durumu kontrol etmediyseniz, olası zararlar / tutarsızlıklar ile ilgili yukarıdaki tüm cevapları kabul ediyorum . Ancak @ shrini1000'in işaret ettiği gibi, sadece wait()senkronize blok içinde çağırmak bu tutarsızlığın olmasını engellemez.

İşte güzel bir okuma ..


5
@ Popeye 'Düzgün' doğru şekilde açıklayınız. Yorumunuz hiç kimseye faydası yok.
Lorne Marquis

4

Eğer yoksa o yol açabilir sorun değil önce senkronize wait()aşağıdaki gibidir:

  1. 1 diş girerse makeChangeOnX()süre koşulu ve çekler, ve öyle true( x.metCondition()döner false, aracı x.conditionolduğunu falsebunun içine alacak şekilde). Sonra hemen önce wait()yöntemle, başka bir iş parçacığı gider setConditionToTrue()ve setler x.conditioniçin trueve notifyAll().
  2. Sonra sadece bundan sonra, 1. iş parçacığı wait()yöntemine girecektir ( notifyAll()bundan birkaç dakika önce etkilenenlerden etkilenmez ). Bu durumda, 1. iş parçacığı başka bir iş parçacığının gerçekleştirilmesini beklemeye devam eder setConditionToTrue(), ancak bu bir daha gerçekleşmeyebilir.

Ancak synchronized, nesne durumunu değiştiren yöntemlerin önüne koyarsanız , bu olmayacaktır.

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

2

Vidalı iletişimler için wait (), notify () ve notifyAll () yöntemlerinin kullanıldığını hepimiz biliyoruz. Cevapsız sinyalden ve sahte uyandırma problemlerinden kurtulmak için, bekleyen iplik her zaman bazı koşulları bekler. Örneğin-

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

Sonra iş parçacığı kümeleri bildirilmesi wasNotified değişkeni true olarak ve bildirir.

Her iş parçacığının yerel önbelleği vardır, böylece tüm değişiklikler önce oraya yazılır ve daha sonra yavaş yavaş ana belleğe yükseltilir.

Bu yöntemler senkronize blok içinde başlatılmasaydı, wasNotified değişkeni ana belleğe boşaltılmazdı ve iş parçacığının yerel önbelleğinde olurdu, bu nedenle bekleyen iş parçacığı, iş parçacığı bildirerek sıfırlanmasına rağmen sinyali beklemeye devam eder.

Bu tür sorunları gidermek için, bu yöntemler her zaman senkronize blok içinde çağrılır, bu senkronize blok başladığında her şeyin ana bellekten okunmasını ve senkronize bloktan çıkmadan önce ana belleğe akıtılmasını sağlar.

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

Teşekkürler, umarım açıklar.


1

Bu temel olarak donanım mimarisi (yani RAM ve önbellekler ) ile ilgilidir.

Eğer kullanmıyorsanız synchronizedile birlikte wait()veya notify()başka bir iş parçacığı olabilir yerine girmek için monitörün bekleyen aynı bloğunu girin. Senkronize bir blok olmaksızın bir dizi erişirken örneğin Dahası, başka bir iş parçacığı aslında başka iş parçacığı ... buna changement görmeyebilir olmaz Bunda herhangi changements bkz zaman zaten x düzeyinde önbellekte dizinin bir kopyası vardır ( 1. / 2. / 3. seviye önbellekler).

Ancak senkronize edilmiş bloklar madalyanın sadece bir tarafıdır: Senkronize edilmiş bağlamdaki bir nesneye gerçekten senkronize edilmemiş bir bağlamdan erişirseniz, nesne senkronize edilmiş bir blok içinde bile senkronize edilmez, çünkü nesnesi önbelleğinde. Bu sorunlar hakkında burada yazdım: https://stackoverflow.com/a/21462631 ve Bir kilit son olmayan bir nesneyi barındırdığında, nesnenin başvurusu yine de başka bir iş parçacığı tarafından değiştirilebilir mi?

Ayrıca, x düzeyinde önbelleklerin çoğaltılamayan çalışma zamanı hatalarından sorumlu olduğuna ikna oldum. Çünkü geliştiriciler genellikle CPU'nun nasıl çalıştığı veya bellek hiyerarşisinin uygulamaların çalışmasını nasıl etkilediği gibi düşük seviyeli şeyleri öğrenmezler: http://en.wikipedia.org/wiki/Memory_hierarchy

Programlama sınıflarının neden önce bellek hiyerarşisi ve CPU mimarisi ile başlamaması bir bilmece olmaya devam ediyor. "Merhaba dünya" burada yardımcı olmayacak. ;)


1
Mükemmel ve derinlemesine açıklayan bir web sitesi keşfettim: javamex.com/tutorials/…
Marcus

Hmm .. takip ettiğimden emin değilim. Önbelleğe alma, beklemeye ve bildirimi içine senkronize etmenin tek nedeniyse, senkronizasyon neden bekle / bildir uygulamasının içine yerleştirilmez?
aioobe

İyi soru, çünkü bekle / bildir çok iyi senkronize yöntemler olabilir ... belki Sun'ın eski Java geliştiricileri cevabı biliyor? Yukarıdaki bağlantıya bir göz atın veya belki de bu size yardımcı olacaktır: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

Bunun nedeni şunlar olabilir: Java'nın ilk günlerinde bu çok iş parçacıklı işlemler gerçekleştirilmeden önce senkronize çağrılmadığında derleme hataları yoktu. Bunun yerine yalnızca çalışma zamanı hataları vardı (örn. Coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Belki de @SUN'un programcıların bu hataları aldıkları zaman iletişim kurulduklarını düşündüler, bu da onlara sunucularının daha fazlasını satma fırsatı vermiş olabilir. Ne zaman değişti? Belki Java 5.0 veya 6.0, ama aslında dürüst olmayı hatırlamıyorum ...
Marcus

TBH Cevabınızla ilgili birkaç sorun görüyorum 1) İkinci cümlenizin bir anlamı yok: Bir iş parçacığının hangi nesnede kilitli olduğu önemli değildir . İki iş parçacığının hangi nesne üzerinde senkronize edildiğine bakılmaksızın, tüm değişiklikler görünür hale getirilir. 2) Başka bir iş parçacığı demek "olmaz" herhangi bir değişiklik bakın. Bu "olmayabilir" olmalıdır . 3) Neden 1. / 2. / 3. seviye önbellekleri getirdiğinizi bilmiyorum ... Burada önemli olan Java Bellek Modelinin söylediği ve JLS'de belirtildiği. Donanım mimarisi JLS'nin neden ne söylediğini anlamaya yardımcı olsa da, bu bağlamda kesinlikle önemsizdir.
aioobe

0

doğrudan bu java oracle eğitiminden:

Bir iş parçacığı d.wait komutunu çağırdığında, d için içsel kilide sahip olmalıdır - aksi halde bir hata atılır. Senkronize bir yöntem içinde beklemeyi çağırmak, içsel kilidi elde etmenin basit bir yoludur.


Yazarın yaptığı sorudan, soru yazarının öğreticiden alıntıladığım şey hakkında net bir anlayışa sahip olduğu görülmüyor ve dahası, cevabım "Neden" i açıklıyor.
Rollerball

0

T nesnesinden notify () öğesini çağırdığınızda, java belirli bir t.wait () yöntemini bildirir. Ancak, java belirli bir bekleme yöntemini nasıl arar ve bildirir.

java yalnızca t nesnesi tarafından kilitlenen senkronize edilmiş kod bloğuna bakar. java belirli bir t.wait () öğesini bildirmek için kodun tamamını arayamaz.


0

dokümanlara göre:

Geçerli evre bu nesnenin monitörüne sahip olmalıdır. İş parçacığı bu monitörün sahipliğini serbest bırakır.

wait()yöntem basitçe nesnenin üzerindeki kilidi açtığı anlamına gelir. Böylece nesne sadece senkronize blok / yöntem içinde kilitlenir. İş parçacığı eşitleme bloğunun dışındaysa, kilitli olmadığı anlamına gelir, kilitli değilse nesne üzerinde ne serbest bırakırsınız?


0

İzleme nesnesindeki iş parçacığı beklemesi (senkronizasyon bloğu tarafından kullanılan nesne), Tek bir iş parçacığının tüm yolculuğunda n sayıda izleme nesnesi olabilir. İş parçacığı eşitleme bloğunun dışında beklerse, izleme nesnesi yoktur ve ayrıca başka iş parçacığı izleme nesnesine erişim bildirir, bu nedenle eşitleme bloğunun dışındaki iş parçacığına nasıl bildirildiğini bilir. Bu, wait (), notify () ve notifyAll () öğesinin thread sınıfı yerine nesne sınıfında olmasının nedenlerinden biridir.

Temel olarak izleme nesnesi burada tüm evreler için ortak bir kaynaktır ve izleme nesneleri sadece senkronizasyon bloğunda kullanılabilir.

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**
       }
  }
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.