खोज…


टिप्पणियों

जावा मेमोरी मॉडल जेएलएस का वह खंड है जो उन शर्तों को निर्दिष्ट करता है जिनके तहत एक धागे के माध्यम से किए गए मेमोरी राइट्स के प्रभावों को देखने के लिए एक थ्रेड की गारंटी दी जाती है। हाल के संस्करणों में संबंधित अनुभाग "JLS 17.4 मेमोरी मॉडल" ( जावा 8 , जावा 7 , जावा 6 में ) है

जावा 5 में जावा मेमोरी मॉडल का एक प्रमुख ओवरहाल था जो (अन्य चीजों के अलावा) ने volatile काम करने के तरीके को बदल दिया। तब से, स्मृति मॉडल अनिवार्य रूप से अपरिवर्तित था।

मेमोरी मॉडल के लिए प्रेरणा

निम्नलिखित उदाहरण पर विचार करें:

public class Example {
    public int a, b, c, d;
    
    public void doIt() {
       a = b + 1;
       c = d + 1;
    }
}

यदि इस वर्ग का उपयोग एकल-थ्रेडेड अनुप्रयोग है, तो अवलोकन योग्य व्यवहार वैसा ही होगा जैसा आप अपेक्षा करेंगे। उदाहरण के लिए:

public class SingleThreaded {
    public static void main(String[] args) {
        Example eg = new Example();
        System.out.println(eg.a + ", " + eg.c);
        eg.doIt();
        System.out.println(eg.a + ", " + eg.c);
    }
}

उत्पादन होगा:

0, 0
1, 1

जहाँ तक "मुख्य" धागा बता सकता है , main() विधि और doIt() विधि में दिए गए कथनों को उस क्रम में निष्पादित किया जाएगा जो वे स्रोत कोड में लिखे गए हैं। यह जावा लैंग्वेज स्पेसिफिकेशन (JLS) की स्पष्ट आवश्यकता है।

अब बहु-थ्रेडेड एप्लिकेशन में उपयोग किए गए समान वर्ग पर विचार करें।

public class MultiThreaded {
    public static void main(String[] args) {
        final Example eg = new Example();
        new Thread(new Runnable() {
            public void run() {
                while (true) {
                    eg.doIt();
                }
            }
        }).start();
        while (true) {
            System.out.println(eg.a + ", " + eg.c);
        }
    }
}

यह क्या छपेगा?

वास्तव में, JLS के अनुसार यह अनुमान लगाना संभव नहीं है कि यह प्रिंट होगा:

  • आप शायद शुरुआत करने के लिए 0, 0 की कुछ पंक्तियाँ देखेंगे।
  • तब आपको संभवतः N, N या N, N + 1 जैसी रेखाएँ दिखाई देंगी।
  • आपको N + 1, N जैसी लाइनें दिखाई दे सकती हैं।
  • सिद्धांत रूप में, आप यह भी देख सकते हैं कि 0, 0 लाइनें हमेशा 1 बनी रहती हैं

1 - व्यवहार में की उपस्थिति println बयान कुछ आकस्मिक लाभ तुल्यकालन और मेमोरी कैश फ्लशिंग पैदा करने के लिए उत्तरदायी है। यह उपरोक्त प्रभावों का कारण बनने वाले कुछ प्रभावों को छिपाने की संभावना है।

तो हम इन्हें कैसे समझा सकते हैं?

असाइनमेंट का पुन: व्यवस्थित करना

अप्रत्याशित परिणामों के लिए एक संभावित स्पष्टीकरण यह है कि JIT संकलक ने doIt() पद्धति में असाइनमेंट के क्रम को बदल दिया है। जेएलएस को वर्तमान थ्रेड के दृष्टिकोण से आदेशों को निष्पादित करने की आवश्यकता होती है। इस मामले में, doIt() पद्धति के कोड में कुछ भी उन दो कथन के (काल्पनिक) पुनरावर्तन के प्रभाव का निरीक्षण नहीं कर सकता है। इसका मतलब है कि JIT कंपाइलर को ऐसा करने की अनुमति होगी।

वह ऐसा क्यों करेगा?

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

मेमोरी कैश का प्रभाव

एक दूसरा संभावित स्पष्टीकरण मेमोरी कैशिंग का प्रभाव है। शास्त्रीय कंप्यूटर वास्तुकला में, प्रत्येक प्रोसेसर में रजिस्टरों का एक छोटा सा सेट होता है, और बड़ी मात्रा में मेमोरी होती है। मुख्य मेमोरी तक पहुंच की तुलना में रजिस्टरों तक पहुंच बहुत तेज है। आधुनिक आर्किटेक्चर में, मेमोरी कैश हैं जो रजिस्टरों की तुलना में धीमी हैं, लेकिन मुख्य मेमोरी से तेज हैं।

एक कंपाइलर रजिस्टरों में या मेमोरी कैश में वेरिएबल्स की प्रतियां रखने की कोशिश करके इसका फायदा उठाएगा। एक चर मुख्य स्मृति में प्लावित होने की जरूरत नहीं होती है, तो या स्मृति से पढ़ा जा की जरूरत नहीं है, वहाँ इस से नहीं कर में महत्वपूर्ण प्रदर्शन लाभ हैं। ऐसे मामलों में जहां जेएलएस को किसी अन्य थ्रेड को दिखाई देने के लिए मेमोरी ऑपरेशन की आवश्यकता नहीं होती है, जावा जेआईटी कंपाइलर को "रीड बैरियर" और "रेज़ बैरियर" निर्देश नहीं जोड़ने की संभावना है जो मुख्य मेमोरी रीड और राइट को मजबूर करेगा। एक बार फिर, ऐसा करने के प्रदर्शन लाभ महत्वपूर्ण हैं।

उचित सिंक्रनाइज़ेशन

अब तक, हमने देखा है कि जेएलएस जेआईटी संकलक को कोड उत्पन्न करने की अनुमति देता है जो स्मृति संचालन को पुन: व्यवस्थित या टालकर एकल-थ्रेडेड कोड को तेजी से बनाता है। लेकिन क्या होता है जब अन्य धागे मुख्य मेमोरी में (साझा) चर की स्थिति का निरीक्षण कर सकते हैं?

इसका उत्तर यह है कि, अन्य सूत्र चर राज्यों का निरीक्षण करने के लिए उत्तरदायी हैं जो कि असंभव प्रतीत होता है ... जावा कथनों के कोड क्रम के आधार पर। इसका समाधान उचित सिंक्रनाइज़ेशन का उपयोग करना है। तीन मुख्य दृष्टिकोण हैं:

  • आदिम म्यूटेक्स और synchronized निर्माण का उपयोग करना।
  • volatile चर का उपयोग करना।
  • उच्च स्तर की संगामिति समर्थन का उपयोग करना; जैसे java.util.concurrent पैकेज में कक्षाएं।

लेकिन इसके साथ भी, यह समझना महत्वपूर्ण है कि सिंक्रनाइज़ेशन की आवश्यकता कहां है, और आप पर क्या प्रभाव डाल सकते हैं। यह वह जगह है जहाँ जावा मेमोरी मॉडल आता है।

मेमोरी मॉडल

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

इसके साथ सशस्त्र, एक जावा कार्यक्रम में मेमोरी सुसंगतता के बारे में तर्क करना संभव है, और यह तय करें कि क्या यह सभी निष्पादन प्लेटफार्मों के लिए अनुमानित और सुसंगत होगा।

होता है-रिश्तों से पहले

(जावा लैंग्वेज स्पेसिफिकेशन जो कहते हैं, उसका एक सरलीकृत संस्करण है। गहन समझ के लिए, आपको स्वयं विनिर्देश पढ़ने की आवश्यकता है।)

हैपन्स-रिलेशनशिप्स मेमोरी मॉडल का हिस्सा हैं जो हमें मेमोरी विजिबिलिटी के बारे में समझने और तर्क करने की अनुमति देते हैं। जैसा कि JLS कहता है ( JLS 17.4.5 ):

"दो क्रियाओं को एक होने से पहले आदेश दिया जा सकता है। यदि एक क्रिया होती है- दूसरे से पहले , तो दूसरी के पहले आदेश दिखाई देता है।"

इसका क्या मतलब है?

क्रिया

उपरोक्त उद्धरण में उल्लिखित क्रियाएँ JLS 17.4.2 में निर्दिष्ट हैं। कल्पना द्वारा परिभाषित 5 प्रकार की क्रिया सूचीबद्ध हैं:

  • पढ़ें: एक गैर-वाष्पशील चर पढ़ना।

  • लिखना: एक गैर-वाष्पशील चर लिखना।

  • तुल्यकालन क्रियाएँ:

    • वाष्पशील पाठ: वाष्पशील चर पढ़ना।

    • वाष्पशील लेखन: वाष्पशील चर लिखना।

    • ताला। एक निगरानी ताला

    • अनलॉक। एक मॉनिटर अनलॉक करना।

    • (सिंथेटिक) एक धागे की पहली और आखिरी क्रिया।

    • क्रियाएं जो एक धागा शुरू करती हैं या पता लगाती हैं कि एक धागा समाप्त हो गया है।

  • बाहरी क्रिया। एक क्रिया जिसका एक परिणाम होता है जो उस वातावरण पर निर्भर करता है जिसमें कार्यक्रम है।

  • सूत्र विचलन क्रिया। ये कुछ प्रकार के अनंत लूप के व्यवहार को दर्शाते हैं।

प्रोग्राम ऑर्डर और सिंक्रोनाइज़ेशन ऑर्डर

ये दो आदेश ( JLS 17.4.3 और JLS 17.4.4 ) जावा में कथनों के निष्पादन को नियंत्रित करते हैं

कार्यक्रम का आदेश एक एकल थ्रेड के भीतर स्टेटमेंट निष्पादन के आदेश का वर्णन करता है।

सिंक्रोनाइज़ेशन ऑर्डर, सिंक्रोनाइज़ेशन द्वारा जुड़े दो स्टेटमेंट्स के लिए स्टेटमेंट एक्जीक्यूशन के ऑर्डर का वर्णन करता है:

  • मॉनिटर पर एक अनलॉक कार्रवाई सिंक्रनाइज़ करती है- उस मॉनिटर पर सभी बाद के लॉक कार्यों के साथ

  • एक अस्थिर चर के लिए एक लेखन किसी भी धागे द्वारा एक ही चर के सभी बाद में पढ़ता है के साथ सिंक्रनाइज़ करता है

  • एक क्रिया जो थ्रेड शुरू करती है (यानी Thread.start() लिए कॉल) को सिंक्रनाइज़ करती है- थ्रेड में पहली क्रिया के साथ यह शुरू होती है (यानी थ्रेड को run() लिए कॉल run() विधि)।

  • फ़ील्ड का डिफ़ॉल्ट इनिशियलाइज़ेशन सिंक्रोनाइज़ करता है- हर थ्रेड में पहली क्रिया के साथ । (इसकी व्याख्या के लिए JLS देखें।)

  • एक थ्रेड में अंतिम क्रिया सिंक्रनाइज़ होती है- किसी अन्य थ्रेड में किसी भी क्रिया के साथ जो समाप्ति का पता लगाती है; जैसे किसी join() कॉल या isTerminated() कॉल की वापसी जो true लौटाता true

  • यदि एक थ्रेड दूसरे थ्रेड को बाधित करता है, तो पहले थ्रेड में इंटरप्ट कॉल सिंक्रोनाइज़ करता है- उस बिंदु के साथ जहां दूसरा थ्रेड यह पता लगाता है कि थ्रेड बाधित हुआ था।

ऑर्डर से पहले होता है

यह ऑर्डरिंग ( JLS 17.4.5 ) वह है जो यह निर्धारित करता है कि मेमोरी मेमोरी बाद के मेमोरी रीड को दिखाई देने की गारंटी है या नहीं।

विशेष रूप से, एक चर के लिए पठन v करने के लिए एक लिखने निरीक्षण करने के लिए गारंटी है v यदि और केवल यदि write(v) होता है-पहले read(v) और वहाँ के लिए कोई हस्तक्षेप लिखने है v । यदि हस्तक्षेप करने वाले लिखते हैं, तो read(v) पहले वाले के बजाय उनके परिणाम देख सकता है।

आदेश देने से पहले होने वाले नियम इस प्रकार हैं:

  • हैपन्स-बिफोर रूल # 1 - यदि x और y एक ही धागे की क्रिया हैं और x, प्रोग्राम क्रम में y से पहले आता है, तो x होता है- y से पहले

  • हप्सेन्स-बिफोर रूल # 2 - किसी ऑब्जेक्ट के कंस्ट्रक्टर के अंत से उस ऑब्जेक्ट के लिए एक फाइनल के शुरू होने से पहले होता है।

  • हप्सेन्स-बिफोर रूल # ३ - यदि कोई एक्शन x सिंक्रोनाइज़ करता है- एक एक्शन वाई के बाद, तो x होता है-इससे पहले y।

  • हैपन्स-बिफोर रूल # 4 - अगर x होता है- y से पहले और y होता है- z से पहले तो x होता है-पहले z।

इसके अलावा, जावा मानक पुस्तकालयों में विभिन्न कक्षाएं संबंधों को परिभाषित करने से पहले निर्दिष्ट की जाती हैं। आप इसका अर्थ यह समझ सकते हैं कि यह किसी भी तरह से होता है, बिना यह जानने की आवश्यकता के कि वास्तव में गारंटी कैसे मिलने वाली है।

कुछ उदाहरणों पर लागू होने से पहले होता है

हम कुछ उदाहरण प्रस्तुत करते हैं कि कैसे होता है- लागू करने से पहले यह जांचने के लिए कि लिखने के बाद के दृश्य दिखाई देते हैं।

एकल-थ्रेडेड कोड

जैसा कि आप उम्मीद करते हैं, लेखन हमेशा एक एकल-थ्रेडेड प्रोग्राम में बाद में पढ़ने के लिए दिखाई देते हैं।

public class SingleThreadExample {
    public int a, b;
    
    public int add() {
       a = 1;         // write(a)
       b = 2;         // write(b)
       return a + b;  // read(a) followed by read(b)
    }
}

नियम # 1 से पहले होता है:

  1. write(a) कार्रवाई होती है - write(b) से पहले write(b) कार्रवाई।
  2. write(b) कार्रवाई होती है - read(a) से पहले read(a) कार्रवाई।
  3. read(a) कार्रवाई होती है - read(a) से पहले read(a) कार्रवाई।

नियम # 4 से पहले होता है:

  1. write(a) होता है- write(b) से पहले write(b) और write(b) होता है- read(a) से पहले read(a) write(a) होता है- read(a) से पहले write(a) होता है
  2. write(b) होता है- read(a) से पहले read(a) और read(a) होता है- read(b) से पहले read(b) write(b) होता है- read(b) से पहले read(b)

उपसंहार:

  1. write(a) होता है- read(a) से पहले read(a) संबंध का मतलब है कि a + b बयान की सही मूल्य देखने की गारंटी a
  2. write(b) होता है-पहले read(b) संबंध का अर्थ है कि a + b बयान के सही मूल्य को देखने के लिए गारंटी है b

2 थ्रेड्स के साथ एक उदाहरण में 'अस्थिर' का व्यवहार

हम `वाष्पशील के लिए मेमोरी मॉडल के कुछ निहितार्थों का पता लगाने के लिए निम्न उदाहरण कोड का उपयोग करेंगे।

public class VolatileExample {
    private volatile int a;
    private int b;         // NOT volatile
    
    public void update(int first, int second) {
       b = first;         // write(b)
       a = second;         // write-volatile(a)
    }

    public int observe() {
       return a + b;       // read-volatile(a) followed by read(b)
    }
}

पहले, 2 थ्रेड्स को शामिल करने वाले कथनों के निम्नलिखित अनुक्रम पर विचार करें:

  1. VolatileExample का एक एकल उदाहरण बनाया गया है; इसे ve ,
  2. ve.update(1, 2) को एक धागे में कहा जाता है, और
  3. ve.observe() को दूसरे धागे में कहा जाता है।

नियम # 1 से पहले होता है:

  1. write(a) कार्रवाई होता है-पहले volatile-write(a) कार्रवाई।
  2. volatile-read(a) क्रिया होती है -पढ़ने से पहले read(b) क्रिया।

नियम # 2 से पहले:

  1. पहले थ्रेड में volatile-write(a) क्रिया होती है- दूसरे थ्रेड में volatile-read(a) कार्रवाई से पहले।

नियम # 4 से पहले होता है:

  1. पहले थ्रेड में write(b) एक्शन होता है- दूसरे थ्रेड में read(b) एक्शन से पहले।

दूसरे शब्दों में, इस विशेष अनुक्रम के लिए, हमें गारंटी दी जाती है कि दूसरा धागा पहले थ्रेड द्वारा बनाए गए गैर-वाष्पशील चर b को अपडेट देखेगा। हालांकि, यह भी स्पष्ट कर दिया कि अगर में कार्य होना चाहिए update विधि दूसरी तरह के आसपास थे, या observe() विधि पढ़ चर b से पहले a , फिर क्या होता है-पहले श्रृंखला टूट किया जाएगा। यदि दूसरे भाग में volatile-read(a) पहले धागे में volatile-write(a) बाद नहीं था तो श्रृंखला भी टूट जाएगी।

जब श्रृंखला टूट जाती है, तो कोई गारंटी नहीं है कि observe() b का सही मूल्य देखेगा।

तीन धागे के साथ अस्थिर

मान लीजिए कि हम पिछले उदाहरण में एक तीसरा सूत्र जोड़ते हैं:

  1. VolatileExample का एक एकल उदाहरण बनाया गया है; इसे ve ,
  2. दो सूत्र कॉल update :
    • ve.update(1, 2) को एक धागे में कहा जाता है,
    • ve.update(3, 4) को दूसरे धागे में कहा जाता है,
  3. ve.observe() को बाद में तीसरे धागे में कहा जाता है।

इसे पूरी तरह से विश्लेषण करने के लिए, हमें थ्रेड एक और थ्रेड टू में कथनों के सभी संभव उपायों पर विचार करना होगा। इसके बजाय, हम उनमें से सिर्फ दो पर विचार करेंगे।

परिदृश्य # 1 - मान लें कि update(1, 2) पूर्ववर्ती update(3,4) हमें यह अनुक्रम मिलता है:

write(b, 1), write-volatile(a, 2)     // first thread
write(b, 3), write-volatile(a, 4)     // second thread
read-volatile(a), read(b)             // third thread

इस मामले में, यह देखना आसान है कि write(b, 3) से read(b) से पहले एक अखंड घटना होती है । इसके अलावा b कोई हस्तक्षेप करने वाला लेखन नहीं है। तो, इस परिदृश्य के लिए, तीसरे धागे को b को मान 3 रूप में देखने की गारंटी है।

परिदृश्य # 2 - मान लें कि update(1, 2) और update(3,4) ओवरलैप है और प्याज इंटरलेयर हैं:

write(b, 3)                           // second thread
write(b, 1)                           // first thread
write-volatile(a, 2)                  // first thread
write-volatile(a, 4)                  // second thread
read-volatile(a), read(b)             // third thread

अब, जबकि write(b, 3) से read(b) से पहले होने वाली श्रृंखला है write(b, 3) read(b) , दूसरे धागे द्वारा किया गया एक हस्तक्षेप write(b, 1) क्रिया है। इसका अर्थ है कि हम निश्चित नहीं हो सकते हैं कि कौन सा मूल्य read(b) जाएगा।

(एक तरफ: यह प्रदर्शित करता है कि हम गैर-वाष्पशील चर की दृश्यता सुनिश्चित करने के लिए volatile पर भरोसा नहीं कर सकते हैं, बहुत ही सीमित परिस्थितियों को छोड़कर।)

मेमोरी मॉडल को समझने की आवश्यकता से कैसे बचें

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

यदि आप जावा में समवर्ती कोड लिखते समय निम्नलिखित प्रिंसिपलों को अपनाते हैं, तो आप बड़े पैमाने पर तर्क से पहले होने वाले विकल्प का सहारा लेने से बच सकते हैं।

  • जहाँ संभव हो, अपरिवर्तनीय डेटा संरचनाओं का उपयोग करें। एक उचित रूप से कार्यान्वित अपरिवर्तनीय वर्ग थ्रेड-सुरक्षित होगा, और जब आप इसे अन्य वर्गों के साथ उपयोग करते हैं तो थ्रेड-सेफ्टी समस्याओं को प्रस्तुत नहीं करेंगे।

  • समझें और "असुरक्षित प्रकाशन" से बचें।

  • थ्रेड-सेफ 1 होने की जरूरत है जो उत्परिवर्तित वस्तुओं में राज्य तक पहुंच को सिंक्रनाइज़ करने के लिए आदिम म्यूटेक्स या Lock ऑब्जेक्ट का उपयोग करें।

  • सीधे थ्रेड प्रबंधन करने के प्रयास के बजाय Executor / ExecutorService या फोर्क ज्वाइन फ्रेमवर्क का उपयोग करें।

  • सीधे प्रतीक्षा / अधिसूचित / सूचना का उपयोग करने के बजाय, उन्नत ताले, अर्ध-कोष्ठक, लाचियां और अवरोध प्रदान करने वाले `java.util.concurrent वर्गों का उपयोग करें।

  • गैर-समवर्ती संग्रह के बाहरी सिंक्रोनाइज़ेशन के बजाय नक्शे, सेट, सूचियों, कतारों और देवताओं के java.util.concurrent संस्करणों का उपयोग करें।

सामान्य सिद्धांत यह है कि जावा के अंतर्निहित संगामिति पुस्तकालयों का उपयोग करने के बजाय "अपना खुद का रोल करने" का उपयोग करें। आप उन पर काम करने पर भरोसा कर सकते हैं, अगर आप उनका सही इस्तेमाल करते हैं।


1 - सभी वस्तुओं को धागा सुरक्षित रखने की आवश्यकता नहीं है। उदाहरण के लिए, यदि कोई वस्तु या वस्तु थ्रेड-सीमित है (अर्थात यह केवल एक थ्रेड तक पहुँच योग्य है), तो इसकी थ्रेड-सुरक्षा प्रासंगिक नहीं है।



Modified text is an extract of the original Stack Overflow Documentation
के तहत लाइसेंस प्राप्त है CC BY-SA 3.0
से संबद्ध नहीं है Stack Overflow