जावा मेमरी मॉडेलचा परिचय

Java मेमरी मॉडेल (JMM) Java रनटाइम वातावरणातील थ्रेड्सच्या वर्तनाचे वर्णन करते. मेमरी मॉडेल हे जावा भाषेच्या शब्दार्थाचा भाग आहे आणि विशिष्ट Java मशीनसाठी नव्हे तर संपूर्ण Java साठी सॉफ्टवेअर विकसित करताना प्रोग्रामर काय अपेक्षा करू शकतो आणि काय करू नये याचे वर्णन करतो.

1995 मध्ये विकसित केलेले मूळ जावा मेमरी मॉडेल (जे विशेषतः "पर्कोलोकल मेमरी" चा संदर्भ देते), ते अयशस्वी मानले जाते: कोड सुरक्षिततेची हमी न गमावता अनेक ऑप्टिमायझेशन केले जाऊ शकत नाहीत. विशेषतः, मल्टी-थ्रेडेड "सिंगल" लिहिण्यासाठी अनेक पर्याय आहेत:

  • एकतर सिंगलटन ऍक्सेस करण्याच्या प्रत्येक कृतीमुळे (जरी वस्तू फार पूर्वी तयार केली गेली होती, आणि काहीही बदलू शकत नाही) इंटर-थ्रेड लॉक होऊ शकते;
  • किंवा विशिष्ट परिस्थितीत, सिस्टम एक अपूर्ण एकटा जारी करेल;
  • किंवा विशिष्ट परिस्थितीत, सिस्टम दोन एकटे तयार करेल;
  • किंवा डिझाइन विशिष्ट मशीनच्या वर्तनावर अवलंबून असेल.

म्हणून, मेमरी यंत्रणा पुन्हा डिझाइन केली गेली आहे. 2005 मध्ये, Java 5 च्या रिलीझसह, एक नवीन दृष्टीकोन सादर केला गेला, जो Java 14 च्या रिलीझसह आणखी सुधारला गेला.

नवीन मॉडेल तीन नियमांवर आधारित आहे:

नियम #1 : सिंगल-थ्रेड केलेले प्रोग्राम स्यूडो-अनुक्रमाने चालतात. याचा अर्थ: प्रत्यक्षात, प्रोसेसर प्रत्येक घड्याळात अनेक ऑपरेशन्स करू शकतो, त्याच वेळी त्यांचा क्रम बदलतो, तथापि, सर्व डेटा अवलंबित्व राहतात, म्हणून वर्तन अनुक्रमिकांपेक्षा भिन्न नसते.

नियम क्रमांक 2 : कुठेही बाहेरची मूल्ये नाहीत. कोणतेही व्हेरिएबल वाचणे (नॉन-व्होलॅटाइल लाँग आणि डबल वगळता, ज्यासाठी हा नियम असू शकत नाही) एकतर डीफॉल्ट मूल्य (शून्य) किंवा दुसर्‍या कमांडद्वारे लिहिलेले काहीतरी परत करेल.

आणि नियम क्रमांक 3 : उर्वरित घटना क्रमाने अंमलात आणल्या जातात, जर ते कठोर आंशिक ऑर्डर संबंधाने जोडलेले असतील तर "पूर्वी कार्यान्वित होते" ( आधी घडते ).

आधी घडते

लेस्ली लॅम्पपोर्ट यांनी यापूर्वी हॅपेन्स ही संकल्पना मांडली होती . हा अणु आदेश (++ आणि -- अणू नसलेल्या) दरम्यान सादर केलेला एक कठोर आंशिक क्रम संबंध आहे आणि याचा अर्थ "शारीरिकदृष्ट्या आधी" नाही.

त्यात म्हटले आहे की दुसऱ्या संघाला पहिल्याने केलेल्या बदलांची "माहिती" असेल.

आधी घडते

उदाहरणार्थ, अशा ऑपरेशन्ससाठी एक दुसऱ्याच्या आधी अंमलात आणला जातो:

सिंक्रोनाइझेशन आणि मॉनिटर्स:

  • मॉनिटर कॅप्चर करणे ( लॉक पद्धत , सिंक्रोनाइझ केलेले प्रारंभ) आणि त्यानंतर त्याच थ्रेडवर जे काही होते ते.
  • मॉनिटरचे परत येणे (पद्धत अनलॉक , सिंक्रोनाइझचा शेवट) आणि त्याच्या आधी समान थ्रेडवर जे काही घडते.
  • मॉनिटर परत करणे आणि नंतर ते दुसर्या थ्रेडद्वारे कॅप्चर करणे.

लेखन आणि वाचन:

  • कोणत्याही व्हेरिएबलवर लिहिणे आणि नंतर त्याच प्रवाहात ते वाचणे.
  • volatile variable वर लिहिण्याआधी सर्व काही एकाच धाग्यात आणि स्वतःच लेखन. अस्थिर वाचन आणि त्या नंतर त्याच धाग्यावर सर्व काही.
  • अस्थिर व्हेरिएबलवर लिहिणे आणि नंतर ते पुन्हा वाचणे. अस्थिर लेखन मॉनिटर रिटर्न प्रमाणेच मेमरीशी संवाद साधते, तर वाचन हे कॅप्चरसारखे असते. असे दिसून आले की जर एका थ्रेडने अस्थिर व्हेरिएबलला लिहिले आणि दुसर्‍याला ते सापडले, तर लेखनाच्या आधीची प्रत्येक गोष्ट वाचल्यानंतर येणार्‍या प्रत्येक गोष्टीपूर्वी कार्यान्वित केली जाते; चित्र पहा.

वस्तूंची देखभाल:

  • स्टॅटिक इनिशिएलायझेशन आणि ऑब्जेक्ट्सच्या कोणत्याही उदाहरणांसह कोणतीही क्रिया.
  • कन्स्ट्रक्टरमधील अंतिम फील्ड आणि कन्स्ट्रक्टर नंतर सर्वकाही लिहिणे. अपवाद म्‍हणून, घडते-पूर्वीचे संबंध इतर नियमांशी संक्रामकपणे जोडत नाहीत आणि त्यामुळे आंतर-थ्रेड शर्यत होऊ शकते.
  • ऑब्जेक्टसह कोणतेही कार्य करा आणि अंतिम करा() .

प्रवाह सेवा:

  • थ्रेड आणि थ्रेडमधील कोणताही कोड सुरू करत आहे.
  • थ्रेड आणि थ्रेडमधील कोणत्याही कोडशी संबंधित व्हेरिएबल्स शून्य करणे.
  • थ्रेडमधील कोड आणि join() ; थ्रेडमधील कोड आणि isAlive() == false .
  • interrupt() थ्रेड आणि तो थांबला असल्याचे आढळले.

कामाच्या बारकावे आधी घडते

एकच मॉनिटर मिळवण्यापूर्वी घडते-आधी मॉनिटर रिलीझ करणे. हे लक्षात घेण्यासारखे आहे की ते रिलीझ आहे, बाहेर पडणे नाही, म्हणजेच प्रतीक्षा वापरताना तुम्हाला सुरक्षिततेबद्दल काळजी करण्याची गरज नाही.

हे ज्ञान आपल्याला आपले उदाहरण सुधारण्यास कशी मदत करेल ते पाहू या. या प्रकरणात, सर्वकाही अगदी सोपे आहे: फक्त बाह्य चेक काढा आणि सिंक्रोनाइझेशन जसे आहे तसे सोडा. आता दुसरा थ्रेड सर्व बदल पाहण्याची हमी देतो, कारण इतर थ्रेडने तो रिलीज केल्यानंतरच त्याला मॉनिटर मिळेल. आणि सर्वकाही सुरू होईपर्यंत तो ते सोडणार नाही, आम्ही सर्व बदल एकाच वेळी पाहू, स्वतंत्रपणे नाही:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

अस्थिर व्हेरिएबलवर लिहिणे घडते - त्याच व्हेरिएबलमधून वाचण्यापूर्वी. आम्ही केलेला बदल, अर्थातच, बगचे निराकरण करतो, परंतु ज्याने मूळ कोड लिहिला आहे तेथून परत ठेवतो - प्रत्येक वेळी अवरोधित करणे. अस्थिर कीवर्ड वाचवू शकतो. खरं तर, प्रश्नातील विधानाचा अर्थ असा आहे की अस्थिर घोषित केलेली प्रत्येक गोष्ट वाचताना, आपल्याला नेहमीच वास्तविक मूल्य मिळेल.

याव्यतिरिक्त, मी आधी म्हटल्याप्रमाणे, अस्थिर फील्डसाठी, लेखन नेहमीच (लांब आणि दुहेरीसह) एक अणू ऑपरेशन असते. आणखी एक महत्त्वाचा मुद्दा: जर तुमच्याकडे अस्थिर अस्तित्व असेल ज्यामध्ये इतर घटकांचा संदर्भ असेल (उदाहरणार्थ, अॅरे, सूची किंवा इतर काही वर्ग), तर केवळ घटकाचाच संदर्भ नेहमीच "ताजे" असेल, परंतु प्रत्येक गोष्टीसाठी नाही. तो येणार आहे.

तर, आमच्या डबल-लॉकिंग रॅमकडे परत या. अस्थिर वापरून, आपण याप्रमाणे परिस्थितीचे निराकरण करू शकता:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

येथे आपल्याकडे अद्याप लॉक आहे, परंतु डेटा == शून्य असल्यासच. आम्ही अस्थिर वाचन वापरून उर्वरित प्रकरणे फिल्टर करतो. वॉलेटाइल स्टोअर घडते या वस्तुस्थितीद्वारे अचूकता सुनिश्चित केली जाते - अस्थिर वाचनापूर्वी, आणि कन्स्ट्रक्टरमध्ये होणारी सर्व ऑपरेशन्स फील्डचे मूल्य वाचणाऱ्याला दृश्यमान असतात.