प्रस्तावना
2022 वर्ष के 09 महीने की 22 तारीख को JDK19 जारी हुआ। इस संस्करण की सबसे बड़ी विशेषता वर्चुअल थ्रेड का समर्थन है, जिससे हल्के थ्रेड के परिवार में एक और दमदार सदस्य जुड़ गया। वर्चुअल थ्रेड JVM को ऑपरेटिंग सिस्टम के माध्यम से थ्रेड शेड्यूल करने की बाध्यता से मुक्त करते हैं और JVM स्वयं थ्रेड शेड्यूल करता है। वास्तव में, शुरुआती दौर में Sun ने Solaris ऑपरेटिंग सिस्टम की वर्चुअल मशीन में JVM द्वारा थ्रेड शेड्यूलिंग लागू की थी। उसकी जटिलता और रखरखाव संबंधी विचारों के कारण अंततः फिर ऑपरेटिंग सिस्टम द्वारा थ्रेड शेड्यूल करने के मॉडल पर लौटना पड़ा।
“长安归来锦衣客,昨日城南起新宅。” — चांगआन से रेशमी वस्त्रों में लौटा यात्री, कल नगर के दक्षिण में उसका नया घर बना। इस पूरी यात्रा पर पीछे मुड़कर देखें तो मल्टीथ्रेडिंग की अवधारणाएँ आँखें चकरा देती हैं। इंटरनेट पर इनके अनगिनत विवरण भी हैं, फिर भी हमेशा एक समग्र दृष्टिकोण की कमी महसूस होती है। इसलिए मैंने Java में मल्टीथ्रेडिंग के विकास का इतिहास व्यवस्थित रूप से संकलित किया है। आशा है कि इससे आपको मल्टीथ्रेडिंग का ज्ञान हासिल करने में मदद मिलेगी।
इस लेख में किन बातों की चर्चा नहीं की गई है:
1 कुछ तकनीकी बिंदुओं के विस्तृत कार्यान्वयन सिद्धांत नहीं समझाए जाएँगे, स्रोत कोड का विश्लेषण नहीं होगा और चित्र नहीं बनाए जाएँगे, यदि इस लेख में कोई अवधारणा या तकनीक आपकी रुचि जगाए तो आप स्वयं खोज सकते हैं 2 समवर्तिता का समर्थन करने वाली लाइब्रेरी और फ़्रेमवर्क, जैसे Quasar、Akka、Guava आदि, पर चर्चा नहीं होगी
यह लेख किस बारे में है
1 JDK मल्टीथ्रेडिंग के विकास का इतिहास समझाना 2 विकास के दौरान कुछ तकनीकी पहलुओं के कार्य सिद्धांत, पृष्ठभूमि और उनके द्वारा सुलझाई गई समस्याएँ समझाना 3 कुछ तकनीकी पहलुओं पर लेखक के विचार प्रस्तुत करना; अलग विचार रखने वाले पाठकों का टिप्पणी अनुभाग में चर्चा करने के लिए स्वागत है
मील का पत्थर
हमेशा की तरह, पहले एक सांख्यिकीय तालिका प्रस्तुत है। इसमें JDK के विभिन्न संस्करणों में थ्रेड से जुड़ी मुख्य अवधारणाओं को व्यवस्थित किया गया है। यहाँ एक शायद पूरी तरह उपयुक्त न होने वाली उपमा दी जा सकती है:मल्टीथ्रेडिंग के विकास को कारों के विकास से जोड़ा जा सकता है। मल्टीथ्रेडिंग क्रमशः मैनुअल गियर के युग(JDK1.4 और उससे पहले), ऑटोमैटिक गियर के युग(JDK5-JDK18), और स्वचालित ड्राइविंग के युग(JDK19 और उसके बाद) से गुज़री है। इस उपमा का उद्देश्य केवल पाठकों को यह बताना है कि JDK5 के बाद मल्टीथ्रेडिंग को अधिक सहज ढंग से नियंत्रित किया जा सकता है। JDK19 के बाद बात केवल सहजता तक सीमित नहीं रही, इसने IO-प्रधान सेवाओं के प्रदर्शन में गुणात्मक छलाँग ला दी।
| युग | संस्करण | प्रकाशन समय | मुख्य अवधारणाएँ |
|---|---|---|---|
| मैनुअल मोड | JDK1.0 | 1996-01-23 | Thread और Runnable |
| मैनुअल मोड | JDK1.2 | 1998-12-04 | ThreadLocal、Collections |
| स्वचालित गियर | JDK1.5/5.0 | 2004-09-30 | Java मेमोरी मॉडल को स्पष्ट किया और समवर्ती प्रोग्रामिंग पैकेज जोड़ा |
| स्वचालित गियर | JDK1.6/6.0 | 2006-12-11 | synchronized का अनुकूलन |
| स्वचालित गियर | JDK1.7/7.0 | 2011-07-28 | Fork/Join फ़्रेमवर्क |
| स्वचालित गियर | JDK1.8/8.0 | 2014-03-18 | CompletableFuture、Stream |
| स्वचालित गियर | JDK1.9/9.0 | 2014-09-08 | लॉक के लिए प्रतिस्पर्धा की व्यवस्था में सुधार |
| स्वचालित गियर | JDK10 | 2018-03-21 | थ्रेड स्थानीय नियंत्रण |
| स्वचालित गियर | JDK15 | 2020-09-15 | बायस्ड लॉकिंग को अक्षम और अप्रचलित करना |
| स्वायत्त वाहन संचालन | JDK19 | 2022-09-22 | वर्चुअल थ्रेड |

मैनुअल गियर का युग
JDK1.4 और उससे पुराने संस्करणों को लेखक मल्टीथ्रेडिंग का “मैनुअल गियर” युग कहते हैं, जिसे मूल मल्टीथ्रेडिंग युग भी कहा जाता है। थ्रेड संचालन अभी अपेक्षाकृत मूल स्तर पर था और थ्रेड पूल उपलब्ध नहीं थे। बार-बार थ्रेड बनाने से संसाधनों की बर्बादी रोकने के लिए डेवलपरों को स्वयं उपकरण लिखने पड़ते थे और साझा संसाधनों पर स्वयं लॉक लगाना पड़ता था। इसी युग में कई उत्कृष्ट मल्टीथ्रेडिंग फ़्रेमवर्क भी विकसित हुए, जिनमें सबसे प्रसिद्ध को JDK5.0 ने अपनाया।
JDK 1.0 Thread और Runnable
1996 वर्ष के 1वें महीने में आए JDK1.0 संस्करण ने शुरुआत से ही Java के सबसे बुनियादी थ्रेड मॉडल की नींव रख दी थी। बाद के सुधारों और पैबंदों के दौरान इस थ्रेड मॉडल में कोई मूलभूत बदलाव नहीं हुआ;इसे ऐसा अच्छा डिज़ाइन कहा जा सकता है जिसकी परंपरा आगे चली। प्रीएम्प्टिव और सहकारी, प्रक्रिया/थ्रेड शेड्यूलिंग के दो आम तरीके हैं। ऑपरेटिंग सिस्टम के लिए अपनी प्रक्रियाओं को प्रीएम्प्टिव तरीके से शेड्यूल करना बहुत उपयुक्त है;वह विभिन्न प्रक्रियाओं को समय खंड आवंटित करता है और लंबे समय तक प्रतिक्रिया न देने वाली प्रक्रिया से संसाधन छीन सकता है, यहाँ तक कि उसे बलपूर्वक रोक भी सकता है। सहकारी तरीके में प्रक्रियाओं को स्वयं आगे बढ़कर संसाधन छोड़ने होते हैं। इस शेड्यूलिंग में, बहुत देर तक चलने वाला एक थ्रेड CPU चाहने वाले बाकी सभी थ्रेड को “भूखा” रख सकता है। Java hotspot वर्चुअल मशीन प्रीएम्प्टिव शेड्यूलिंग अपनाती है, इसलिए Java भाषा ने शुरुआत से ही प्रीएम्प्टिव थ्रेड शेड्यूलिंग का उपयोग किया। JDK 1.0 में थ्रेड बनाने के मुख्य तरीके Thread क्लास को विस्तारित करना या Runnable इंटरफ़ेस लागू करना हैं। ऑब्जेक्ट इंस्टेंस के start मेथड से थ्रेड शुरू होता है, समानांतर चलने वाला कोड run मेथड में रखा जाता है, और थ्रेडों के बीच समन्वय तथा संचार के लिए stop/resume/suspend जैसे सीधे लेकिन कठोर तरीके उपयोग किए जाते हैं।

stop/resume/suspend की अवधारणाएँ कैसे समझाएँ?इसका अर्थ है कि मुख्य थ्रेड सीधे उपथ्रेड के समाप्त करने, रोकने और फिर जारी रखने वाले मेथड बुला सकता है। यदि आपने बचपन में पोर्टेबल कैसेट प्लेयर इस्तेमाल किया हो, तो उस पर तीन बटन होते थे, बंद करना, रोकना, फिर चलाना। कल्पना कीजिए कि आप एक साथ 3 पोर्टेबल कैसेट प्लेयर सुन रहे हैं, ये तीन प्लेयर तीन उपथ्रेड हैं, और आप मुख्य थ्रेड हैं, आप अपनी इच्छानुसार इन तीन उपकरणों को शुरू और बंद कर सकते हैं।
इस पूरी व्यवस्था में एक घातक समस्या है: इसमें आसानी से डेडलॉक हो सकता है। कारण यह है कि जब थ्रेड A किसी संसाधन को लॉक करता है और उसे छोड़ने से पहले मुख्य थ्रेड उसे रोक देता है (suspend विधि लॉक नहीं छोड़ती), तब यदि थ्रेड B उस संसाधन पर अधिकार चाहता है, तो उसे थ्रेड A के फिर चलने (resume) और संसाधन छोड़ने की प्रतीक्षा करनी पड़ती है। अन्यथा उसे वह संसाधन कभी नहीं मिलेगा और डेडलॉक हो जाएगा।
JDK 1.2
इस संस्करण में कठोर stop/resume/suspend तंत्र का उपयोग निषिद्ध कर दिया गया और उसकी जगह wait/notify/sleep जैसे तरीकों से कई थ्रेडों के मिलकर काम करने की व्यवस्था अपनाई गई। उल्लेखनीय है कि इस संस्करण में परमाण्विक ऑब्जेक्ट AtomicityXXX पहले ही डिज़ाइन किए जा चुके थे, जिनका मुख्य उद्देश्य i++ के गैर-परमाण्विक होने की समस्या हल करना था। ThreadLocal और Collections के जुड़ने से मल्टीथ्रेडिंग के उपयोग के तरीके बढ़ गए। इन दो तकनीकों के कारण लेखक इसे Java का टर्बोचार्जिंग युग कहते हैं।
ThreadLocal
ThreadLocal एक ऐसी व्यवस्था है जो बिना लॉक के कई थ्रेडों को थ्रेड-सुरक्षित न होने वाले ऑब्जेक्ट साझा करने देती है。यह “बैंक खाते या भंडार में वृद्धि और कटौती” जैसी समस्याएँ हल नहीं कर सकती,यह “उपकरण” के रूप में काम करने वाली क्लासों की प्रतियाँ बनाकर उनके “उपकरण” तरीकों को सुरक्षित ढंग से चलाने में उपयोगी है。इसके सामान्य उदाहरण SimpleDateFormat,डेटाबेस कनेक्शन आदि हैं。इसका डिज़ाइन बहुत चतुर है,कल्पना करें कि यदि आपको इसे डिज़ाइन करना हो,तो सामान्य सरल विचार होगा:ThreadLocal में एक वैश्विक थ्रेड-सुरक्षित Map रखा जाए,जिसकी key थ्रेड हो और value साझा ऑब्जेक्ट。इस डिज़ाइन की कमी मेमोरी लीक है,क्योंकि अधिक से अधिक थ्रेड जुड़ने पर यह Map अनंत रूप से बढ़ता जाएगा,और मेमोरी लीक हल करने के लिए थ्रेड समाप्त होने पर इस Map को साफ़ करना पड़ेगा,जिसके लिए GC की क्षमता और बढ़ानी होगी,स्पष्ट है कि लागत के अनुपात में लाभ उचित नहीं है。इसलिए,ThreadLocal को इस तरह डिज़ाइन किया गया कि Map को ThreadLocal के बजाय स्वयं Thread रखे。इसमें key ThreadLocal वेरिएबल और value उसका मान होता है。प्रत्येक Thread उपयोग किए जाने वाले ThreadLoacl को इसमें रखता है(निश्चित ही इस डिज़ाइन से उत्पन्न अन्य समस्याएँ भी हैं,जिनकी यहाँ चर्चा नहीं की गई है,रुचि रखने वाले पाठक स्वयं खोज सकते हैं)。
Collections
Collections यूटिलिटी क्लास इसी संस्करण में डिज़ाइन की गई थी। यह SynchronizedList जैसे कुछ थ्रेड सुरक्षित संग्रहों को रैप करती है। उस दौर में, जब थ्रेड सुरक्षित संग्रहों के नाम पर केवल Hashtable, Vector, Stack आदि थे, इसका आना भी युगांतकारी था। Collections यूटिलिटी का मूल विचार है:मैं तुम्हारे थ्रेड असुरक्षित संग्रह को थ्रेड सुरक्षित बनाकर रैप कर दूँगा, ताकि तुम्हें मौजूदा कोड को उन्नत और संशोधित करने में बहुत समय न लगाना पड़े;संग्रह बनाते समय बस मेरे दिए तरीके से उसे इनिशियलाइज़ कर दो। यह कुछ हद तक कारों की टर्बोचार्जिंग तकनीक जैसा है, जो इंजन का विस्थापन बदले बिना उसकी शक्ति और टॉर्क बढ़ाती है। Java का टर्बोचार्जिंग युग आ गया है^\_^
स्वचालित गियर का युग
JDK 5.0
समवर्ती प्रोग्रामिंग पैकेज जोड़ें
Doug Lea, जिनका चीनी नाम 道格·利 है, अमेरिका में एक विश्वविद्यालय के अध्यापक हैं और अपने क्षेत्र के दिग्गज हैं। J.U.C उन्हीं की देन है। JDK1.5 से पहले, प्रोग्राम में सिंक्रोनाइज़ किए गए कोड तक समवर्ती पहुँच को नियंत्रित करने के लिए हम केवल synchronized का उपयोग कर सकते थे। उस समय synchronized का प्रदर्शन अभी ठीक से अनुकूलित नहीं हुआ था और अच्छा नहीं था। थ्रेड को नियंत्रित करने के लिए भी केवल Object की wait और notify विधियाँ ही उपलब्ध थीं। इसी समय Doug Lea ने JCP को JSR-166 का प्रस्ताव दिया। JSR-166 प्रस्तुत करने से पहले ही Doug Lea तीन साल से अधिक समय से J.U.C पैकेज जैसी कार्यक्षमता वाले कोड का उपयोग कर रहे थे। यही कोड J.U.C का प्रारूप बना।
J.U.C ने परमाण्विक ऑब्जेक्ट、लॉक और उपयोगिता उपकरणों के समूह、थ्रेड पूल、थ्रेड-सुरक्षित कंटेनर जैसी कई श्रेणियों के उपकरण प्रदान किए।डेवलपर अपनी ज़रूरत के अनुसार इन क्षमताओं का लचीले ढंग से उपयोग करके अपने उत्पाद बना सकते हैं,चाहें तो ReentrantLock से निचले स्तर का फ़्रेमवर्क बनाएँ,या सीधे तैयार उपकरणों अथवा कंटेनरों का उपयोग करके व्यावसायिक कोड लिखें।ऐतिहासिक दृष्टि से देखें,तो 2004 में J.U.C को निर्विवाद रूप से “अत्याधुनिक तकनीकी उत्पाद” कहा जा सकता था।इसने Java के प्रसार में अत्यंत महत्वपूर्ण योगदान दिया।Java का ऑटोमैटिक गियर वाला युग आ गया था,जिस तरह ऑटोमैटिक गियर वाली कार चलाना आसान बनाती है,उसी तरह J.U.C ने प्रोग्रामरों के लिए मल्टीथ्रेडिंग का उपयोग बहुत आसान बना दिया।यह ऐसा उत्पाद था जिसने एक नए युग की शुरुआत की।
बेशक J.U.C में भी कुछ कमियाँ हैं:
CPU पर अधिक भार:यदि स्पिन करते हुए CAS लंबे समय तक सफल न हो, तो CPU पर बहुत अधिक अतिरिक्त भार पड़ता है।
समाधान:JUC में कुछ स्थानों पर CAS स्पिन की संख्या सीमित की गई है,उदाहरण के लिए BlockingQueue का SynchronousQueue。
ABA समस्या:यदि कोई मान पहले A था,फिर B हो गया,और फिर वापस A हो गया,तो CAS जाँच में कोई बदलाव दिखाई नहीं देगा,लेकिन वास्तव में वह बदल चुका है,यही ABA समस्या है।अधिकांश मामलों में ABA समस्या प्रोग्राम के समवर्ती निष्पादन की शुद्धता को प्रभावित नहीं करती।
समाधान:हर चर में एक संस्करण संख्या जोड़ें और हर बदलाव पर उसमें 1 जोड़ें, अर्थात A —> B —> A बदलकर 1A —> 2B —> 3A हो जाता है।Java इसे हल करने के लिए AtomicStampedReference प्रदान करता है।AtomicStampedReference [E,Integer] के टपल को आवरण में रखकर ऑब्जेक्ट पर संस्करण चिह्न(stamp)लगाता है, जिससे ABA समस्या से बचा जाता है।
केवल एक साझा चर पर परमाण्विक ऑपरेशन की गारंटी दे सकता है:CAS तंत्र केवल एक चर पर होने वाली संक्रिया की परमाण्विकता सुनिश्चित करता है, पूरे कोड ब्लॉक की परमाण्विकता सुनिश्चित नहीं कर सकता।
समाधान:उदाहरण के लिए, यदि 3 चरों को एक साथ परमाण्विक ढंग से अद्यतन करना सुनिश्चित करना हो, तो Synchronized का उपयोग करना ही पड़ेगा। कई चरों को लपेटने के लिए AtomicReference का उपयोग करने पर भी विचार किया जा सकता है, जिससे कई साझा चरों वाली स्थिति को संभाला जा सके।
Java मेमोरी मॉडल स्पष्ट करें
JDK के इस संस्करण ने Java मेमोरी मॉडल को नए सिरे से स्पष्ट किया। इससे पहले प्रचलित मेमोरी मॉडलों में क्रमिक संगतता मेमोरी मॉडल और हैपन्स-बिफोर मॉडल शामिल थे। क्रमिक संगतता मॉडल में प्रोग्राम के निष्पादन का क्रम कोड में दिखाई देने वाले क्रम से पूरी तरह मेल खाता है। आधुनिक बहु-कोर CPU के लिए, जो निर्देशों के निष्पादन को अनुकूलित करते हैं, इसकी गारंटी देना बहुत कठिन है। इसके अलावा, क्रमिक संगतता की गारंटी JVM द्वारा रनटाइम पर किए जाने वाले कोड अनुकूलन को बहुत सीमित कर देती है।
लेकिन इस संस्करण के JSR 133 विनिर्देश में निर्धारित पहले घटित होने का संबंध (Happens-before) निर्देशों के निष्पादन क्रम को लचीला बनाता है:
एक ही थ्रेड में, कोड के निष्पादन क्रम(अर्थात कोड के अर्थगत क्रम)के अनुसार, पहले वाला ऑपरेशन बाद वाले ऑपरेशन से पहले घटित होता है किसी monitor ऑब्जेक्ट को अनलॉक करने का ऑपरेशन उसी monitor ऑब्जेक्ट को बाद में लॉक करने के ऑपरेशन से पहले घटित होता है volatile फ़ील्ड में लिखने का ऑपरेशन उस फ़ील्ड को बाद में पढ़ने के ऑपरेशन से पहले घटित होता है किसी थ्रेड का start ऑपरेशन(थ्रेड ऑब्जेक्ट की start() विधि कॉल करना)उस थ्रेड के अन्य सभी ऑपरेशनों से पहले घटित होता है एक थ्रेड के सभी ऑपरेशन किसी भी अन्य थ्रेड द्वारा इस थ्रेड पर join() विधि कॉल करने से पहले घटित होते हैं यदि ऑपरेशन A, B से पहले घटित होता है और ऑपरेशन B, C से पहले घटित होता है, तो ऑपरेशन A, C से पहले घटित होता है
मेमोरी आवंटन के मामले में, प्रत्येक थ्रेड की अपनी कार्यशील मेमोरी को मुख्य मेमोरी से अलग करने पर JVM को थ्रेड के भीतर निर्देशों के निष्पादन को अनुकूलित करने की बहुत गुंजाइश मिलती है। मुख्य मेमोरी के वेरिएबल थ्रेड की कार्यशील मेमोरी में कॉपी किए जा सकते हैं और उन पर अलग से कार्य किया जा सकता है। निष्पादन समाप्त होने के बाद परिणाम किसी समय मुख्य मेमोरी में वापस लिखे जा सकते हैं: लेकिन अलग-अलग थ्रेडों के बीच डेटा की संगति कैसे सुनिश्चित की जाए?JLS(Java Language Specification)का तरीका यह है कि डिफ़ॉल्ट रूप से हर क्षण डेटा की संगति की गारंटी नहीं दी जा सकती, लेकिन synchronized、volatile और final जैसे अधिक सशक्त अर्थवत्ता वाले कीवर्ड इस्तेमाल करके डेटा की संगति प्राप्त की जा सकती है।
JDK 6.0 synchronized का अनुकूलन
“गणराज्य के ज्येष्ठ पुत्र” के रूप में पहचाने जाने वाले synchronized कीवर्ड की चमक 5.0 संस्करण में ReentrantLock ने फीकी कर दी थी। इस संस्करण में बाज़ी पलटना ज़रूरी था, इसलिए JDK 6.0 ने लॉक में कुछ अनुकूलन किए, जैसे लॉक स्पिनिंग、लॉक उन्मूलन、लॉक समेकन、हल्के लॉक、पक्षपाती लॉक आदि। यह अनुकूलन “सूक्ष्म प्रबंधन” की अवधारणा की एक व्याख्या है। अनुकूलन से पहले synchronized से लॉक किए गए ऑब्जेक्ट की केवल दो अवस्थाएँ थीं:बिना लॉक, लॉक लगा हुआ(भारी लॉक)。अनुकूलन के बाद लॉक की कुल 4 अवस्थाएँ हैं, जिनका स्तर निम्न से उच्च क्रम में है:बिना लॉक、पक्षपाती लॉक、हल्का लॉक、भारी लॉक। प्रतिस्पर्धा की स्थिति के अनुसार ये अवस्थाएँ धीरे-धीरे ऊपर के स्तर पर जाती हैं, लेकिन नीचे नहीं आ सकतीं;इसका उद्देश्य लॉक प्राप्त करने और छोड़ने की दक्षता बढ़ाना है(लेखक को लगता है कि असल में यह बहुत जटिल था, जिससे JVM के अनुसंधान और विकास कर्मी पीछे हट गए)。
इस अनुकूलन ने synchronized को सिर ऊँचा करने का मौका दिया;अब किसी को यह कहने की अनुमति नहीं थी कि इसका प्रदर्शन ReentrantLock से खराब है। लेकिन असली मोड़ आगे था:JDK 15 में बायस्ड लॉक को अप्रचलित घोषित कर दिया गया(─.─||)。लेखक की राय में synchronized की कमज़ोरी यह है कि वह केवल एक कीवर्ड है और उसके निचले स्तर के काम JVM करता है। अनुप्रयोग के लिए लॉक लगाने का कौन सा तरीका सुविधाजनक होगा, यह JVM को “अनुमान” लगाना पड़ता है। ReentrantLock अलग है:वह यह काम सीधे प्रोग्रामर को सौंप देता है। निष्पक्षता चाहिए तो निष्पक्ष लॉक इस्तेमाल करें, और निष्पक्षता नहीं चाहिए तो गैर-निष्पक्ष लॉक। डिज़ाइन के स्तर पर यह कुछ हद तक मेहनत बचाना है, लेकिन साथ ही लचीलापन भी है।
JDK 7.0 Fork/Join फ़्रेमवर्क
Fork/Join का आगमन भी एक अपेक्षाकृत उन्नत तकनीक के रूप में हुआ। इसकी प्रमुख ताकत कार्यों को पुनरावर्ती ढंग से छोटे कार्यों में बाँटने और फिर उनके परिणामों को जोड़ने का समर्थन है। लेकिन यह तकनीक जानी-पहचानी भी है और अपरिचित भी। परिचित इसलिए कि इसका कई जगह इस्तेमाल होता है, जैसे आगे JDK8.0 में चर्चा किए जाने वाले CompletableFuture और Stream में; अपरिचित इसलिए कि व्यावसायिक अनुप्रयोगों के विकास के दौरान हम शायद ही कभी इसका इस्तेमाल करते हैं।
कुछ लोग तो इसे लगभग बेकार भी मानते हैं। मेरा विचार है कि यदि आप व्यावसायिक आवश्यकताओं पर काम करने वाले विकासकर्ता हैं, तो यह आपके लिए लगभग बेकार है, क्योंकि इसकी ज़रूरत शायद ही पड़ती है। बहुत बड़ी मात्रा के डेटा वाले मामलों के लिए डेटा वेयरहाउस के उपकरण हैं, और अन्य मामलों में थ्रेड पूल इस्तेमाल किए जा सकते हैं; यदि आप मिडलवेयर फ़्रेमवर्क विकसित करते हैं, तो यह बेकार नहीं है, शायद काम आ जाए। चीनी इंटरनेट पर बहुत कम लोग इस तकनीक पर सवाल उठाते हैं, लेकिन विदेशों में इस पर चर्चा शुरू हो चुकी है। रुचि रखने वाले सीधे लिंक पर जाकर पढ़ सकते हैं क्या Java का Fork-Join फ़्रेमवर्क खराब है?
JDK 8.0
इस संस्करण की रिलीज़ Java के लिए युगांतकारी थी, यहाँ तक कि आज दुनिया भर में चल रहे Java प्रोग्रामों में आधे से अधिक इसी संस्करण पर चलते हैं। लेकिन मल्टीथ्रेडिंग संबंधी बदलाव JDK5.0 जितने क्रांतिकारी नहीं थे। कुछ परमाण्विक ऑब्जेक्ट जोड़ने के अलावा, इस संस्करण के सबसे उल्लेखनीय बदलाव निम्नलिखित दो थे।
CompletableFuture
इंटरनेट पर CompletableFuture के बारे में बहुत सारी जानकारियाँ हैं, जिनमें अधिकतर उसके सिद्धांत और उपयोग का तरीका बताया जाता है। लेकिन लेखक को एक सवाल हमेशा समझ नहीं आया:जब थ्रेड पूल के इतने सारे उपकरण मौजूद हैं, तो CompletableFuture की ज़रूरत क्यों पड़ी, वह कौन-सी परेशानी दूर करता है?आख़िर उसकी मुख्य ताकत क्या है?मुझे विश्वास है कि अगर आपने इस पर विचार किया होगा तो आपके मन में भी यह सवाल उठा होगा, कोई बात नहीं, लेखक ने आपके लिए इसका उत्तर खोज लिया है।
निष्कर्ष:CompletableFuture की मुख्य प्रतिस्पर्धात्मक ताकत हैकार्य संयोजन।CompletableFuture को Future इंटरफ़ेस की विशेषताएँ विरासत में मिलती हैं,जैसे कार्यों को समवर्ती रूप से चलाना,इन क्षमताओं के विकल्प उपलब्ध हैं। लेकिन कार्यों का संयोजन करने की उसकी क्षमता का कोई विकल्प नहीं है,उसके मुख्य API में कार्य शृंखला बनाना और कार्यों के परिणाम जोड़ना जैसी सुविधाएँ इसी संयोजन के लिए बनाई गई हैं। इसलिए JDK के इस संस्करण में यह ढाँचा लाने का मुख्य उद्देश्य व्यावसायिक सॉफ़्टवेयर विकास में लगातार अधिक कठिन होती जा रही कार्य संयोजन की आवश्यकता को पूरा करना था।
अंत में एक बात और, CompletableFuture का अंतर्निहित कार्यान्वयन Fork/Join फ़्रेमवर्क पर आधारित है।
Stream
《क्लीन आर्किटेक्चर》 में प्रोग्रामिंग के तीन प्रतिमानों का उल्लेख किया गया है, संरचित प्रोग्रामिंग(प्रक्रिया उन्मुख प्रोग्रामिंग), वस्तु उन्मुख प्रोग्रामिंग और कार्यात्मक प्रोग्रामिंग। Stream, Java भाषा में कार्यात्मक प्रोग्रामिंग की एक अभिव्यक्ति है, लेखक का मानना है कि शुरुआती प्रोग्रामर से मध्य स्तर तक बढ़ने के लिए Stream पर महारत हासिल करना अनिवार्य है, पहली बार Stream से परिचय होने पर निश्चित ही बहुत असहज लगेगा, लेकिन उससे परिचित हो जाने के बाद प्रोग्रामिंग के तरीके को लेकर सोच का एक नया रास्ता खुलेगा। डेवलपर अक्सर तीन अवधारणाओं को आपस में मिला देते हैं, कार्यात्मक प्रोग्रामिंग, Stream और Lambda अभिव्यक्तियाँ, और हमेशा समझते हैं कि ये तीनों एक ही बात हैं। इस बारे में लेखक की समझ इस प्रकार है:
•फ़ंक्शनल प्रोग्रामिंग एक प्रोग्रामिंग विचारधारा है, जिसका प्रयोग विभिन्न प्रोग्रामिंग भाषाओं में होता है
•Stream JDK8.0 की एक नई सुविधा है। इसे एक नई अवधारणा भी समझ सकते हैं, जिसका उद्देश्य फ़ंक्शनल प्रोग्रामिंग के विचार के अनुरूप होना है। Stream के माध्यम से कलेक्शन क्लास पर फ़ंक्शनल प्रोग्रामिंग की जा सकती है
•Lambda अभिव्यक्ति(lambda expression)एक अनाम फ़ंक्शन है, जिससे फ़ंक्शनल प्रोग्रामिंग को अधिक संक्षिप्त और कुशल ढंग से व्यक्त किया जा सकता है
इतना सब कहने के बाद, Stream और मल्टीथ्रेडिंग का क्या संबंध है?Stream की संबंधित समानांतर विधियाँ अंदरूनी स्तर पर Fork/Join फ़्रेमवर्क से लागू की जाती हैं। 《Effective Java》 में इससे जुड़ी एक सलाह है “Stream के समानांतर निष्पादन का सावधानी से उपयोग करें”, क्योंकि सभी समानांतर कार्य एक साझा Fork/Join पूल में चलते हैं। किसी एक पाइपलाइन का असामान्य निष्पादन दूसरे असंबंधित हिस्सों के प्रदर्शन को प्रभावित कर सकता है।
JDK 9.0
लॉक के लिए प्रतिस्पर्धा की व्यवस्था में सुधार
लॉक के लिए प्रतिस्पर्धा कई Java बहुथ्रेड अनुप्रयोगों के प्रदर्शन को सीमित करती है। नया लॉक प्रतिस्पर्धा तंत्र Java ऑब्जेक्ट मॉनिटर का प्रदर्शन सुधारता है, जिसकी पुष्टि कई बेंचमार्क परीक्षणों(जैसे Volano)से हुई है। ऐसे परीक्षण JVM के अधिकतम थ्रूपुट का अनुमान लगा सकते हैं। व्यवहार में, नए लॉक प्रतिस्पर्धा तंत्र ने 22 अलग बेंचमार्क परीक्षणों में उत्कृष्ट परिणाम दिए हैं। यदि नया तंत्र Java 9 में लागू हो सके, तो अनुप्रयोगों का प्रदर्शन बहुत बढ़ जाएगा। सरल शब्दों में, जब कई थ्रेड लॉक के लिए प्रतिस्पर्धा करते हैं, अनुकूलन से पहले:देर से आने वाले सभी थ्रेड लॉक की प्रतीक्षा के लिए समान मानक प्रक्रिया अपनाते थे। अनुकूलन के बाद:JVM कुछ अनुकूलन योग्य स्थितियाँ पहचानकर देर से आने वाले थ्रेडों को सीधे “VIP मार्ग” से लॉक हथियाने देता है।
विस्तृत व्याख्या के लिए देखें: लॉक पर प्रतिस्पर्धा की व्याख्या – प्रदर्शन के दृष्टिकोण से
प्रतिक्रियाशील स्ट्रीम
प्रतिक्रियाशील प्रवाह(Reactive Streams)अवरुद्ध न करने वाले बैकप्रेशर के साथ अतुल्यकालिक डेटा प्रवाह को संसाधित करने का एक मानक है,जो आवश्यक क्रियाओं और इकाइयों का वर्णन करने के लिए इंटरफ़ेसों,विधियों और प्रोटोकॉलों का एक न्यूनतम समूह प्रदान करता है।
गैर-अवरोधक बैकप्रेशर क्या होता है? बैकप्रेशर, back pressure का संक्षिप्त रूप है,सरल शब्दों में,उत्पादक उपभोक्ता को डेटा भेजता है,जब उपभोक्ता उसे संसाधित नहीं कर पाता,तो वह उत्पादक को सूचित करता है,तब उत्पादक उत्पादन की गति कम कर देता है,इस व्यवस्था को अवरोधक तरीके से लागू करना सबसे सरल है,अर्थात डेटा भेजते समय सीधे दबाव संबंधी डेटा लौटाना। गैर-अवरोधक कार्यान्वयन डिज़ाइन की जटिलता बढ़ाता है,साथ ही प्रदर्शन भी बेहतर करता है। PS:मुझे लगता है कि “背压”(पीछे का दबाव)अनुवाद अच्छा नहीं है,इसका अर्थ केवल अक्षरों को देखकर नहीं समझा जा सकता। क्या “反压”(प्रतिकूल दबाव)बेहतर नहीं होगा^\_^
उपभोक्ता पर संसाधनों का भारी दबाव(pressure) पड़ने से उसके ठप हो जाने की समस्या हल करने के लिए,डेटा धारा की गति नियंत्रित करनी पड़ती है,अर्थात प्रवाह नियंत्रण(flow control),ताकि तेज़ डेटा धारा लक्ष्य को ठप न कर दे। इसलिए प्रतिदाब यानी बैकप्रेशर(back pressure) की आवश्यकता होती है,उत्पादक और उपभोक्ता को बैकप्रेशर की एक व्यवस्था लागू करके परस्पर समन्वय करना होता है। इस बैकप्रेशर व्यवस्था का कार्यान्वयन अतुल्यकालिक और गैर-अवरोधक होना चाहिए,यदि यह समकालिक और अवरोधक हो,तो उपभोक्ता द्वारा डेटा संसाधित करते समय उत्पादक को प्रतीक्षा करनी पड़ेगी,जिससे प्रदर्शन संबंधी समस्याएँ होंगी।
प्रतिक्रियाशील धाराएँ(Reactive Streams) इकाइयों, इंटरफ़ेस और पारस्परिक संचालन की विधियों का एक समूह परिभाषित करके गैर-अवरोधक बैकप्रेशर लागू करने का मानक देती हैं। तृतीय पक्ष इस मानक का पालन करके ठोस समाधान बनाते हैं; प्रचलित उदाहरणों में Reactor,RxJava,Akka Streams,Ratpack आदि शामिल हैं।
JDK 10 थ्रेड-स्थानीय नियंत्रण
Safepoint और उसकी कमियाँ:
Safepoint, Hotspot JVM की एक ऐसी व्यवस्था है जो सभी अनुप्रयोगों को रोकती है। कुछ निचले स्तर के कार्य करने के लिए JVM को Stop The World करना पड़ता है,जिससे सभी अनुप्रयोग थ्रेड रुक जाएँ। लेकिन उन्हें सीधे ज़बरदस्ती नहीं रोका जा सकता,इसके बजाय अनुप्रयोग थ्रेड को एक निर्देश संकेत भेजकर बताया जाता है कि अब रुकना चाहिए। जब अनुप्रयोग थ्रेड निष्पादन के दौरान किसी Safepoint पर पहुँचता है,तो वह निर्देश का पालन करके प्रतिक्रिया देता है। इसी कारण इसे Safepoint कहा जाता है। इसमें safe जोड़ने का उद्देश्य इस बात पर ज़ोर देना है कि JVM अब कुछ वैश्विक कार्य सुरक्षित रूप से करने वाला है,इसलिए इस बिंदु के नाम में safe जोड़ा गया है।
वैश्विक सुरक्षित अवस्था से जुड़े कार्यों में निम्नलिखित शामिल हैं: 1、कचरा संग्रहण के लिए विराम 2、कोड का अनुकूलन हटाना(Code deoptimization)。 3、कोड कैश फ़्लश करना। 4、क्लास फ़ाइलों की पुनर्परिभाषा के समय(Class redefinition, जैसे हॉट अपडेट या इंस्ट्रुमेंटेशन)。 5、बायस्ड लॉक रद्द करना(Biased lock revocation)。 6、विभिन्न डीबग कार्य(जैसे: डेडलॉक की जाँच या स्टैक ट्रेस डंप आदि)।
हालाँकि, सभी थ्रेड को उनके निकटतम safepoint पर रोकने में ही काफ़ी समय लगता है। और क्या सभी थ्रेड को रोक देना कुछ ज़्यादा ही उतावला और मनमाना नहीं लगता। इसी कारण Java10 में ऐसा तरीका पेश किया गया जिसमें सभी थ्रेड को रोकना ज़रूरी नहीं होता, जिसे थ्रेड-स्थानीय नियंत्रण(Thread Local Handshake)कहते हैं।
उदाहरण के लिए, ये स्थितियाँ सभी थ्रेड रोके बिना संभाली जा सकती हैं: 1、बायस्ड लॉक को रद्द करना। इसके लिए केवल एक थ्रेड रोकना पर्याप्त है;सभी थ्रेड रोकने की आवश्यकता नहीं। 2、विभिन्न प्रकार की सर्विसेबिलिटी जाँचों से VM की कुल विलंबता पर पड़ने वाला प्रभाव कम करना;उदाहरण के लिए, बड़ी संख्या में Java थ्रेड वाले VM पर सभी थ्रेडों के स्टैक ट्रेस प्राप्त करना धीमा हो सकता है। 3、सिग्नल(signals)पर निर्भरता घटाकर अधिक सुरक्षित स्टैक ट्रेस सैंपलिंग करना। 4、तथाकथित असममित Dekker समकालिकरण तकनीक का उपयोग करते हुए Java थ्रेडों से हैंडशेक करके कुछ मेमोरी बैरियर हटाना। उदाहरण के लिए, G1 और CMS में प्रयुक्त “सशर्त कार्ड चिह्नांकन कोड”(conditional card mark code)को अब “मेमोरी बैरियर” नाम की चीज़ की आवश्यकता नहीं होगी। इससे G1 द्वारा जारी “राइट बैरियर(write barrier)” को अनुकूलित किया जा सकेगा, और “मेमोरी बैरियर” से बचने की कोशिश करने वाली शाखाएँ भी हटाई जा सकेंगी।
JDK 15 में पक्षपाती लॉकिंग अक्षम और पदावनत की गई
बायस्ड लॉक को अप्रचलित क्यों किया जाए?पहले बायस्ड लॉक से मिलने वाला प्रदर्शन लाभ अब उतना स्पष्ट नहीं रह गया है। इससे लाभ पाने वाले अनुप्रयोग अक्सर शुरुआती Java संग्रह API(JDK 1.1)का उपयोग करते हैं, जिनमें ये API(Hashtable और Vector)हर पहुँच पर सिंक्रोनाइज़ेशन करते हैं। JDK 1.2 ने एकल थ्रेड वाले परिदृश्यों के लिए बिना सिंक्रोनाइज़ेशन वाले संग्रह(HashMap और ArrayList)प्रस्तुत किए, और JDK 1.5 ने अनेक थ्रेड वाले परिदृश्यों के लिए अधिक कुशल समवर्ती डेटा संरचनाएँ पेश कीं। इसका अर्थ है कि यदि कोड को नई कक्षाओं का उपयोग करने के लिए बदला जाए, तो अनावश्यक सिंक्रोनाइज़ेशन के कारण बायस्ड लॉक से लाभ पाने वाले अनुप्रयोगों के प्रदर्शन में बड़ा सुधार दिख सकता है। इसके अलावा, थ्रेड पूल की कतारों और कार्यकर्ता थ्रेडों पर आधारित अनुप्रयोगों का प्रदर्शन अक्सर बायस्ड लॉक बंद करने पर बेहतर होता है।
नीचे Hashtable और Vector का उपयोग करने वाले API के कार्यान्वयन दिए गए हैं: java.lang.Classloader Vector का उपयोग करता है java.util.Properties extends Hashtable java.security.Provider extends Properties java.net.URL Hashtable का उपयोग करता है java.net.URConnection Hashtable का उपयोग करता है java.util.ZipOutputStream Vector का उपयोग करता है javax.management.timer.TimerMBean इंटरफ़ेस पर Vector मौजूद है
स्वचालित ड्राइविंग का युग
वर्चुअल थ्रेड ने Java को स्वचालित ड्राइविंग के युग में पहुँचा दिया है। कई भाषाओं में “वर्चुअल थ्रेड” जैसी तकनीक है, जैसे Go, C#, Erlang, Lua आदि;वे इसे “कोरूटीन” कहते हैं। इस बार java ने कोई नया कीवर्ड नहीं जोड़ा, यहाँ तक कि कोई नई अवधारणा भी नहीं जोड़ी। goroutine और कोरूटीन की तुलना में वर्चुअल थ्रेड को समझना कहीं आसान है;नाम देखकर ही लगभग समझ आ जाता है कि यह क्या करता है।
JDK 19 वर्चुअल थ्रेड
पारंपरिक Java में थ्रेड मॉडल ऑपरेटिंग सिस्टम के थ्रेडों से 1:1 अनुपात में मेल खाता है। थ्रेड बनाना और उनके बीच स्विच करना महँगा होता है, और ऑपरेटिंग सिस्टम की सीमाओं के कारण केवल सीमित संख्या में थ्रेड बनाए जा सकते हैं। समवर्ती अनुरोध बहुत अधिक होने पर हर अनुरोध के लिए एक थ्रेड बनाना संभव नहीं होता। थ्रेड पूल इस समस्या को कुछ कम कर सकता है। यह थ्रेड निर्माण का खर्च घटाता है, लेकिन थ्रेडों की संख्या नहीं बढ़ा सकता। मान लें समवर्ती अनुरोध 2000 हैं और थ्रेड पूल में केवल 1000 थ्रेड हैं, तो एक समय में केवल 1000 अनुरोध संभाले जा सकते हैं। शेष 1000 अनुरोध संभाले नहीं जा सकते;उन्हें अस्वीकार किया जा सकता है या किसी थ्रेड के उपलब्ध होने तक प्रतीक्षा कराई जा सकती है।
वर्चुअल थ्रेड से पहले का समाधान अतुल्यकालिक शैली अपनाना था। अनेक फ़्रेमवर्क पहले ही इस शैली की समवर्ती प्रोग्रामिंग लागू कर चुके हैं(जैसे Spring5 का Reactor), जो थ्रेड साझा करके अधिक उपलब्धता हासिल करते हैं। इसका सिद्धांत है कि थ्रेड साझा करने से उनके बीच स्विच कम होते हैं और खर्च घटता है;साथ ही ब्लॉकिंग से बचा जाता है। थ्रेड केवल प्रोग्राम चलने के दौरान इस्तेमाल होता है और प्रोग्राम के प्रतीक्षा करने पर थ्रेड व्यस्त नहीं रहता। अतुल्यकालिक शैली से कई सुधार होते हैं, लेकिन इसकी कमियाँ भी हैं। अधिकतर अतुल्यकालिक फ़्रेमवर्क चेन शैली में लिखे जाते हैं, जिसमें प्रोग्राम कई चरणों में बाँटा जाता है और प्रत्येक चरण अलग थ्रेड में चल सकता है। अब आप परिचित ThreadLocal जैसे समवर्ती प्रोग्रामिंग API इस्तेमाल नहीं कर सकते, अन्यथा त्रुटियाँ हो सकती हैं। प्रोग्रामिंग शैली में भी बड़ा बदलाव आता है;यह पारंपरिक शैली से बहुत अधिक जटिल है और इसे सीखने की लागत अधिक है। परियोजना के कई मौजूदा मॉड्यूल को भी अतुल्यकालिक मॉडल के अनुरूप बदलना पड़ सकता है।
वर्चुअल थ्रेड का कार्यान्वयन सिद्धांत कुछ अतुल्यकालिक फ़्रेमवर्क जैसा है:यह भी थ्रेड साझा करता है, इसलिए पूल बनाने की आवश्यकता नहीं होती। उपयोग करते समय आप वर्चुअल थ्रेड को असीमित रूप से उपलब्ध मान सकते हैं और जितने चाहें बना सकते हैं, बिना किसी समस्या की चिंता किए। इतना ही नहीं, वर्चुअल थ्रेड डिबगिंग का समर्थन करते हैं और Java से जुड़े निगरानी उपकरण भी उनका समर्थन कर सकते हैं, जो बहुत महत्वपूर्ण है। वर्चुअल थ्रेड आपके प्रोग्राम की मेमोरी खपत बहुत घटा देंगे। Web Servers जैसे सभी IO प्रधान अनुप्रयोग समान हार्डवेयर पर IO थ्रूपुट बहुत बढ़ा सकते हैं। पहले 1G मेमोरी में एक साथ 1000 अनुरोध संभाले जा सकते थे। आधिकारिक कथन के अनुसार, वर्चुअल थ्रेड का उपयोग करने के बाद आसानी से 100 लाख समवर्ती अनुरोध संभाले जा सकते हैं। किसी विशिष्ट व्यावसायिक परिस्थिति में यह संभव है या नहीं, यह दबाव परीक्षण से ही पता चलेगा, लेकिन अनुमान कम करके देखें तो 10 लाख तो आसानी से संभव होने चाहिए। इसके लिए आपको कोई अतिरिक्त कीमत नहीं चुकानी पड़ती, शायद कोड भी बदलना न पड़े। वर्चुअल थ्रेड आपको पारंपरिक प्रोग्रामिंग शैली, यानी हर अनुरोध के लिए एक थ्रेड वाला मॉडल बनाए रखने देते हैं। उन्हें सामान्य थ्रेड की तरह इस्तेमाल किया जा सकता है और प्रोग्राम में बहुत कम बदलाव चाहिए। वर्चुअल थ्रेड कोई नया सिंटैक्स भी नहीं लाते, इसलिए सीखने और माइग्रेशन की लागत बेहद कम कही जा सकती है।
उल्लेखनीय है कि वर्चुअल थ्रेड के निचले स्तर पर अब भी Fork/Join फ्रेमवर्क का उपयोग होता है।