नमस्ते! हम मल्टीथ्रेडिंग का अपना अध्ययन जारी रखते हैं।
volatile
आज हम कीवर्ड और yield()
विधि के बारे में जानेंगे । चलो गोता लगाएँ :)
अस्थिर कीवर्ड
मल्टीथ्रेडेड एप्लिकेशन बनाते समय, हम दो गंभीर समस्याओं में भाग सकते हैं। सबसे पहले, जब एक मल्टीथ्रेडेड एप्लिकेशन चल रहा होता है, तो विभिन्न थ्रेड वेरिएबल्स के मूल्यों को कैश कर सकते हैं (हम पहले ही इस बारे में बात कर चुके हैं 'अस्थिर का उपयोग' शीर्षक वाले पाठ में )। आपके पास ऐसी स्थिति हो सकती है जहां एक धागा एक चर के मान को बदलता है, लेकिन दूसरा धागा परिवर्तन नहीं देखता है, क्योंकि यह चर की कैश की गई प्रति के साथ काम कर रहा है। स्वाभाविक रूप से, परिणाम गंभीर हो सकते हैं। मान लीजिए कि यह केवल कोई पुराना चर नहीं है, बल्कि आपके बैंक खाते की शेष राशि है, जो अचानक बेतरतीब ढंग से ऊपर और नीचे कूदना शुरू कर देती है :) यह मज़ेदार नहीं लगता है, है ना? दूसरा, जावा में, सभी आदिम प्रकारों को पढ़ने और लिखने के लिए संचालन,long
double
, परमाणु हैं। ठीक है, उदाहरण के लिए, यदि आप एक थ्रेड पर एक चर के मान को बदलते हैं int
, और दूसरे थ्रेड पर आप चर के मान को पढ़ते हैं, तो आपको या तो उसका पुराना मान मिलेगा या नया, यानी वह मान जो परिवर्तन के परिणामस्वरूप हुआ थ्रेड 1 में। कोई 'मध्यवर्ती मान' नहीं हैं। long
हालांकि, यह एस और double
एस के साथ काम नहीं करता है । क्यों? क्रॉस-प्लेटफ़ॉर्म समर्थन के कारण। शुरुआती स्तरों पर याद रखें कि हमने कहा था कि जावा का मार्गदर्शक सिद्धांत 'एक बार लिखो, कहीं भी भागो' है? इसका मतलब क्रॉस-प्लेटफॉर्म सपोर्ट है। दूसरे शब्दों में, जावा एप्लिकेशन सभी प्रकार के विभिन्न प्लेटफॉर्म पर चलता है। उदाहरण के लिए, Windows ऑपरेटिंग सिस्टम पर, Linux या MacOS के विभिन्न संस्करण। यह उन सभी पर बिना किसी रोक-टोक के चलेगा। 64 बिट्स में वजनी,long
double
जावा में 'सबसे भारी' आदिम हैं। और कुछ 32-बिट प्लेटफॉर्म केवल 64-बिट चर के परमाणु पढ़ने और लिखने को लागू नहीं करते हैं। ऐसे वेरिएबल्स को दो ऑपरेशन में पढ़ा और लिखा जाता है। सबसे पहले, पहले 32 बिट्स को वेरिएबल में लिखा जाता है, और फिर अन्य 32 बिट्स को लिखा जाता है। नतीजतन, एक समस्या उत्पन्न हो सकती है। एक थ्रेड एक X
चर के लिए कुछ 64-बिट मान लिखता है और ऐसा दो ऑपरेशनों में करता है। उसी समय, एक दूसरा थ्रेड वेरिएबल के मान को पढ़ने की कोशिश करता है और ऐसा उन दो ऑपरेशनों के बीच करता है - जब पहले 32 बिट्स लिखे गए हैं, लेकिन दूसरे 32 बिट्स नहीं लिखे गए हैं। नतीजतन, यह एक मध्यवर्ती, गलत मान पढ़ता है, और हमारे पास एक बग है। उदाहरण के लिए, यदि ऐसे प्लेटफॉर्म पर हम संख्या को 9223372036854775809 पर लिखने का प्रयास करते हैं एक चर के लिए, यह 64 बिट्स पर कब्जा कर लेगा। द्विआधारी रूप में, यह इस तरह दिखता है: 100000000000000000000000000000000000000000000000000000000000001 पहला धागा चर को संख्या लिखना शुरू करता है। सबसे पहले, यह पहले 32 बिट्स (1000000000000000000000000000000) लिखता है और फिर दूसरा 32 बिट्स (0000000000000000000000000000001) लिखता है और दूसरा धागा चर के मध्यवर्ती मान (10000000000000000000000000000000) को पढ़ते हुए इन परिचालनों के बीच फंस सकता है, जो पहले 32 बिट्स हैं जो पहले ही लिखे जा चुके हैं। दशमलव प्रणाली में, यह संख्या 2,147,483,648 है। दूसरे शब्दों में, हम सिर्फ एक चर के लिए संख्या 9223372036854775809 लिखना चाहते थे, लेकिन इस तथ्य के कारण कि यह ऑपरेशन कुछ प्लेटफार्मों पर परमाणु नहीं है, हमारे पास बुराई संख्या 2,147,483,648 है, जो कहीं से भी निकली और इसका अज्ञात प्रभाव होगा कार्यक्रम। दूसरे थ्रेड ने लिखे जाने से पहले केवल वेरिएबल के मान को पढ़ा, यानी थ्रेड ने पहले 32 बिट्स को देखा, लेकिन दूसरे 32 बिट्स को नहीं। बेशक, ये समस्याएं कल नहीं उठीं। जावा उन्हें एक ही कीवर्ड से हल करता है: volatile
. अगर हम इस्तेमाल करते हैंvolatile
हमारे कार्यक्रम में कुछ चर घोषित करते समय कीवर्ड…
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…यह मतलब है कि:
- यह हमेशा परमाणु रूप से पढ़ा और लिखा जाएगा। भले ही यह 64-बिट
double
याlong
. - जावा मशीन इसे कैश नहीं करेगी। तो आपके पास ऐसी स्थिति नहीं होगी जहां 10 धागे अपनी स्थानीय प्रतियों के साथ काम कर रहे हों।
उपज () विधि
हम पहले हीThread
कक्षा के कई तरीकों की समीक्षा कर चुके हैं, लेकिन एक महत्वपूर्ण तरीका है जो आपके लिए नया होगा। यह yield()
तरीका है । और यह वही करता है जो इसका नाम दर्शाता है! जब हम yield
किसी थ्रेड पर मेथड को कॉल करते हैं, तो यह वास्तव में अन्य थ्रेड्स से बात करता है: 'अरे दोस्तों। मुझे कहीं भी जाने की कोई विशेष जल्दी नहीं है, इसलिए यदि आप में से किसी के लिए प्रोसेसर समय प्राप्त करना महत्वपूर्ण है, तो इसे लें - मैं प्रतीक्षा कर सकता हूं'। यह कैसे काम करता है इसका एक सरल उदाहरण यहां दिया गया है:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
हम क्रमिक रूप से तीन सूत्र बनाते और शुरू करते हैं: Thread-0
, Thread-1
, और Thread-2
। Thread-0
पहले शुरू होता है और तुरंत दूसरों को देता है। फिर Thread-1
शुरू होता है और उपज भी देता है। फिर Thread-2
शुरू किया जाता है, जिसका फल भी मिलता है। हमारे पास कोई और धागा नहीं है, और Thread-2
अंतिम स्थान पर आने के बाद, धागा अनुसूचक कहता है, 'हम्म, कोई और नया धागा नहीं है। हमारे पास कतार में कौन है? इससे पहले किसने अपनी जगह दी Thread-2
? ऐसा लगता है कि यह था Thread-1
। ठीक है, इसका मतलब है कि हम इसे चलने देंगे'। Thread-1
अपना काम पूरा करता है और फिर थ्रेड शेड्यूलर अपना समन्वय जारी रखता है: 'ठीक है, Thread-1
समाप्त। क्या हमारे पास कतार में कोई और है?'। थ्रेड-0 कतार में है: इसने ठीक पहले अपना स्थान प्राप्त कियाThread-1
. अब इसकी बारी आती है और पूर्णता की ओर दौड़ती है। फिर शेड्यूलर थ्रेड्स का समन्वय करना समाप्त करता है: 'ठीक है, Thread-2
आप अन्य थ्रेड्स के लिए तैयार हो गए हैं, और वे सब अब हो गए हैं। तुम सबसे आखिर में झुके थे, अब तुम्हारी बारी है।' फिर Thread-2
पूरा करने के लिए दौड़ता है। कंसोल आउटपुट इस तरह दिखेगा: थ्रेड-0 दूसरों को अपनी जगह देता है थ्रेड-1 दूसरों को अपनी जगह देता है थ्रेड-2 दूसरों को अपनी जगह देता है थ्रेड-1 ने निष्पादन समाप्त कर दिया है। थ्रेड-0 का निष्पादन समाप्त हो गया है। थ्रेड-2 का निष्पादन समाप्त हो गया है। बेशक, थ्रेड शेड्यूलर थ्रेड्स को एक अलग क्रम में शुरू कर सकता है (उदाहरण के लिए, 0-1-2 के बजाय 2-1-0), लेकिन सिद्धांत समान रहता है।
होता है-नियमों से पहले
आज हम जिस आखिरी चीज को छूएंगे वह है ' पहले होता है ' की अवधारणा। जैसा कि आप पहले से ही जानते हैं, जावा में थ्रेड शेड्यूलर अपने कार्यों को करने के लिए थ्रेड्स को समय और संसाधन आवंटित करने में शामिल अधिकांश कार्य करता है। आपने यह भी बार-बार देखा है कि कैसे थ्रेड्स को एक यादृच्छिक क्रम में निष्पादित किया जाता है जो आमतौर पर भविष्यवाणी करना असंभव है। और सामान्य तौर पर, 'अनुक्रमिक' प्रोग्रामिंग के बाद हमने पहले किया था, बहुप्रचारित प्रोग्रामिंग कुछ यादृच्छिक दिखती है। आपको पहले ही विश्वास हो गया है कि आप बहुप्रचारित प्रोग्राम के प्रवाह को नियंत्रित करने के लिए कई विधियों का उपयोग कर सकते हैं। लेकिन जावा में मल्टीथ्रेडिंग का एक और स्तंभ है - 4 ' होता है-पहले ' नियम। इन नियमों को समझना काफी सरल है। कल्पना कीजिए कि हमारे पास दो सूत्र हैं -A
औरB
. इनमें से प्रत्येक थ्रेड ऑपरेशन कर सकता है 1
और 2
. प्रत्येक नियम में, जब हम कहते हैं कि ' A होता है-B से पहले ', तो हमारा मतलब है कि A
ऑपरेशन से पहले थ्रेड द्वारा किए गए सभी परिवर्तन 1
और इस ऑपरेशन के परिणामस्वरूप होने वाले परिवर्तन थ्रेड को तब दिखाई देते हैं B
जब ऑपरेशन 2
किया जाता है और उसके बाद। प्रत्येक नियम गारंटी देता है कि जब आप एक मल्टीथ्रेडेड प्रोग्राम लिखते हैं, तो कुछ घटनाएं 100% दूसरों से पहले घटित होंगी, और यह कि ऑपरेशन के समय 2
थ्रेड हमेशा ऑपरेशन के दौरान किए गए B
परिवर्तनों से अवगत रहेगा । आइए उनकी समीक्षा करें। A
1
नियम 1।
एक ही मॉनिटर को दूसरे थ्रेड द्वारा अधिग्रहित किए जाने से पहले एक म्यूटेक्स जारी किया जाता है। मुझे लगता है कि आप यहां सब कुछ समझते हैं। यदि किसी वस्तु या वर्ग का म्यूटेक्स एक थ्रेड द्वारा अधिग्रहित किया जाता है, उदाहरण के लिए, थ्रेड द्वारा , एक ही समय मेंA
एक और थ्रेड (थ्रेड ) इसे प्राप्त नहीं कर सकता है। B
म्यूटेक्स जारी होने तक इसे इंतजार करना होगा।
नियम 2।
विधि पहले होती हैThread.start()
। फिर, यहाँ कुछ भी मुश्किल नहीं है। आप पहले से ही जानते हैं कि विधि के अंदर कोड चलाना शुरू करने के लिए , आपको थ्रेड पर विधि को कॉल करना होगा। विशेष रूप से, प्रारंभ विधि, स्वयं विधि नहीं! यह नियम सुनिश्चित करता है कि कॉल करने से पहले सेट किए गए सभी वेरिएबल्स के मान एक बार शुरू होने के बाद विधि के अंदर दिखाई देंगे । Thread.run()
run()
start()
run()
Thread.start()
run()
नियम 3।
run()
विधि का अंत विधि से वापसी से पहले होता है । join()
आइए अपने दो धागों पर लौटते हैं: A
और B
। हम join()
विधि को कॉल करते हैं ताकि थ्रेड को अपना काम करने से पहले B
थ्रेड के पूरा होने की प्रतीक्षा करने की गारंटी हो । A
इसका मतलब यह है कि ए ऑब्जेक्ट की run()
विधि बहुत अंत तक चलने की गारंटी है। run()
और थ्रेड की विधि में होने वाले डेटा में सभी परिवर्तन A
एक सौ प्रतिशत गारंटीकृत होते हैं कि थ्रेड में B
एक बार दिखाई देने के बाद थ्रेड A
अपना काम पूरा कर लेता है ताकि वह अपना काम शुरू कर सके।
नियम 4।
volatile
एक चर को लिखना उसी चर से पढ़ने से पहले होता है । जब हम volatile
कीवर्ड का उपयोग करते हैं, तो हम वास्तव में हमेशा वर्तमान मान प्राप्त करते हैं। यहां तक कि एक long
या के साथ double
(हमने पहले उन समस्याओं के बारे में बात की थी जो यहां हो सकती हैं)। जैसा कि आप पहले से ही समझते हैं, कुछ थ्रेड्स में किए गए परिवर्तन हमेशा अन्य थ्रेड्स को दिखाई नहीं देते हैं। लेकिन, निश्चित रूप से, बहुत बार ऐसी स्थितियां होती हैं जहां ऐसा व्यवहार हमें शोभा नहीं देता। मान लीजिए कि हम थ्रेड पर एक चर के लिए एक मान निर्दिष्ट करते हैं A
:
int z;
….
z = 555;
यदि हमारे B
थ्रेड को कंसोल पर वेरिएबल का मान प्रदर्शित करना चाहिए z
, तो यह आसानी से 0 प्रदर्शित कर सकता है, क्योंकि यह असाइन किए गए मान के बारे में नहीं जानता है। लेकिन नियम 4 गारंटी देता है कि यदि हम z
वेरिएबल को के रूप में घोषित करते हैं volatile
, तो एक थ्रेड पर उसके मान में परिवर्तन हमेशा दूसरे थ्रेड पर दिखाई देगा। volatile
यदि हम पिछले कोड में शब्द जोड़ते हैं ...
volatile int z;
….
z = 555;
...फिर हम उस स्थिति को रोकते हैं जहां थ्रेड B
0 प्रदर्शित कर सकता है। volatile
वेरिएबल्स को लिखना उनसे पढ़ने से पहले होता है।
GO TO FULL VERSION