स्थैतिक इनिशियलाइज़र में लैम्ब्डा के साथ समानांतर धारा एक गतिरोध का कारण क्यों बनती है?


86

मैं एक अजीब स्थिति में आया था, जहां एक स्थिर इनिशियलाइज़र में लैम्ब्डा के साथ समानांतर धारा का उपयोग करना हमेशा के लिए सीपीयू उपयोग के साथ प्रतीत होता है। यहाँ कोड है:

class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(i -> i).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

यह इस व्यवहार के लिए एक न्यूनतम प्रजनन परीक्षण मामला प्रतीत होता है। अगर मैं:

  • स्टैटिक इनिशियलाइज़र के बजाय ब्लॉक को मुख्य विधि में रखें,
  • समानांतर हटाना, या
  • लंबोदर को हटाओ,

कोड तुरंत पूरा होता है। क्या कोई इस व्यवहार की व्याख्या कर सकता है? यह एक बग है या यह इरादा है?

मैं OpenJDK संस्करण 1.8.0_66-आंतरिक का उपयोग कर रहा हूं।


4
रेंज (0, 1) के साथ कार्यक्रम सामान्य रूप से समाप्त हो जाता है। (0, 2) या उच्चतर हैंग के साथ।
लेज़्ज़्लो हिरडी


2
वास्तव में यह बिल्कुल एक ही प्रश्न / मुद्दा है, बस एक अलग एपीआई के साथ।
दीदीयर एल

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

4
@ सोलोमनऑफ़सर्किट i -> iएक विधि संदर्भ नहीं है क्योंकि यह static methodडेडलॉक वर्ग में लागू है। यदि इस कोड से प्रतिस्थापित i -> iकिया Function.identity()जाए तो ठीक होना चाहिए।
पीटर लॉरी

जवाबों:


71

मुझे एक बहुत ही समान मामले ( JDK-8143380 ) की बग रिपोर्ट मिली, जो स्टुअर्ट मार्क्स द्वारा "नॉट ए इशू " के रूप में बंद थी:

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

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


मैं ( JDK-8136753 ) की एक और बग रिपोर्ट पा रहा था , जिसे स्टुअर्ट मार्क्स द्वारा "नॉट ए इशू " के रूप में भी बंद किया गया था:

यह एक गतिरोध है जो हो रहा है क्योंकि फ्रूट एनम के स्टैटिक इनिशियलाइज़र वर्ग आरंभीकरण के साथ बुरी तरह से बातचीत कर रहा है।

वर्ग आरंभीकरण के विवरण के लिए जावा भाषा विनिर्देश, खंड 12.4.2 देखें।

http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2

संक्षेप में, जो कुछ हो रहा है वह इस प्रकार है।

  1. मुख्य धागा फ्रूट क्लास को संदर्भित करता है और आरंभीकरण प्रक्रिया शुरू करता है। यह इनिशियलाइज़ेशन-प्रोग्रेस फ़्लैग को सेट करता है और स्टैटिक इनिशियलाइज़र को मुख्य थ्रेड पर चलाता है।
  2. स्टैटिक इनिशियलाइज़र किसी और धागे में कुछ कोड चलाता है और इसके खत्म होने का इंतज़ार करता है। यह उदाहरण समानांतर धाराओं का उपयोग करता है, लेकिन इसका प्रति धाराओं के साथ कोई लेना-देना नहीं है। किसी भी माध्यम से किसी अन्य थ्रेड में कोड निष्पादित करना, और उस कोड को समाप्त करने के लिए इंतजार करना, एक ही प्रभाव होगा।
  3. दूसरे थ्रेड में कोड फ्रूट क्लास को संदर्भित करता है, जो इनिशियलाइज़ेशन इन-प्रोग्रेस फ़्लैग की जाँच करता है। यह दूसरे धागे को तब तक अवरुद्ध करने का कारण बनता है जब तक झंडा साफ नहीं हो जाता। (JLS 12.4.2 का चरण 2 देखें।)
  4. मुख्य धागा दूसरे धागे के समाप्त होने के इंतजार में अवरुद्ध है, इसलिए स्थैतिक आरंभीकरण कभी पूरा नहीं होता है। चूंकि प्रारंभिक इनिशियलाइज़र के पूरा होने के बाद प्रारंभ-प्रगति ध्वज को साफ़ नहीं किया जाता है, इसलिए थ्रेड्स को बंद कर दिया जाता है।

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

एक मुद्दा के रूप में बंद करना।


ध्यान दें कि FindBugs में इस स्थिति के लिए चेतावनी जोड़ने के लिए एक खुला मुद्दा है।


20
"यह जब हम सुविधा के लिए बनाया गया माना जाता था" और "हम जानते हैं कि इस बग का कारण बनता है, लेकिन नहीं है कि यह कैसे तय करने के लिए" ऐसा नहीं मतलब "यह एक बग नहीं है" । यह बिल्कुल बग है।
ब्लूराजा - डैनी पफ्लुगुएफ्ट

13
@ bayou.io मुख्य मुद्दा स्थिर इनिशियलाइज़र के भीतर थ्रेड्स का उपयोग कर रहा है, लैम्ब्डा नहीं।
स्टुअर्ट मार्क्स

5
मेरी बग रिपोर्ट खोदने के लिए BTW तुनाकी धन्यवाद। :-)
स्टुअर्ट मार्क्स

13
@ bayou.io: यह क्लास स्तर पर एक ही बात है क्योंकि यह एक कंस्ट्रक्टर में होगा, thisऑब्जेक्ट निर्माण के दौरान बच जाएगा। मूल नियम है, आरंभिकों में बहु-थ्रेडेड संचालन का उपयोग न करना। मुझे नहीं लगता कि यह समझना मुश्किल है। एक लंबोदर कार्यान्वित फ़ंक्शन को एक रजिस्ट्री में पंजीकृत करने का आपका उदाहरण एक अलग बात है, यह गतिरोध पैदा नहीं करता है जब तक कि आप इन अवरुद्ध पृष्ठभूमि धागे के लिए इंतजार नहीं करने जा रहे हैं। फिर भी, मैं इस तरह के ऑपरेशन को एक क्लास इनिशियलाइज़र में करने से दृढ़ता से हतोत्साहित करता हूँ। ऐसा नहीं है कि वे किसके लिए हैं।
होल्गर

9
मुझे लगता है कि प्रोग्रामिंग शैली सबक है: स्थिर initalizers रखें।
राएडवल्ड

16

उन लोगों के लिए जो यह सोच रहे हैं कि अन्य धागे Deadlockकक्षा को संदर्भित करने के लिए कहां हैं , जावा लैम्ब्डा ऐसा व्यवहार करते हैं जैसे आपने यह लिखा है:

public class Deadlock {
    public static int lambda1(int i) {
        return i;
    }
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return lambda1(operand);
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

नियमित अनाम कक्षाओं के साथ कोई गतिरोध नहीं है:

public class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return operand;
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

5
@ सोलोमनऑफ़सर्किट एक कार्यान्वयन विकल्प है। लैम्ब्डा में कोड को कहीं जाना है। जावेद इसे युक्त कक्षा में एक स्थिर विधि में संकलित करता है ( lambda1मैं इस उदाहरण के अनुरूप )। प्रत्येक मेमने को अपनी कक्षा में रखना काफी महंगा होता।
स्टुअर्ट मार्क्स

1
@StuartMarks यह देखते हुए कि लैम्ब्डा कार्यात्मक इंटरफ़ेस को लागू करने वाला एक वर्ग बनाता है, क्या यह लैम्बडा के कार्यान्वयन को कार्यात्मक इंटरफ़ेस के लैम्ब्डा के कार्यान्वयन में लगाने के लिए उतना ही कुशल नहीं होगा जितना कि इस पोस्ट के दूसरे उदाहरण में? यह निश्चित रूप से चीजों को करने का स्पष्ट तरीका है, लेकिन मुझे यकीन है कि एक कारण है कि वे जिस तरह से कर रहे हैं, वे क्यों कर रहे हैं।
मोनिका

6
@ सोलोमनऑफ़सिटेक लैम्बडा रनटाइम पर ( java.lang.invoke.LambdaMetafactory के माध्यम से ) एक क्लास बना सकता है , लेकिन लैम्ब्डा बॉडी को कंपाइल टाइम पर कहीं रखा जाना चाहिए। लैम्ब्डा कक्षाएं इस प्रकार .class फ़ाइलों से भरी हुई सामान्य कक्षाओं की तुलना में कुछ VM जादू का लाभ ले सकती हैं।
जेफरी बोसबोम

1
@ सोलोमनऑफसर्किट हां, जेफरी बोसबोम का जवाब सही है। यदि भविष्य के जेवीएम में मौजूदा वर्ग के लिए एक विधि को जोड़ना संभव हो जाता है, तो मेटाफैक्ट एक नए वर्ग को स्पिन करने के बजाय ऐसा कर सकता है। (शुद्ध अटकलें।)
स्टुअर्ट मार्क्स

3
@ सोलोमनऑफ सीक्रेट: अपने जैसे तुच्छ मेमने के भावों को देखकर न्याय न करें i -> i; वे आदर्श नहीं होंगे। लैम्ब्डा भाव अपने आसपास के वर्ग के सभी सदस्यों का उपयोग कर सकते हैं, जिनमें privateलोग भी शामिल हैं, और यह परिभाषित करने वाले वर्ग को अपना स्वाभाविक स्थान बनाता है। इन सभी उपयोग के मामलों को लागू करना, वर्ग आरम्भिकों के विशेष मामले के लिए अनुकूलित कार्यान्वयन से ग्रस्त है, जिसमें बहुभाषी लैम्ब्डा अभिव्यक्ति के बहु-थ्रेडेड उपयोग के साथ, उनके परिभाषित वर्ग के सदस्यों का उपयोग नहीं करना, एक व्यवहार्य विकल्प नहीं है।
होल्गर

14

आंद्रेई पैंगिन द्वारा इस समस्या का एक उत्कृष्ट विवरण है , दिनांक 07 अप्रैल 2015 तक। यह यहां उपलब्ध है , लेकिन यह रूसी में लिखा गया है (मैं वैसे भी कोड नमूनों की समीक्षा करने का सुझाव देता हूं - वे अंतरराष्ट्रीय हैं)। क्लास इनिशियलाइजेशन के दौरान सामान्य समस्या ताला है।

लेख के कुछ उद्धरण इस प्रकार हैं:


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

मैंने एक साधारण प्रोग्राम लिखा जो पूर्णांक की राशि की गणना करता है, इसे क्या प्रिंट करना चाहिए?

public class StreamSum {
    static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt();

    public static void main(String[] args) {
        System.out.println(SUM);
    }
} 

अब कॉल के parallel()साथ लैम्ब्डा को हटा दें या बदल दें Integer::sum- क्या बदल जाएगा?

यहाँ हम फिर से गतिरोध को देखते हैं [पहले लेख में कक्षा शुरुआती में गतिरोध के कुछ उदाहरण थे]। क्योंकि parallel()स्ट्रीम ऑपरेशन एक अलग थ्रेड पूल में चलते हैं। ये धागे लैम्ब्डा बॉडी को निष्पादित करने की कोशिश करते हैं, जिसे क्लास के private staticअंदर एक विधि के रूप में बाईटेकोड में लिखा जाता है StreamSum। लेकिन इस विधि को वर्ग स्थिर इनिशियलाइज़र के पूरा होने से पहले निष्पादित नहीं किया जा सकता है, जो स्ट्रीम पूरा होने के परिणामों की प्रतीक्षा करता है।

अधिक माइंडब्लोइंग क्या है: यह कोड अलग-अलग वातावरण में अलग-अलग तरीके से काम करता है। यह एक एकल सीपीयू मशीन पर सही ढंग से काम करेगा और बहु ​​सीपीयू मशीन पर लटकाएगा। यह अंतर फोर्क-जॉइन पूल कार्यान्वयन से आता है। आप पैरामीटर को बदलते हुए इसे स्वयं सत्यापित कर सकते हैं-Djava.util.concurrent.ForkJoinPool.common.parallelism=N

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.