प्रतीक्षा () हमेशा सिंक्रनाइज़ ब्लॉक में क्यों होनी चाहिए


257

हम सभी जानते हैं कि आह्वान करने के लिए Object.wait(), इस कॉल को सिंक्रनाइज़ ब्लॉक में रखा जाना चाहिए, अन्यथा IllegalMonitorStateExceptionइसे फेंक दिया जाता है। लेकिन इस प्रतिबंध को बनाने का क्या कारण है? मुझे पता है कि wait()मॉनिटर जारी करता है, लेकिन हमें विशेष ब्लॉक को सिंक्रनाइज़ करके मॉनिटर को स्पष्ट रूप से प्राप्त करने की आवश्यकता क्यों है और फिर कॉल करके मॉनिटर को रिलीज़ करें wait()?

संभावित नुकसान क्या है अगर यह wait()एक सिंक्रनाइज़ ब्लॉक के बाहर आह्वान करना संभव था , यह शब्दार्थ को बनाए रखना है - कॉलर थ्रेड को निलंबित करना?

जवाबों:


232

एक wait()ही समझ में आता है जब वहाँ भी है notify(), तो यह हमेशा धागे के बीच संचार के बारे में है, और यह सही ढंग से काम करने के लिए सिंक्रनाइज़ेशन की आवश्यकता है। एक तर्क दे सकता है कि यह अंतर्निहित होना चाहिए, लेकिन यह वास्तव में मदद नहीं करेगा, निम्नलिखित कारण के लिए:

शब्दार्थ, आप कभी नहीं wait()। आपको सत्संग करने के लिए कुछ शर्त की आवश्यकता होती है, और यदि यह नहीं है, तो आप तब तक प्रतीक्षा करते हैं जब तक यह है। तो आप वास्तव में क्या करते हैं

if(!condition){
    wait();
}

लेकिन स्थिति एक अलग थ्रेड द्वारा सेट की जा रही है, इसलिए इस काम को सही ढंग से करने के लिए आपको सिंक्रनाइज़ेशन की आवश्यकता है।

एक जोड़े ने इसके साथ कुछ और गलत किया, जहां सिर्फ इसलिए कि आपके धागे ने इंतजार करना छोड़ दिया, इसका मतलब यह नहीं है कि आप जिस स्थिति की तलाश कर रहे हैं वह सच है:

  • आप स्पिरिचुअल वेकअप प्राप्त कर सकते हैं (जिसका अर्थ है कि एक थ्रेड बिना किसी सूचना के प्रतीक्षा करने से जाग सकता है), या

  • स्थिति सेट हो सकती है, लेकिन एक तीसरा धागा उस स्थिति को फिर से गलत बनाता है जब तक प्रतीक्षा धागा जाग जाता है (और मॉनिटर को पुनः प्राप्त करता है)।

इन मामलों से निपटने के लिए जो आपको वास्तव में चाहिए, वह है हमेशा इसके कुछ बदलाव:

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

बेहतर अभी तक, तुल्यकालन आदिम के साथ बिल्कुल भी गड़बड़ न करें और java.util.concurrentपैकेज में पेश किए गए सार के साथ काम करें ।


3
अनिवार्य रूप से एक ही बात कहते हुए, यहां एक विस्तृत चर्चा है। coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
btw, यदि आप बाधित ध्वज को अनदेखा नहीं कर रहे हैं तो लूप भी जाँच करेगा Thread.interrupted()
bestsss

2
मैं अभी भी कुछ ऐसा कर सकता हूं: जबकि (स्थिति!) {सिंक्रोनाइज़ (यह) {प्रतीक्षा ();}} जिसका अर्थ है कि अभी भी स्थिति की जाँच करने और प्रतीक्षा करने के बीच की एक दौड़ है, भले ही प्रतीक्षा () को एक सिंक ब्लॉक में सही ढंग से कहा जाता है। तो क्या इस प्रतिबंध के पीछे कोई और कारण है, शायद जावा में इसे लागू करने के तरीके के कारण?
तीर्थाटन १००

9
एक और बुरा परिदृश्य: स्थिति झूठी है, हम प्रतीक्षा में () जाने वाले हैं और फिर एक और धागा स्थिति को बदलता है और सूचित करता है ()। क्योंकि हम अभी इंतजार में नहीं हैं () हम इस सूचना को याद करेंगे ()। दूसरे शब्दों में, परीक्षण और प्रतीक्षा, साथ ही परिवर्तन और अधिसूचित परमाणु होना चाहिए ।

1
@ नलपाइंटर: यदि यह एक प्रकार का है जिसे परमाणु रूप से लिखा जा सकता है (जैसे कि बूलियन इसे एक अगर क्लॉज में सीधे उपयोग करके निहित है) और अन्य साझा किए गए डेटा के साथ अन्योन्याश्रितता नहीं है, तो आप इसे अस्थिर घोषित करने के साथ दूर हो सकते हैं। लेकिन आपको यह सुनिश्चित करने के लिए या तो सिंक्रनाइज़ेशन की आवश्यकता है कि अपडेट तुरंत अन्य थ्रेड्स को दिखाई देगा।
माइकल बोर्गवर्ड

282

संभावित नुकसान क्या है अगर यह wait()एक सिंक्रनाइज़ ब्लॉक के बाहर आह्वान करना संभव था , यह शब्दार्थ को बनाए रखना है - कॉलर थ्रेड को निलंबित करना?

आइए स्पष्ट करें कि हम wait()एक ठोस उदाहरण के साथ सिंक्रनाइज़ किए गए ब्लॉक के बाहर बुलाया जा सकता है तो हम किन मुद्दों पर चलेंगे ।

मान लीजिए हम एक अवरुद्ध कतार को लागू करने वाले थे (मुझे पता है, एपीआई में पहले से ही एक है :)

पहला प्रयास (बिना सिंक्रोनाइज़ेशन) नीचे दी गई लाइनों के साथ कुछ दिख सकता है

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

यह संभवतः वही हो सकता है:

  1. एक उपभोक्ता धागा कॉल take()और कहा कि देखता है buffer.isEmpty()

  2. इससे पहले कि उपभोक्ता धागा कॉल करने के लिए जाता है wait(), एक निर्माता धागा साथ आता है और एक पूर्ण आह्वान करता give()है,buffer.add(data); notify();

  3. उपभोक्ता थ्रेड अब कॉल करेगा wait()(और याद किया जाएगा notify()कि बस बुलाया गया था)।

  4. यदि अशुभ है, तो उत्पादक धागा give()इस तथ्य के परिणामस्वरूप अधिक उत्पादन नहीं करेगा कि उपभोक्ता धागा कभी नहीं उठता है, और हमारे पास एक डेड-लॉक है।

एक बार जब आप इस मुद्दे को समझ लेते हैं, तो समाधान स्पष्ट होता है: synchronizedयह सुनिश्चित करने के लिए उपयोग करें notifyकि कभी भी isEmptyऔर के बीच में नहीं बुलाया जाता है wait

विवरण में जाने के बिना: यह सिंक्रनाइज़ेशन समस्या सार्वभौमिक है। जैसा कि माइकल बोर्गवर्ड बताते हैं, थ्रेड्स के बीच संचार के बारे में प्रतीक्षा / सूचित सब कुछ है, इसलिए आप हमेशा ऊपर वर्णित एक के समान एक दौड़ की स्थिति के साथ समाप्त करेंगे। यही कारण है कि "केवल सिंक्रनाइज़ के अंदर प्रतीक्षा करें" नियम लागू किया गया है।


@Willie द्वारा पोस्ट किए गए लिंक का एक पैराग्राफ इसे अच्छी तरह से सारांशित करता है:

आपको एक पूर्ण गारंटी की आवश्यकता है कि वेटर और नोटिफ़ायर विधेय की स्थिति के बारे में सहमत हैं। वेटर किसी बिंदु पर विधेय की स्थिति की जाँच करता है थोड़ा सोने से पहले, लेकिन यह विधेय पर सही होने के लिए निर्भर करता है जब वह सोने जाता है। उन दो घटनाओं के बीच भेद्यता की अवधि है, जो कार्यक्रम को तोड़ सकती है।

निर्माता और उपभोक्ता को इस बात पर सहमत होने की आवश्यकता है कि उपर्युक्त उदाहरण में है buffer.isEmpty()। और यह सुनिश्चित करने के लिए समझौता किया जाता है कि synchronizedब्लॉक में प्रतीक्षा और सूचना का प्रदर्शन किया जाए ।


इस पोस्ट को यहां एक लेख के रूप में फिर से लिखा गया है: जावा: प्रतीक्षा को एक सिंक्रनाइज़ ब्लॉक में क्यों बुलाया जाना चाहिए


इसके अलावा, यह सुनिश्चित करने के लिए कि प्रतीक्षा करने () के खत्म होने के तुरंत बाद हालत में बदलाव देखा जाता है, मुझे लगता है। अन्यथा, अधिसूचित () से पहले से ही एक डेड-लॉक भी कहा जाता है।
सूर्या विजया मदजीद

दिलचस्प है, लेकिन ध्यान दें कि केवल सिंक्रनाइज़ कॉलिंग वास्तव में इंतजार की "अविश्वसनीय" प्रकृति () और सूचित () के कारण ऐसी समस्याओं को हल नहीं करेगी। यहां अधिक पढ़ें: stackoverflow.com/questions/21439355/... । हार्डवेयर आर्किटेक्चर के भीतर सिंक्रनाइज़ किए जाने की आवश्यकता क्यों है इसका कारण (नीचे मेरा उत्तर देखें)।
मार्कस

लेकिन अगर return buffer.remove();ब्लॉक में जोड़ने के बाद wait();, लेकिन यह काम करता है?
बोबजियांग

@ याकूबजियांग, नहीं, किसी को फोन देने के अलावा अन्य कारणों से धागे को जगाया जा सकता है। दूसरे शब्दों में, बफर waitरिटर्न के बाद भी खाली हो सकता है।
aioobe

मेरे पास केवल Thread.currentThread().wait();उस mainफंक्शन के लिए है, जिसे मैंने कोशिश की थी InterruptedExceptionsynchronizedब्लॉक के बिना , यह मुझे एक ही अपवाद देता है IllegalMonitorStateException। अब यह अवैध राज्य तक कैसे पहुँचता है? synchronizedहालांकि यह ब्लॉक के अंदर काम करता है ।
शशवत

12

@ रोलरबॉल सही है। wait()कहा जाता है, ताकि धागे तब होता है जब यह कुछ हालत के लिए इंतजार कर सकते हैं wait()कॉल, होता है धागा अपने ताला देने के लिए मजबूर किया जाता है।
किसी चीज़ को त्यागने के लिए, आपको पहले खुद की ज़रूरत है। थ्रेड को पहले लॉक का मालिक होना चाहिए। इसलिए इसे एक synchronizedविधि / ब्लॉक के अंदर कॉल करने की आवश्यकता है ।

हां, मैं संभावित नुकसान / विसंगतियों के बारे में उपरोक्त सभी उत्तरों से सहमत हूं यदि आपने synchronizedविधि / ब्लॉक के भीतर की स्थिति की जांच नहीं की है । हालाँकि, @ shrini1000 ने बताया है कि, केवल wait()सिंक्रोनाइज़्ड ब्लॉक में कॉल करने से यह असंगति घटित नहीं होगी।

यहाँ एक अच्छा पढ़ा है ..


5
@Popeye 'ठीक से' ठीक से समझाएँ। आपकी टिप्पणी से किसी को कोई फायदा नहीं है।
लोरने का मार्किस

4

समस्या यह हो सकती है यदि आप निम्नानुसार पहले सिंक्रनाइज़ नहीं करते wait()हैं:

  1. यदि 1 थ्रेड में जाता है makeChangeOnX()और समय की स्थिति की जांच करता है, और यह है true( x.metCondition()रिटर्न false, साधन x.conditionहै false) तो यह उसके अंदर मिल जाएगा। फिर wait()विधि से ठीक पहले , एक और धागा करने setConditionToTrue()के x.conditionलिए trueऔर करने के लिए सेट और चला जाता है notifyAll()
  2. फिर उसके बाद ही, पहला धागा उसकी wait()विधि में प्रवेश करेगा ( notifyAll()कुछ समय पहले हुई घटना से प्रभावित नहीं )। इस स्थिति में, 1 थ्रेड दूसरे थ्रेड के प्रदर्शन की प्रतीक्षा में रहेगा setConditionToTrue(), लेकिन ऐसा दोबारा नहीं हो सकता है।

लेकिन यदि आप synchronizedउन तरीकों के सामने रखते हैं जो ऑब्जेक्ट स्थिति को बदलते हैं, तो ऐसा नहीं होगा।

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

हम सभी जानते हैं कि इंटर-थ्रेडेड संचार के लिए प्रतीक्षा (), सूचित () और सूचना () तरीके का उपयोग किया जाता है। मिस्ड सिग्नल और स्परियस वेक अप समस्याओं से छुटकारा पाने के लिए वेटिंग थ्रेड हमेशा कुछ शर्तों पर प्रतीक्षा करता है। eg-

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

तब थ्रेड सेट को सूचित करना सही और अधिसूचित करने के लिए चर चर था।

प्रत्येक थ्रेड में उनका स्थानीय कैश होता है इसलिए सभी परिवर्तन पहले वहां लिखे जाते हैं और फिर धीरे-धीरे मुख्य मेमोरी में पदोन्नत कर दिए जाते हैं।

अगर इन विधियों को सिंक्रोनाइज़्ड ब्लॉक के भीतर नहीं लगाया जाता है, तो वाष्पित वैरिएबल को मुख्य मेमोरी में फ्लश नहीं किया जाएगा और थ्रेड के लोकल कैश में होगा इसलिए वेटिंग थ्रेड संकेत की प्रतीक्षा करता रहेगा, हालाँकि इसे थ्रेड सूचित करके रीसेट किया गया था।

इस प्रकार की समस्याओं को ठीक करने के लिए, इन विधियों को हमेशा सिंक्रोनाइज़्ड ब्लॉक के अंदर किया जाता है जो यह आश्वासन देता है कि जब सिंक्रोनाइज़्ड ब्लॉक शुरू होगा तो सब कुछ मुख्य मेमोरी से पढ़ा जाएगा और सिंक्रोनाइज़्ड ब्लॉक से बाहर निकलने से पहले मुख्य मेमोरी में फ्लश किया जाएगा।

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

धन्यवाद, आशा है कि यह स्पष्ट करता है।


1

यह मूल रूप से हार्डवेयर आर्किटेक्चर (यानी रैम और कैश ) के साथ करना है।

आप उपयोग नहीं करते हैं synchronizedके साथ एक साथ wait()या notify(), एक और धागा सकता है बजाय मॉनिटर इसे दर्ज करने के लिए इंतज़ार कर के एक ही ब्लॉक दर्ज करें। इसके अलावा, जब एक सरणी को एक सिंक्रनाइज़ किए गए ब्लॉक के बिना एक्सेस किया जाता है, तो दूसरा थ्रेड इसे बदलने के लिए नहीं देख सकता है ... वास्तव में किसी अन्य थ्रेड को इसमें कोई परिवर्तन नहीं दिखाई देगा, जब उसके पास एक्स-स्तरीय कैश में सरणी की एक प्रति हो ( सीपीयू कोर को संभालने वाले थ्रेड का उर्फ ​​1st / 2nd / 3rd-level कैश)।

लेकिन सिंक्रनाइज़ किए गए ब्लॉक मेडल का केवल एक पक्ष हैं: यदि आप वास्तव में किसी ऑब्जेक्ट को गैर-सिंक्रनाइज़ किए गए संदर्भ से सिंक्रनाइज़ संदर्भ के भीतर एक्सेस करते हैं, तो ऑब्जेक्ट अभी भी एक सिंक्रनाइज़ किए गए ब्लॉक के भीतर भी सिंक्रनाइज़ नहीं किया जाएगा, क्योंकि यह एक स्वयं की प्रतिलिपि रखता है। इसके कैश में ऑब्जेक्ट। मैंने इस मुद्दे के बारे में यहां लिखा है: https://stackoverflow.com/a/21462631 और जब एक ताला एक गैर-अंतिम ऑब्जेक्ट रखता है, तो क्या ऑब्जेक्ट का संदर्भ अभी भी दूसरे धागे से बदला जा सकता है?

इसके अलावा, मुझे विश्वास है कि एक्स-स्तरीय कैश अधिकांश गैर-प्रतिलिपि प्रस्तुत करने योग्य रनटाइम त्रुटियों के लिए जिम्मेदार हैं। ऐसा इसलिए है क्योंकि डेवलपर्स आमतौर पर निम्न-स्तरीय सामान नहीं सीखते हैं, जैसे सीपीयू का काम या मेमोरी पदानुक्रम कैसे अनुप्रयोगों को चलाने को प्रभावित करता है: http://en.wikipedia.org/wiki/Memory_hierarchy

यह एक पहेली बनी हुई है कि क्यों प्रोग्रामिंग कक्षाएं पहले मेमोरी पदानुक्रम और सीपीयू वास्तुकला के साथ शुरू नहीं होती हैं। "नमस्ते दुनिया" यहाँ मदद नहीं करेगा। ;)


1
बस एक वेबसाइट की खोज की, जो इसे पूरी तरह से और गहराई से बताती है
मार्कस

हम्म .. मुझे यकीन नहीं है। यदि कैशिंग प्रतीक्षा करने और सूचित करने के लिए सिंक्रनाइज़ेशन के अंदर का एकमात्र कारण था, तो प्रतीक्षा / सूचना के कार्यान्वयन के अंदर सिंक्रोनाइज़ेशन क्यों नहीं रखा गया है?
Aioobe

अच्छा सवाल, चूंकि प्रतीक्षा / सूचना बहुत अच्छी तरह से सिंक्रनाइज़ की जा सकती है ... शायद सूर्य के पूर्व जावा डेवलपर्स को इसका जवाब पता है? ऊपर दिए गए लिंक पर एक नज़र डालें, या शायद यह भी आपकी मदद करेगा: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
मार्कस

एक कारण यह हो सकता है: जावा के शुरुआती दिनों में इन मल्टीथ्रेडिंग ऑपरेशंस को करने से पहले सिंक्रोनाइज़ नहीं करने पर कंपाइल एरर थे। इसके बजाय केवल रनटाइम त्रुटियाँ थीं (उदाहरण के लिए coderanch.com/t/239491/java-programmer-SCJP/certification/… )। हो सकता है कि उन्होंने वास्तव में @SUN सोचा हो कि जब प्रोग्रामर को ये त्रुटियां मिल रही हैं, तो उनसे संपर्क किया जा रहा है, जिससे उन्हें अपने सर्वर को अधिक बेचने का अवसर मिल सकता है। यह कब बदल गया? शायद जावा 5.0 या 6.0, लेकिन वास्तव में मुझे ईमानदार होना याद नहीं है ...
मार्कस

टीबीएच मैं आपके उत्तर के साथ कुछ मुद्दों को देखता हूं 1) आपका दूसरा वाक्य समझ में नहीं आता है: इससे कोई फर्क नहीं पड़ता कि किस वस्तु पर एक ताला है। इसके बावजूद कि दो थ्रेड्स किस ऑब्जेक्ट को सिंक्रनाइज़ करते हैं, सभी परिवर्तन दिखाई देते हैं। 2) आप कहते हैं कि एक और धागा " कोई परिवर्तन नहीं देखेगा " । यह "हो सकता है " नहीं होना चाहिए । 3) मुझे नहीं पता कि आप 1st / 2nd / 3rd लेवल कैश क्यों ला रहे हैं ... जावा मेमोरी मॉडल क्या कहता है और जेएलएस में निर्दिष्ट है, यहां क्या मायने रखता है। हालांकि हार्डवेयर आर्किटेक्चर यह समझने में मदद कर सकता है कि जेएलएस क्यों कहता है, वह इस संदर्भ में सख्ती से अप्रासंगिक बोल रहा है।
Aioobe

0

सीधे इस जावा ओर्कल ट्यूटोरियल से:

जब कोई धागा d.wait को आमंत्रित करता है, तो उसे d के लिए आंतरिक लॉक का स्वामी होना चाहिए - अन्यथा एक त्रुटि डाली जाती है। एक समन्वित विधि के अंदर प्रतीक्षा को शामिल करना आंतरिक ताला प्राप्त करने का एक सरल तरीका है।


लेखक द्वारा किए गए प्रश्न से, ऐसा नहीं लगता है कि प्रश्न लेखक को ट्यूटोरियल से क्या उद्धृत किया गया है, इसकी स्पष्ट समझ है। और इसके अलावा, मेरा जवाब, "क्यों" बताते हैं।
रोलरबॉल

0

जब आप ऑब्जेक्ट t से सूचित () कॉल करते हैं, तो java एक विशेष t.wait () विधि को सूचित करता है। लेकिन, जावा कैसे खोजता है और किसी विशेष प्रतीक्षा विधि को सूचित करता है।

java केवल कोड के सिंक्रोनाइज़्ड ब्लॉक को देखता है जो ऑब्जेक्ट t द्वारा लॉक किया गया था। java किसी विशेष t.wait () को सूचित करने के लिए पूरे कोड को नहीं खोज सकता है।


0

डॉक्स के अनुसार:

वर्तमान थ्रेड को इस ऑब्जेक्ट के मॉनीटर का स्वामी होना चाहिए। धागा इस मॉनीटर का स्वामित्व जारी करता है।

wait()विधि बस इसका मतलब है कि यह ऑब्जेक्ट पर लॉक जारी करता है। तो ऑब्जेक्ट केवल सिंक्रनाइज़ किए गए ब्लॉक / विधि के भीतर बंद हो जाएगा। यदि थ्रेड सिंक ब्लॉक के बाहर है तो इसका मतलब है कि यह लॉक नहीं है, अगर यह लॉक नहीं है तो आप ऑब्जेक्ट पर क्या छोड़ेंगे?


0

मॉनिटरिंग ऑब्जेक्ट पर थ्रेड प्रतीक्षा (सिंक्रनाइज़ेशन ब्लॉक द्वारा उपयोग की जाने वाली वस्तु), किसी एकल थ्रेड की संपूर्ण यात्रा में मॉनिटरिंग ऑब्जेक्ट की n संख्या हो सकती है। यदि थ्रेड सिंक्रोनाइज़ेशन ब्लॉक के बाहर प्रतीक्षा करता है, तो मॉनिटरिंग ऑब्जेक्ट नहीं है और मॉनिटरिंग ऑब्जेक्ट के लिए एक्सेस करने के लिए अन्य थ्रेड को सूचित करता है, इसलिए सिंक्रोनाइज़ेशन ब्लॉक के बाहर थ्रेड कैसे पता चलेगा कि यह अधिसूचित किया गया है। यह भी एक कारण है कि थ्रेड क्लास के बजाय प्रतीक्षा (), नोटिफ़िकेशन () और नोटिफिकेशन () ऑब्जेक्ट क्लास में हैं।

मूल रूप से मॉनिटरिंग ऑब्जेक्ट सभी थ्रेड्स के लिए यहां सामान्य संसाधन है, और मॉनिटरिंग ऑब्जेक्ट केवल सिंक्रनाइज़ेशन ब्लॉक में उपलब्ध हो सकते हैं।

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**
       }
  }
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.