लूप बिना प्रिंट स्टेटमेंट के दूसरे थ्रेड द्वारा बदले गए मूल्य को नहीं देखता है


91

मेरे कोड में मेरे पास एक लूप है जो कुछ राज्य के लिए एक अलग धागे से बदलने की प्रतीक्षा करता है। दूसरा धागा काम करता है, लेकिन मेरा लूप कभी भी बदले हुए मूल्य को नहीं देखता है। यह हमेशा इंतजार करता है। हालांकि, जब मैं एक System.out.printlnबयान को लूप में डालता हूं , तो यह अचानक काम करता है! क्यों?


निम्नलिखित मेरे कोड का एक उदाहरण है:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (pizzaArrived == false) {
            //System.out.println("waiting");
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        pizzaArrived = true;
    }
}

जबकि लूप चल रहा है, मैं चर deliverPizza()सेट करने के लिए एक अलग थ्रेड से कॉल करता हूं pizzaArrived। लेकिन लूप तभी काम करता है जब मैं System.out.println("waiting");स्टेटमेंट को अनसुना करता हूं । क्या चल रहा है?

जवाबों:


152

JVM को यह मानने की अनुमति है कि pizzaArrivedलूप के दौरान अन्य थ्रेड्स वेरिएबल को नहीं बदलते हैं । दूसरे शब्दों में, यह pizzaArrived == falseलूप के बाहर परीक्षण को फहरा सकता है, इसको अनुकूलित करते हुए:

while (pizzaArrived == false) {}

इस मामले में:

if (pizzaArrived == false) while (true) {}

जो एक अनंत लूप है।

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

volatile boolean pizzaArrived = false;

एक वैरिएबल की volatileगारंटी देता है कि अलग-अलग धागे एक-दूसरे के बदलावों के प्रभावों को देखेंगे। यह JVM pizzaArrivedको लूप के बाहर परीक्षण को रोकने या परीक्षण को रोकने से रोकता है । इसके बजाय, उसे हर बार वास्तविक चर का मूल्य पढ़ना चाहिए।

(अधिक औपचारिक रूप से, वैरिएबल तक पहुँच के बीच volatileएक होता है-पहले संबंध बनाता है । इसका मतलब है कि पिज्जा वितरित करने से पहले किया गया एक अन्य कार्य पिज्जा प्राप्त करने वाले धागे को भी दिखाई देता है, भले ही वे अन्य परिवर्तन volatileचर में न हों ।)

सिंक्रनाइज़ किए गए तरीकों का उपयोग मुख्य रूप से पारस्परिक बहिष्करण (एक ही समय में दो चीजों को रोकने) को लागू करने के लिए किया जाता है, लेकिन उनके पास सभी समान प्रभाव volatileभी होते हैं। एक चर को पढ़ने और लिखने के दौरान उनका उपयोग करना अन्य परिवर्तनों को अन्य धागों को दिखाई देने का एक और तरीका है:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (getPizzaArrived() == false) {}
        System.out.println("That was delicious!");
    }

    synchronized boolean getPizzaArrived() {
        return pizzaArrived;
    }

    synchronized void deliverPizza() {
        pizzaArrived = true;
    }
}

एक प्रिंट स्टेटमेंट का प्रभाव

System.outएक PrintStreamवस्तु है। के तरीके PrintStreamइस तरह से सिंक्रनाइज़ हैं:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

सिंक्रनाइज़ेशन pizzaArrivedलूप के दौरान कैश होने से बचाता है । सख्ती से बोलते हुए, दोनों थ्रेड्स को एक ही ऑब्जेक्ट पर यह गारंटी देने के लिए सिंक्रनाइज़ करना चाहिए कि चर में परिवर्तन दिखाई दे रहे हैं। (उदाहरण के लिए, printlnसेटिंग के बाद कॉल करना pizzaArrivedऔर पढ़ने से पहले उसे फिर से कॉल pizzaArrivedकरना सही होगा।) यदि किसी विशेष ऑब्जेक्ट पर केवल एक थ्रेड सिंक्रोनाइज़ करता है, तो JVM को इसे अनदेखा करने की अनुमति है। व्यवहार में, जेवीएम यह साबित करने के लिए पर्याप्त स्मार्ट नहीं है कि printlnसेटिंग के बाद अन्य धागे कॉल नहीं करेंगे pizzaArrived, इसलिए यह मानता है कि वे हो सकते हैं। इसलिए, यदि आप कॉल करते हैं, तो यह लूप के दौरान चर को कैश नहीं कर सकता है System.out.println। यही कारण है कि जब वे एक प्रिंट स्टेटमेंट रखते हैं तो इस काम को पसंद करते हैं, हालांकि यह एक सही फिक्स नहीं है।

उपयोग करना System.outइस आशय का एकमात्र तरीका नहीं है, लेकिन यह एक ऐसा व्यक्ति है जिसे सबसे अधिक बार पता चलता है, जब वे डिबग करने की कोशिश कर रहे हैं कि उनका लूप क्यों काम करता है!


बड़ी समस्या

while (pizzaArrived == false) {}एक व्यस्त-वेट लूप है। यह बुरी बात है! हालांकि यह प्रतीक्षा करता है, यह सीपीयू को हॉग करता है, जो अन्य अनुप्रयोगों को धीमा कर देता है, और सिस्टम के बिजली के उपयोग, तापमान और प्रशंसक गति को बढ़ाता है। आदर्श रूप से, हम प्रतीक्षा करते समय लूप थ्रेड को सोना चाहेंगे, इसलिए यह सीपीयू को हॉग नहीं करता है।

ये करने के कुछ तरीके इस प्रकार हैं:

प्रतीक्षा / सूचना का उपयोग करना

निम्न-स्तरीय समाधान प्रतीक्षा / अधिसूचित विधियों का उपयोग करना हैObject :

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        synchronized (this) {
            while (!pizzaArrived) {
                try {
                    this.wait();
                } catch (InterruptedException e) {}
            }
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        synchronized (this) {
            pizzaArrived = true;
            this.notifyAll();
        }
    }
}

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

किसी ऑब्जेक्ट पर प्रतीक्षा / सूचना देते समय आपको उस ऑब्जेक्ट के सिंक्रोनाइज़ेशन लॉक को पकड़ना चाहिए, जो कि उपरोक्त कोड करता है। आप किसी भी वस्तु का उपयोग कर सकते हैं, जब तक कि आप दोनों धागे एक ही वस्तु का उपयोग न करें: यहाँ मैंने उपयोग किया है this(उदाहरण MyHouse)। आमतौर पर, दो थ्रेड्स एक साथ एक ही ऑब्जेक्ट के सिंक्रनाइज़ किए गए ब्लॉक में प्रवेश करने में सक्षम नहीं होंगे (जो सिंक्रोनाइज़ेशन के उद्देश्य का हिस्सा है) लेकिन यह यहां काम करता है क्योंकि एक थ्रेड अस्थायी रूप से सिंक्रोनाइज़ेशन लॉक को रिलीज़ करता है जब यह wait()विधि के अंदर होता है ।

BlockingQueue

A BlockingQueueका उपयोग उत्पादक-उपभोक्ता कतारों को लागू करने के लिए किया जाता है। "उपभोक्ता" कतार के सामने से आइटम लेते हैं, और "निर्माता" आइटम को पीछे से धक्का देते हैं। एक उदाहरण:

class MyHouse {
    final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();

    void eatFood() throws InterruptedException {
        // take next item from the queue (sleeps while waiting)
        Object food = queue.take();
        // and do something with it
        System.out.println("Eating: " + food);
    }

    void deliverPizza() throws InterruptedException {
        // in producer threads, we push items on to the queue.
        // if there is space in the queue we can return immediately;
        // the consumer thread(s) will get to it later
        queue.put("A delicious pizza");
    }
}

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

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

निष्पादकों

Executors तैयार किए गए BlockingQueues की तरह हैं जो कार्यों को अंजाम देते हैं। उदाहरण:

// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();

Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };

// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish

जानकारी के लिए दस्तावेज़ नहीं देख लिए Executor, ExecutorService, और Executors

घटना से निपटना

UI में कुछ क्लिक करने के लिए उपयोगकर्ता की प्रतीक्षा करते समय लूपिंग करना गलत है। इसके बजाय, UI टूलकिट की ईवेंट हैंडलिंग सुविधाओं का उपयोग करें। उदाहरण के लिए, स्विंग में :

JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
    // This event listener is run when the button is clicked.
    // We don't need to loop while waiting.
    label.setText("Button was clicked");
});

क्योंकि ईवेंट हैंडलर ईवेंट डिस्पैच थ्रेड पर चलता है, ईवेंट हैंडलर में लंबे समय तक काम करना यूआई के साथ अन्य इंटरैक्शन को ब्लॉक करता है जब तक कि काम खत्म नहीं हो जाता। धीमे संचालन को एक नए धागे पर शुरू किया जा सकता है, या उपरोक्त तकनीकों में से एक का उपयोग करके प्रतीक्षा थ्रेड में भेजा जा सकता है (प्रतीक्षा / सूचना, ए BlockingQueue, या Executor)। आप एक का भी उपयोग कर सकते हैं SwingWorker, जो इसके लिए बिल्कुल तैयार किया गया है, और स्वचालित रूप से एक पृष्ठभूमि कार्यकर्ता धागा की आपूर्ति करता है:

JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");

// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {

    // Defines MyWorker as a SwingWorker whose result type is String:
    class MyWorker extends SwingWorker<String,Void> {
        @Override
        public String doInBackground() throws Exception {
            // This method is called on a background thread.
            // You can do long work here without blocking the UI.
            // This is just an example:
            Thread.sleep(5000);
            return "Answer is 42";
        }

        @Override
        protected void done() {
            // This method is called on the Swing thread once the work is done
            String result;
            try {
                result = get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            label.setText(result); // will display "Answer is 42"
        }
    }

    // Start the worker
    new MyWorker().execute();
});

टाइमर

आवधिक क्रियाएं करने के लिए, आप एक का उपयोग कर सकते हैं java.util.Timer। अपनी खुद की टाइमिंग लूप लिखने की तुलना में उपयोग करना आसान है, और शुरू करना और रोकना आसान है। यह डेमो वर्तमान समय को प्रति सेकंड एक बार प्रिंट करता है:

Timer timer = new Timer();
TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println(System.currentTimeMillis());
    }
};
timer.scheduleAtFixedRate(task, 0, 1000);

प्रत्येक java.util.Timerका अपना एक बैकग्राउंड थ्रेड होता है जिसका उपयोग अपने निर्धारित TimerTaskएस को निष्पादित करने के लिए किया जाता है । स्वाभाविक रूप से, थ्रेड कार्यों के बीच सोता है, इसलिए यह सीपीयू को हॉग नहीं करता है।

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

JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
    frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);

दूसरा तरीका

यदि आप मल्टीथ्रेडेड कोड लिख रहे हैं, तो यह देखने के लिए इन पैकेजों में कक्षाओं को देखने लायक है कि क्या उपलब्ध है:

और जावा ट्यूटोरियल्स का कंसीडर सेक्शन भी देखें । मल्टीथ्रेडिंग जटिल है, लेकिन बहुत मदद उपलब्ध है!


बहुत पेशेवर जवाब, यह कोई गलत धारणा है मेरे दिमाग पर छोड़ दिया जाता है पढ़ने के बाद, धन्यवाद
Humoyun अहमद

1
बहुत बढ़िया जवाब। मैं जावा थ्रेड के साथ काफी समय से काम कर रहा हूं और अभी भी यहां कुछ सीखा है ( wait()सिंक्रनाइज़ेशन लॉक जारी करता है!)।
ब्रिम्बोरियम

धन्यवाद, बान! महान जवाब, यह उदाहरण के साथ एक पूर्ण लेख की तरह है! हां, "प्रतीक्षा () सिंक्रोनाइजेशन लॉक" भी पसंद आया
किरील इवानो

java public class ThreadTest { private static boolean flag = false; private static class Reader extends Thread { @Override public void run() { while(flag == false) {} System.out.println(flag); } } public static void main(String[] args) { new Reader().start(); flag = true; } } @ फिर भी, यह कोड pizzaArrived == falseलूप के बाहर परीक्षण को नहीं फहराता है, और लूप को मुख्य धागे से बदला हुआ झंडा देख सकते हैं, क्यों?
गॉसक्लब

1
@gaussclb यदि आपका मतलब है कि आपने एक वर्ग फ़ाइल को विघटित किया है, तो सही। जावा कंपाइलर लगभग कोई अनुकूलन नहीं करता है। उत्थापन जेवीएम द्वारा किया जाता है। आपको मूल मशीन कोड को अलग करना होगा। कोशिश करें: wiki.openjdk.java.net/display/HotSpot/PrintAssembly
Boann
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.