paint-brush
जावा थ्रेड प्रिमिटिव्स को पुनः सीखने पर एक गाइडद्वारा@shai.almog
761 रीडिंग
761 रीडिंग

जावा थ्रेड प्रिमिटिव्स को पुनः सीखने पर एक गाइड

द्वारा Shai Almog8m2023/04/11
Read on Terminal Reader

बहुत लंबा; पढ़ने के लिए

सिंक्रोनाइज़्ड क्रांतिकारी था और अभी भी इसके बहुत उपयोग हैं। लेकिन यह नए थ्रेड प्रिमिटिव में जाने का समय है और संभावित रूप से, हमारे मूल तर्क पर पुनर्विचार करें।
featured image - जावा थ्रेड प्रिमिटिव्स को पुनः सीखने पर एक गाइड
Shai Almog HackerNoon profile picture

मैंने पहले बीटा के बाद से जावा में कोड किया है; उस समय भी, थ्रेड्स मेरी पसंदीदा विशेषताओं की सूची में सबसे ऊपर थे। जावा भाषा में ही थ्रेड सपोर्ट शुरू करने वाली पहली भाषा थी; यह उस समय एक विवादास्पद निर्णय था।


पिछले एक दशक में, प्रत्येक भाषा में async/प्रतीक्षा को शामिल करने के लिए दौड़ लगाई गई थी, और यहां तक कि जावा के पास इसके लिए कुछ तृतीय-पक्ष का समर्थन था ... लेकिन जावा ने ज़ैगिंग के बजाय ज़िग किया और बहुत बेहतर वर्चुअल थ्रेड्स (प्रोजेक्ट लूम) पेश किया। यह पोस्ट उस बारे में नहीं है।


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


इस पोस्ट में, मैं जावा में थ्रेडिंग करने के पुराने तरीकों पर दोबारा गौर करना चाहता हूं। मैं synchronized , wait , notify , आदि के लिए उपयोग किया जाता हूं, लेकिन यह एक लंबा समय रहा है क्योंकि वे जावा में थ्रेडिंग के लिए बेहतर दृष्टिकोण थे।


मैं समस्या का हिस्सा हूँ; मैं अभी भी इन दृष्टिकोणों के लिए अभ्यस्त हूं और जावा 5 के बाद से मौजूद कुछ एपीआई के लिए उपयोग करना मुश्किल है। यह आदत की ताकत है। टी


थ्रेड्स के साथ काम करने के लिए कई बेहतरीन एपीआई हैं जिनकी चर्चा मैं यहां वीडियो में करता हूं, लेकिन मैं उन तालों के बारे में बात करना चाहता हूं जो बुनियादी लेकिन महत्वपूर्ण हैं।

सिंक्रोनाइज़्ड बनाम रेंट्रेंटलॉक

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


JDK 21 इसे ठीक कर सकता है (जब लूम GA जाता है), लेकिन फिर भी इसे छोड़ने का कुछ मतलब है।


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


वास्तव में, इसका एक बड़ा नुकसान है; इसे समझने के लिए, आइए एक उदाहरण देखें। इस प्रकार हम सिंक्रनाइज़ का उपयोग करेंगे:

 synchronized(LOCK) { // safe code } LOCK.lock(); try { // safe code } finally { LOCK.unlock(); }


ReentrantLock का पहला नुकसान वाचालता है। हमें try ब्लॉक की आवश्यकता है क्योंकि यदि ब्लॉक के भीतर कोई अपवाद होता है, तो लॉक बना रहेगा। सिंक्रनाइज़ किए गए हैंडल जो हमारे लिए निर्बाध रूप से काम करते हैं।


कुछ लोग AutoClosable के साथ लॉक को लपेटने की एक तरकीब है जो मोटे तौर पर इस तरह दिखती है:

 public class ClosableLock implements AutoCloseable { private final ReentrantLock lock; public ClosableLock() { this.lock = new ReentrantLock(); } public ClosableLock(boolean fair) { this.lock = new ReentrantLock(fair); } @Override public void close() throws Exception { lock.unlock(); } public ClosableLock lock() { lock.lock(); return this; } public ClosableLock lockInterruptibly() throws InterruptedException { lock.lock(); return this; } public void unlock() { lock.unlock(); } }


ध्यान दें कि मैं लॉक इंटरफ़ेस लागू नहीं करता जो आदर्श होता। ऐसा इसलिए है क्योंकि लॉक विधि void के बजाय ऑटो-क्लोजेबल कार्यान्वयन लौटाती है।


एक बार जब हम ऐसा कर लेते हैं, तो हम इस तरह से अधिक संक्षिप्त कोड लिख सकते हैं:

 try(LOCK.lock()) { // safe code }


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


मुझे लगता है कि लॉक इंटरफ़ेस का समर्थन करने के लिए संसाधन सिंटैक्स के साथ प्रयास करना अच्छा हो सकता है। लेकिन जब तक ऐसा नहीं होता, यह एक सार्थक चाल नहीं हो सकती है।

रेंट्रेंटलॉक के लाभ

ReentrantLock का उपयोग करने का सबसे बड़ा कारण लूम सपोर्ट है। अन्य फायदे अच्छे हैं, लेकिन उनमें से कोई भी "हत्यारा विशेषता" नहीं है।


हम इसे निरंतर ब्लॉक के बजाय विधियों के बीच उपयोग कर सकते हैं। यह शायद एक बुरा विचार है क्योंकि आप लॉक क्षेत्र को कम करना चाहते हैं, और विफलता एक समस्या हो सकती है। मैं उस सुविधा को एक लाभ के रूप में नहीं मानता।


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


यदि आप एक संसाधन पर लगातार कई थ्रेड्स के साथ एक जटिल अनुसूचक लिख रहे हैं, तो आप एक ऐसी स्थिति बना सकते हैं जहां एक थ्रेड "भूखा" हो क्योंकि अन्य थ्रेड्स आते रहते हैं। .


शायद मैं यहाँ कुछ याद कर रहा हूँ ...


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


यदि आप ऐसा कोड लिखते हैं जो बाधित करने के लिए बहुत ही संवेदनशील होना चाहिए, तो आपको उस क्षमता को प्राप्त करने के लिए lockInterruptibly() API का उपयोग करना होगा। लेकिन आप औसतन lock() विधि में कितना समय व्यतीत करते हैं?


ऐसे किनारे मामले हैं जहां यह शायद मायने रखता है लेकिन उन्नत बहु-थ्रेडेड कोड करते समय भी हम में से अधिकांश कुछ नहीं चलेंगे।

ReadWriteReentrantLock

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


इसका मतलब है कि हम लिखने के कार्यों को थोड़ा धीमा करते हुए पढ़ने को अत्यधिक अनुकूलित कर सकते हैं।


यह मानते हुए कि यह आपका उपयोग मामला है, आप बहुत तेज कोड बना सकते हैं। रीड-राइट लॉक के साथ काम करते समय, हमारे पास दो लॉक होते हैं; एक रीड लॉक जैसा कि हम निम्नलिखित इमेज में देख सकते हैं। यह कई धागे के माध्यम से देता है और प्रभावी रूप से "सभी के लिए नि: शुल्क" है।


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


एक बार जब थ्रेड्स पढ़ना समाप्त कर लेते हैं, तो सभी रीडिंग ब्लॉक हो जाएंगी, और राइटिंग ऑपरेशन केवल एक थ्रेड से हो सकता है जैसा कि निम्नलिखित इमेज में देखा गया है। एक बार राइट लॉक जारी करने के बाद, हम पहली छवि में "सभी के लिए निःशुल्क" स्थिति में वापस जाएंगे।


यह एक शक्तिशाली पैटर्न है जिसका लाभ उठाकर हम संग्रह को और तेज़ बना सकते हैं। एक विशिष्ट सिंक्रनाइज़ सूची उल्लेखनीय रूप से धीमी है। यह पढ़ने या लिखने के सभी कार्यों पर सिंक्रनाइज़ करता है। हमारे पास एक CopyOnWriteArrayList है जो पढ़ने में तेज़ है, लेकिन कोई भी लिखना बहुत धीमा है।


यह मानते हुए कि आप अपने तरीकों से पुनरावृत्तियों को वापस करने से बच सकते हैं, आप सूची संचालन को समाहित कर सकते हैं और इस एपीआई का उपयोग कर सकते हैं।


उदाहरण के लिए, निम्नलिखित कोड में, हम नामों की सूची को केवल पढ़ने के लिए उजागर करते हैं, लेकिन फिर जब हमें कोई नाम जोड़ने की आवश्यकता होती है तो हम राइट लॉक का उपयोग करते हैं। यह synchronized सूचियों को आसानी से मात दे सकता है:

 private final ReadWriteLock LOCK = new ReentrantReadWriteLock(); private Collection<String> listOfNames = new ArrayList<>(); public void addName(String name) { LOCK.writeLock().lock(); try { listOfNames.add(name); } finally { LOCK.writeLock().unlock(); } } public boolean isInList(String name) { LOCK.readLock().lock(); try { return listOfNames.contains(name); } finally { LOCK.readLock().unlock(); } }

मुद्रांकित ताला

StampedLock के बारे में सबसे पहले हमें यह समझने की जरूरत है कि यह रीएन्ट्रेंट नहीं है। कहें कि हमारे पास यह ब्लॉक है:

 synchronized void methodA() { // … methodB(); // … } synchronized void methodB() { // … }

यह काम करेगा। चूंकि सिंक्रोनाइज़्ड रीएन्ट्रेंट है। हम पहले से ही लॉक रखते हैं, इसलिए methodB() से methodA() में जाने से ब्लॉक नहीं होगा। यह ReentrantLock के साथ भी काम करता है, यह मानते हुए कि हम एक ही लॉक या एक ही सिंक्रोनाइज़्ड ऑब्जेक्ट का उपयोग करते हैं।


StampedLock एक स्टैम्प लौटाता है जिसका उपयोग हम लॉक को रिलीज़ करने के लिए करते हैं। इस कारण इसकी कुछ सीमाएँ हैं। लेकिन यह अभी भी बहुत तेज़ और शक्तिशाली है। इसमें एक रीड-एंड-राइट स्टैम्प भी शामिल है जिसका उपयोग हम एक साझा संसाधन की रक्षा के लिए कर सकते हैं।


लेकिन ReadWriteReentrantLock, यह हमें लॉक को अपग्रेड करने देता है। हमें ऐसा करने की आवश्यकता क्यों होगी?

addName() विधि को पहले से देखें ... क्या होगा अगर मैं इसे "शाई" के साथ दो बार बुलाऊं?


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

 public void addName(String name) { LOCK.writeLock().lock(); try { if(!listOfNames.contains(name)) { listOfNames.add(name); } } finally { LOCK.writeLock().unlock(); } }


यह बेकार है। मैंने कुछ मामलों में केवल contains() चेक करने के लिए राइट लॉक के लिए "भुगतान" किया है (यह मानते हुए कि कई डुप्लिकेट हैं)। राइट लॉक प्राप्त करने से पहले हम isInList(name) कॉल कर सकते हैं। तब हम:


  • रीड लॉक को पकड़ें


  • रीड लॉक जारी करें


  • राइट लॉक को पकड़ें


  • राइट लॉक जारी करें


हड़पने के दोनों मामलों में, हम कतारबद्ध हो सकते हैं, और यह अतिरिक्त परेशानी के लायक नहीं हो सकता है।


StampedLock के साथ, हम रीड लॉक को राइट लॉक में अपडेट कर सकते हैं और यदि आवश्यक हो तो मौके पर ही बदलाव कर सकते हैं:

 public void addName(String name) { long stamp = LOCK.readLock(); try { if(!listOfNames.contains(name)) { long writeLock = LOCK.tryConvertToWriteLock(stamp); if(writeLock == 0) { throw new IllegalStateException(); } listOfNames.add(name); } } finally { LOCK.unlock(stamp); } }

यह इन मामलों के लिए एक शक्तिशाली अनुकूलन है।

आखिरकार

मैंने उपरोक्त वीडियो श्रृंखला में इसी तरह के कई विषयों को शामिल किया है; इसे देखें, और मुझे बताएं कि आप क्या सोचते हैं।


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


लूम के साथ व्यवहार करते समय यह विशेष रूप से सच है जहां अंतर्निहित विवाद कहीं अधिक संवेदनशील है। 1M समवर्ती थ्रेड्स पर स्केलिंग रीड ऑपरेशंस की कल्पना करें ... उन मामलों में, लॉक विवाद को कम करने का महत्व कहीं अधिक है।


आप सोच सकते हैं, synchronized संग्रह ReadWriteReentrantLock या यहां तक कि StampedLock उपयोग क्यों नहीं कर सकते?


यह समस्याग्रस्त है क्योंकि एपीआई का सतह क्षेत्र इतना बड़ा है कि इसे सामान्य उपयोग के मामले में अनुकूलित करना कठिन है। यही वह जगह है जहाँ निम्न-स्तर के आदिम पर नियंत्रण उच्च थ्रूपुट और ब्लॉकिंग कोड के बीच अंतर कर सकता है।