क्यों यह जावा प्रोग्राम समाप्त होने के बावजूद स्पष्ट रूप से इसे (और नहीं) नहीं करना चाहिए?


205

मेरी प्रयोगशाला में एक संवेदनशील ऑपरेशन आज पूरी तरह से गलत हो गया। इलेक्ट्रॉन माइक्रोस्कोप पर एक एक्ट्यूएटर अपनी सीमा पर चला गया, और घटनाओं की एक श्रृंखला के बाद मैंने $ 12 मिलियन उपकरण खो दिए। मैंने इसमें दोषपूर्ण मॉड्यूल में 40K से अधिक लाइनों को संकुचित किया है:

import java.util.*;

class A {
    static Point currentPos = new Point(1,2);
    static class Point {
        int x;
        int y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    public static void main(String[] args) {
        new Thread() {
            void f(Point p) {
                synchronized(this) {}
                if (p.x+1 != p.y) {
                    System.out.println(p.x+" "+p.y);
                    System.exit(1);
                }
            }
            @Override
            public void run() {
                while (currentPos == null);
                while (true)
                    f(currentPos);
            }
        }.start();
        while (true)
            currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}

आउटपुट के कुछ नमूने मुझे मिल रहे हैं:

$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651

चूँकि यहाँ कोई फ्लोटिंग पॉइंट अंकगणित नहीं है, और हम सभी जानते हैं कि पूर्णांक पर हस्ताक्षर किए गए पूर्णांक जावा में अतिप्रवाह पर अच्छा व्यवहार करते हैं, मुझे लगता है कि इस कोड में कुछ भी गलत नहीं है। हालाँकि, आउटपुट के संकेत के बावजूद कि कार्यक्रम निकास स्थिति तक नहीं पहुंचा था, यह बाहर निकलने की स्थिति तक पहुँच गया (यह दोनों तक पहुँचा गया और पहुँचा नहीं गया?)। क्यों?


मैंने देखा है कि यह कुछ वातावरणों में नहीं होता है। मैं 64 बिट लिनक्स पर OpenJDK 6 पर हूं ।


41
उपकरणों का 12 मिलियन? मैं वास्तव में उत्सुक हूं कि ऐसा कैसे हो सकता है ... आप खाली सिंक्रनाइज़ेशन ब्लॉक का उपयोग क्यों कर रहे हैं: सिंक्रनाइज़ (यह) {}?
मार्टिन वी।

84
यह भी दूर से सुरक्षित धागा नहीं है।
मैट बॉल

8
नोट करने के लिए दिलचस्प: finalखेतों में क्वालीफायर (जो उत्पादित बायोटेक पर कोई प्रभाव नहीं है) को जोड़ना xऔर yबग को "हल" करता है। हालाँकि यह बाइटकोड को प्रभावित नहीं करता है, लेकिन इसके साथ खेतों को चिह्नित किया जाता है, जो मुझे लगता है कि यह एक जेवीएम अनुकूलन का एक साइड-इफेक्ट है।
नीव स्टिंगटन

9
@ यूजीन: यह समाप्त नहीं होना चाहिए । सवाल है "यह समाप्त क्यों होता है?"। ए Point pका निर्माण किया जाता है जो संतुष्ट करता है p.x+1 == p.y, फिर एक संदर्भ मतदान धागे को दिया जाता है। आखिरकार मतदान सूत्र बाहर निकलने का फैसला करता है क्योंकि यह सोचता है कि Pointयह प्राप्त स्थिति में से किसी एक के लिए स्थिति संतुष्ट नहीं है , लेकिन फिर कंसोल आउटपुट दिखाता है कि इसे संतुष्ट होना चाहिए था। volatileयहाँ की कमी का अर्थ है कि मतदान धागा फंस सकता है, लेकिन यह स्पष्ट रूप से यहाँ समस्या नहीं है।
इरमा के। पिजारो

21
@ जॉनकोइलस: असली कोड (जो जाहिर तौर पर यह नहीं है) में 100% परीक्षण कवरेज और हजारों परीक्षण थे, जिनमें से बहुतों ने हजारों विभिन्न आदेशों और क्रमपरिवर्तन में चीजों का परीक्षण किया था ... परीक्षण जादुई रूप से नोंडेमैटेरिस्टिक की वजह से हर किनारे के मामले को नहीं खोजता है JIT / कैश / अनुसूचक। असली समस्या यह है कि इस कोड को लिखने वाले डेवलपर को पता नहीं था कि ऑब्जेक्ट का उपयोग करने से पहले निर्माण नहीं होता है। ध्यान दें कि खाली को हटाने synchronizedसे बग नहीं होता है? ऐसा इसलिए है क्योंकि मुझे बेतरतीब ढंग से कोड लिखना था जब तक कि मुझे एक ऐसा नहीं मिला जो इस व्यवहार को सामान्य रूप से पुन: पेश करेगा।
डॉग

जवाबों:


140

जाहिर है कि करंटपॉस को लिखने से पहले ऐसा नहीं होता है, लेकिन मैं यह नहीं देखता कि यह मुद्दा कैसे हो सकता है।

currentPos = new Point(currentPos.x+1, currentPos.y+1);करने के लिए मूलभूत मूल्यों लेखन सहित कुछ चीजें, करता है xऔर y(0) और उसके बाद निर्माता में अपने प्रारंभिक मान लेखन। चूँकि आपकी वस्तु सुरक्षित रूप से प्रकाशित नहीं हुई है, इसलिए इन 4 राइट्स ऑपरेशन्स को कंपाइलर / JVM द्वारा आसानी से रीऑर्डर किया जा सकता है।

तो पढ़ने के धागे के दृष्टिकोण से, यह xअपने नए मूल्य के साथ पढ़ने के लिए एक कानूनी निष्पादन है लेकिन yउदाहरण के लिए 0 के अपने डिफ़ॉल्ट मूल्य के साथ। जब तक आप printlnस्टेटमेंट तक पहुँचते हैं (जो वैसे भी सिंक्रनाइज़ है और इसलिए रीड ऑपरेशन्स को प्रभावित करता है), चर में उनके प्रारंभिक मूल्य होते हैं और प्रोग्राम अपेक्षित मानों को प्रिंट करता है।

मार्किंग के currentPosरूप में volatileसुरक्षित प्रकाशन सुनिश्चित करेगा क्योंकि आपकी वस्तु प्रभावी रूप से अपरिवर्तनीय है - यदि आपके वास्तविक उपयोग के मामले में निर्माण के बाद ऑब्जेक्ट को उत्परिवर्तित किया जाता है, तो volatileगारंटी पर्याप्त नहीं होगी और आप फिर से एक असंगत वस्तु देख सकते हैं।

वैकल्पिक रूप से, आप Pointअपरिवर्तनीय बना सकते हैं जो उपयोग किए बिना भी सुरक्षित प्रकाशन सुनिश्चित करेगा volatile। अपरिवर्तनीयता प्राप्त करने के लिए, आपको बस चिह्नित xऔर yअंतिम करने की आवश्यकता है ।

एक साइड नोट के रूप में और जैसा कि पहले ही उल्लेख synchronized(this) {}किया गया है, जेवीएम द्वारा नो-ऑप के रूप में माना जा सकता है (मुझे लगता है कि आपने इसे व्यवहार को पुन: पेश करने के लिए शामिल किया था)।


4
मुझे यकीन नहीं है, लेकिन x और y फाइनल करने का एक ही असर नहीं होगा, मेमोरी बैरियर से बचना?
माइकल बॉकलिंग

3
एक सरल डिजाइन एक अपरिवर्तनीय बिंदु वस्तु है जो निर्माण पर आक्रमणकारियों का परीक्षण करती है। इसलिए आप कभी भी एक खतरनाक कॉन्फ़िगरेशन को प्रकाशित करने का जोखिम नहीं उठाते हैं।
रॉन

@ बॉडी कैसिनो वास्तव में - मैंने इसे जोड़ा है। ईमानदार होने के लिए मुझे 3 महीने पहले पूरी चर्चा याद नहीं है (अंतिम का उपयोग करके टिप्पणियों में प्रस्तावित किया गया था, इसलिए यह सुनिश्चित नहीं था कि मैंने इसे एक विकल्प के रूप में शामिल क्यों नहीं किया)।
एसिलियास

2
अपरिवर्तनीयता स्वयं सुरक्षित प्रकाशन की गारंटी नहीं देती है (यदि x y निजी थे लेकिन केवल गेटर्स के साथ उजागर होते हैं, तो वही प्रकाशन समस्या अभी भी मौजूद होगी)। अंतिम या अस्थिर इसकी गारंटी देता है। मैं अस्थिर पर अंतिम पसंद करेंगे।
स्टीव कुओ

@SteveKuo अपरिवर्तनीयता को अंतिम की आवश्यकता होती है - अंतिम के बिना, सबसे अच्छा आप प्राप्त कर सकते हैं प्रभावी अपरिवर्तनीयता है जिसमें समान शब्दार्थ नहीं है।
21

29

चूंकि currentPosइसे धागे के बाहर बदला जा रहा है, इसलिए इसे इस प्रकार चिह्नित किया जाना चाहिए volatile:

static volatile Point currentPos = new Point(1,2);

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

यदि आप उनमें से तुलना और बाद के प्रदर्शन में उपयोग के लिए केवल एक बार थ्रेड के भीतर मान पढ़ते हैं, तो परिणाम बहुत भिन्न दिखाई देते हैं। जब मैं निम्न कार्य xके रूप में हमेशा प्रदर्शित करता है 1और yके बीच भिन्न होता है 0और कुछ बड़े पूर्णांक। मुझे लगता है कि इस बिंदु पर इसका व्यवहार volatileकीवर्ड के बिना कुछ अपरिभाषित है और यह संभव है कि कोड का जेआईटी संकलन इस तरह से कार्य कर रहा है। इसके अलावा अगर मैं खाली synchronized(this) {}ब्लॉक पर टिप्पणी करता हूं तो कोड भी काम करता है और मुझे संदेह है क्योंकि यह लॉकिंग पर्याप्त विलंब का कारण बनता है currentPosऔर इसके क्षेत्र कैश से उपयोग किए जाने के बजाय फिर से व्यवस्थित होते हैं।

int x = p.x + 1;
int y = p.y;

if (x != y) {
    System.out.println(x+" "+y);
    System.exit(1);
}

2
हाँ, और मैं भी सब कुछ चारों ओर एक ताला लगा सकता है। अापका नजरिया क्या है?
डॉग

मैंने के उपयोग के लिए कुछ अतिरिक्त स्पष्टीकरण जोड़ा volatile
एड प्लेज़

19

आपके पास साधारण मेमोरी, 'प्रपोज़' रेफरेंस और पॉइंट ऑब्जेक्ट और इसके पीछे के फ़ील्ड, बिना सिंक्रनाइज़ेशन के 2 थ्रेड्स के बीच साझा किए गए हैं। इस प्रकार, मुख्य थ्रेड में इस मेमोरी में होने वाले राइट्स और बनाए गए थ्रेड में रीड के बीच कोई परिभाषित ऑर्डर नहीं है (इसे टी कहते हैं)।

मुख्य सूत्र निम्नलिखित लिख रहा है (बिंदु के प्रारंभिक सेटअप को अनदेखा करते हुए, px और py में डिफ़ॉल्ट मान होंगे):

  • to px
  • to py
  • प्रस्तुत करना

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

तो T कर रहा है:

  1. पी के लिए प्रस्ताव प्रस्तुत करता है
  2. px और py पढ़ें (या तो क्रम में)
  3. तुलना करें, और शाखा लें
  4. px और py (या तो ऑर्डर) पढ़ें और System.out.println पर कॉल करें

यह देखते हुए वहाँ मुख्य में लेखन के बीच कोई आदेश देने के रिश्तों है, और पढ़ने टी में, वहाँ स्पष्ट रूप से कई मायनों इस अपने परिणाम का उत्पादन कर सकते, के रूप में टी मुख्य के currentpos को लिखने में देख सकते हैं इससे पहले कि currentpos.y या currentpos.x को लिखता है:

  1. यह currentpos.x को पहले पढ़ता है, इससे पहले कि x लेखन हुआ है - 0 प्राप्त करता है, फिर currentpos.y पढ़ता है इससे पहले कि y लेखन हुआ है - हो जाता है 0. evals to true। T. System.out.println को लेखन दिखाई देता है।
  2. यह पहले लिखता है, तब x लिखता है, उसके बाद सबसे पहले पढ़ता है। फिर पढ़ता है। इससे पहले कि आप लिखते हैं- y हो गया है। टी ... आदि के लिए दृश्यमान हो जाते हैं।
  3. यह currentpos.y पढ़ता है। पहले, y लिखने के बाद (0) हुआ है, तो x लिखने के बाद currentpos.x पढ़ता है, यह सही है। आदि।

और इसी तरह ... यहाँ कई डेटा दौड़ हैं।

मुझे संदेह है कि यहां की त्रुटिपूर्ण धारणा यह सोच रही है कि इस पंक्ति के परिणाम को थ्रेड को निष्पादित करने के कार्यक्रम क्रम में सभी थ्रेड्स में दिखाई देते हैं:

currentPos = new Point(currentPos.x+1, currentPos.y+1);

जावा ऐसी कोई गारंटी नहीं देता है (यह प्रदर्शन के लिए भयानक होगा)। यदि आपके प्रोग्राम को अन्य थ्रेड्स में रीड्स के सापेक्ष लिखने की गारंटी देने की आवश्यकता है, तो कुछ और जोड़ा जाना चाहिए। अन्य लोगों ने सुझाव दिया है कि x, y फ़ील्ड्स को अंतिम रूप दें, या वैकल्पिक रूप से करंट को अस्थिर करें।

  • यदि आप x, y फ़ील्ड्स को अंतिम बनाते हैं, तो जावा गारंटी देता है कि उनके मानों के लेखन को कंस्ट्रक्टर रिटर्न से पहले, सभी थ्रेड्स में देखा जाएगा। इस प्रकार, जैसा कि प्रस्तुत करने का असाइनमेंट कंस्ट्रक्टर के बाद है, टी थ्रेड को सही क्रम में लिखने को देखने की गारंटी है।
  • यदि आप करंटपॉइंट को अस्थिर बनाते हैं, तो जावा गारंटी देता है कि यह एक सिंक्रोनाइज़ेशन पॉइंट है जो कुल-ऑर्डर किए गए अन्य सिंक्रोनाइज़ेशन पॉइंट होंगे। जैसा कि मुख्य में लिखते हैं x और y को लिखने के लिए प्रेजेंटेशन से पहले होना चाहिए, फिर किसी अन्य थ्रेड में करंट के रीड को भी x, y के राइट्स को देखना होगा जो पहले हुआ था।

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

Gory विवरणों के लिए जावा भाषा के अध्याय 17 का अध्याय देखें: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

(आरंभिक उत्तर ने एक कमजोर मेमोरी मॉडल मान लिया, क्योंकि मुझे यकीन नहीं था कि जेएलएस की गारंटी अस्थिरता पर्याप्त थी। उत्तर को एसाइलिस से टिप्पणी को प्रतिबिंबित करने के लिए संपादित किया गया है, यह दर्शाता है कि जावा मॉडल अधिक मजबूत है - ऐसा होता है-इससे पहले सकर्मक है - और वर्तमान पर अस्थिरता भी पर्याप्त है। )।


2
मेरी राय में यह सबसे अच्छा स्पष्टीकरण है। आपका बहुत बहुत धन्यवाद!
आकाश

1
@ आकाशी लेकिन अस्थिरता के शब्दार्थ पर गलत। अस्थिर गारंटी जो एक अस्थिर चर को पढ़ता है, वह एक अस्थिर चर के नवीनतम उपलब्ध लेखन और साथ ही किसी भी पूर्ववर्ती लेखन को देखेगा । इस स्थिति में, यदि currentPosइसे अस्थिर बनाया जाता है, तो असाइनमेंट currentPosऑब्जेक्ट के सुरक्षित प्रकाशन के साथ-साथ उसके सदस्यों को भी सुनिश्चित करता है , भले ही वे स्वयं ही अस्थिर क्यों न हों।
assylias

ठीक है, मैं कह रहा था कि मैं अपने लिए नहीं, ठीक से देख सकता हूँ कि कैसे जेएलएस ने गारंटी दी कि वाष्पशील ने अन्य, सामान्य रीड और राइट के साथ एक अवरोध का गठन किया। तकनीकी रूप से, मैं उस पर गलत नहीं हो सकता;) जब यह मेमोरी मॉडल की बात आती है, तो यह मान लेना जरूरी है कि ऑर्डर देने की गारंटी नहीं है और गलत (आप अभी भी सुरक्षित हैं) अन्य तरीकों से इधर-उधर गलत और असुरक्षित हैं। यह बहुत अच्छा है अगर अस्थिरता उस गारंटी को प्रदान करती है। क्या आप बता सकते हैं कि जेएलएस के 17 सदस्य इसे कैसे प्रदान करते हैं?
पौल

2
संक्षेप में, में Point currentPos = new Point(x, y), आपके पास 3 लिखते हैं: (w1) this.x = x, (w2) this.y = yऔर (w3) currentPos = the new point। प्रोग्राम ऑर्डर गारंटी देता है कि hb (w1, w3) और hb (w2, w3)। बाद में कार्यक्रम में आपने पढ़ा (आर 1) currentPos। यदि currentPosअस्थिर नहीं है, तो r1 और w1, w2, w3 के बीच कोई hb नहीं है, इसलिए r1 उनमें से किसी भी (या कोई भी) का निरीक्षण कर सकता है। अस्थिर के साथ, आप hb (w3, r1) का परिचय देते हैं। और hb का रिश्ता सकर्मक होता है इसलिए आप hb (w1, r1) और hb (w2, r1) भी पेश करते हैं। यह संक्षेप में जावा कंजिरेन्सी प्रैक्टिस (3.5.3। सुरक्षित प्रकाशन मुहावरों) में दिया गया है।
अस्सीलास

2
आह, अगर hb उस तरह से सकर्मक है, तो यह एक मजबूत पर्याप्त 'बाधा' है, हाँ। मुझे कहना है, यह निर्धारित करना आसान नहीं है कि जेएलएस की 17.4.5 एचबी को उस संपत्ति को परिभाषित करती है। यह निश्चित रूप से 17.4.5 की शुरुआत के पास दी गई संपत्तियों की सूची में नहीं है। कुछ व्याख्यात्मक नोटों के बाद ट्रांज़िटिव क्लोजर का केवल और अधिक नीचे उल्लेख किया गया है! वैसे भी, पता करने के लिए अच्छा है, उत्तर के लिए धन्यवाद! :)। नोट: मैं एसिलियास की टिप्पणी को प्रतिबिंबित करने के लिए अपने उत्तर को अपडेट करूंगा।
पौल

-2

आप एक ऑब्जेक्ट का उपयोग लिखने और पढ़ने को सिंक्रनाइज़ करने के लिए कर सकते हैं। अन्यथा, जैसा कि दूसरों ने पहले कहा था, currentPos के लिए एक लेख दो रीड्स p.x + 1 और py के बीच में होगा

new Thread() {
    void f(Point p) {
        if (p.x+1 != p.y) {
            System.out.println(p.x+" "+p.y);
            System.exit(1);
        }
    }
    @Override
    public void run() {
        while (currentPos == null);
        while (true)
            f(currentPos);
    }
}.start();
Object sem = new Object();
while (true) {
    synchronized(sem) {
        currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}

वास्तव में यह काम करता है। अपने पहले प्रयास में मैंने रीड को सिंक्रोनाइज़्ड ब्लॉक के अंदर रखा, लेकिन बाद में मैंने महसूस किया कि यह वास्तव में आवश्यक नहीं था।
जर्मनो फ्रोंज़ा

1
-1 जेवीएम यह साबित कर सकता है कि semसाझा नहीं किया गया है और सिंक्रनाइज़ स्टेटमेंट को नो-ऑप माना जाता है ... इस तथ्य को हल करने वाला तथ्य शुद्ध भाग्य है।
अस्सलाइस

4
मुझे मल्टी थ्रेडेड प्रोग्रामिंग से नफरत है, अब तक बहुत सी चीजें किस्मत की वजह से काम करती हैं।
जोनाथन एलन

-3

आप दो बार करंटपॉइंट एक्सेस कर रहे हैं, और इस बात की कोई गारंटी नहीं है कि यह उन दो एक्सेसों के बीच अपडेट नहीं है।

उदाहरण के लिए:

  1. x = 10, y = 11
  2. कार्यकर्ता धागा 10 के रूप में px का मूल्यांकन करता है
  3. मुख्य थ्रेड अपडेट को निष्पादित करता है, अब x = 11 और y = 12 है
  4. वर्कर थ्रेड 12 के रूप में पाई का मूल्यांकन करता है
  5. वर्कर थ्रेड ने नोटिस किया कि 10 + 1! = 12, इसलिए प्रिंट और बाहर।

आप अनिवार्य रूप से दो अलग-अलग बिंदुओं की तुलना कर रहे हैं ।

ध्यान दें कि करंटपोस को अस्थिर बनाने से भी आप इसकी रक्षा नहीं करेंगे, क्योंकि यह वर्कर थ्रेड द्वारा दो अलग-अलग रीड हैं।

A जोड़ें

boolean IsValid() { return x+1 == y; }

अपने अंक वर्ग के लिए विधि। यह सुनिश्चित करेगा कि x + 1 == y की जाँच करते समय currentPos के केवल एक मूल्य का उपयोग किया जाता है।


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