Java Language
जावा कमज़ोरियाँ - थ्रेड्स और कॉन्सैरेसी
खोज…
नुकसान: प्रतीक्षा का गलत उपयोग () / सूचना ()
तरीके object.wait()
, object.notify()
और object.notifyAll()
बहुत विशिष्ट तरीके से उपयोग किए जाने वाले हैं। (देखें http://stackoverflow.com/documentation/java/5409/wait-notify#t=20160811161648303307 )
"लॉस्ट नोटिफिकेशन" समस्या
एक सामान्य शुरुआत गलती बिना शर्त के कॉल करने की है। object.wait()
private final Object lock = new Object();
public void myConsumer() {
synchronized (lock) {
lock.wait(); // DON'T DO THIS!!
}
doSomething();
}
यह गलत है इसका कारण यह है कि यह lock.notify()
करने के लिए किसी अन्य थ्रेड पर निर्भर करता है। lock.notify()
या lock.notifyAll()
, लेकिन कुछ भी गारंटी नहीं देता है कि दूसरे थ्रेड ने उस कॉल को lock.wait()
नामक उपभोक्ता थ्रेड से पहले नहीं किया है।
lock.notify()
और lock.notifyAll()
कुछ भी नहीं करते हैं अगर कुछ अन्य धागा पहले से ही अधिसूचना के लिए इंतजार नहीं कर रहा है। इस उदाहरण में myConsumer()
को कॉल करने myConsumer()
थ्रेड हमेशा के लिए हैंग हो जाएगा यदि अधिसूचना को पकड़ने में बहुत देर हो गई है।
"अवैध मॉनिटर राज्य" बग
यदि आप लॉक पकड़े बिना किसी ऑब्जेक्ट पर wait()
या notify()
, तो JVM IllegalMonitorStateException
को फेंक IllegalMonitorStateException
।
public void myConsumer() {
lock.wait(); // throws exception
consume();
}
public void myProducer() {
produce();
lock.notify(); // throws exception
}
( wait()
लिए डिजाइन wait()
/ notify()
आवश्यकता है कि ताला आयोजित किया जाता है क्योंकि यह प्रणालीगत दौड़ की स्थिति से बचने के लिए आवश्यक है। यदि लॉक किए बिना wait()
या notify()
को कॉल wait()
संभव था, तो इसे लागू करना असंभव होगा इन प्राथमिकताओं के लिए प्राथमिक उपयोग-मामला: एक स्थिति होने की प्रतीक्षा कर रहा है।)
प्रतीक्षा करें / सूचित करें निम्न स्तर है
wait()
और notify()
साथ समस्याओं से बचने का सबसे अच्छा तरीका उनका उपयोग नहीं करना है। अधिकांश सिंक्रनाइज़ेशन समस्याओं को java.utils.concurrent
पैकेज में उपलब्ध उच्च-स्तरीय सिंक्रोनाइज़ेशन ऑब्जेक्ट्स (कतार, अवरोध, सेमीफ़ोर्स, आदि) का उपयोग करके हल किया जा सकता है।
नुकसान - 'java.lang.Thread' का विस्तार
Thread
वर्ग के लिए javadoc एक धागे को परिभाषित करने और उपयोग करने के दो तरीके दिखाता है:
कस्टम थ्रेड क्लास का उपयोग करना:
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
PrimeThread p = new PrimeThread(143);
p.start();
एक Runnable
का उपयोग करना:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
(स्रोत: java.lang.Thread
javadoc ।)
कस्टम थ्रेड क्लास दृष्टिकोण काम करता है, लेकिन इसमें कुछ समस्याएं हैं:
PrimeThread
को एक संदर्भ में उपयोग करना अजीब है जो एक क्लासिक थ्रेड पूल, एक निष्पादक, या फ़ॉर्क्जॉइन फ्रेमवर्क का उपयोग करता है। (यह असंभव है क्योंकि नहीं है,PrimeThread
परोक्ष रूप से लागू करता हैRunnable
, लेकिन एक कस्टम का उपयोग करThread
वर्ग एक के रूप मेंRunnable
निश्चित रूप से अनाड़ी है, और व्यवहार्य नहीं हो सकता है ... वर्ग के अन्य पहलुओं पर निर्भर करता है।)अन्य तरीकों में गलतियों के लिए अधिक अवसर है। उदाहरण के लिए, यदि आप एक घोषित
PrimeThread.start()
को सौंपने के बिनाThread.start()
, आप एक "सूत्र" वर्तमान धागे पर चलाए गए के साथ समाप्त होगा।
थ्रेड लॉजिक को Runnable
में डालने का दृष्टिकोण इन समस्याओं से बचा जाता है। वास्तव में, यदि आप Runnable
को लागू करने के लिए एक अनाम वर्ग (जावा 1.1 आगे) का उपयोग करते हैं, तो परिणाम अधिक रसीला है, और ऊपर दिए गए उदाहरणों की तुलना में अधिक पठनीय है।
final long minPrime = ...
new Thread(new Runnable() {
public void run() {
// compute primes larger than minPrime
. . .
}
}.start();
लैम्ब्डा एक्सप्रेशन (जावा 8 आगे) के साथ, उपरोक्त उदाहरण और भी सुरुचिपूर्ण हो जाएगा:
final long minPrime = ...
new Thread(() -> {
// compute primes larger than minPrime
. . .
}).start();
नुकसान - बहुत सारे धागे एक आवेदन को धीमा कर देते हैं।
बहुत से लोग जो मल्टी-थ्रेडिंग में नए हैं, वे सोचते हैं कि थ्रेड्स का उपयोग करने से स्वचालित रूप से एक एप्लिकेशन तेजी से आगे बढ़ता है। वास्तव में, यह उससे कहीं अधिक जटिल है। लेकिन एक बात जो हम निश्चित रूप से बता सकते हैं, वह यह है कि किसी भी कंप्यूटर के लिए एक ही समय में चलने वाले थ्रेड्स की संख्या की सीमा होती है:
- कंप्यूटर में कोर (या हाइपरथ्रेड ) की एक निश्चित संख्या होती है।
- जावा थ्रेड को चलाने के लिए किसी कोर या हाइपरथ्रेड पर शेड्यूल करना पड़ता है।
- यदि उपलब्ध (उपलब्ध) कोर / हाइपरथ्रेड्स से अधिक चलने योग्य जावा थ्रेड्स हैं, तो उनमें से कुछ का इंतजार करना होगा।
यह हमें बताता है कि अधिक से बस बनाने और अधिक जावा धागे आवेदन तेजी से और तेजी से जाने नहीं कर सकते। लेकिन अन्य विचार भी हैं:
प्रत्येक थ्रेड को अपने थ्रेड स्टैक के लिए ऑफ-हाइप मेमोरी क्षेत्र की आवश्यकता होती है। विशिष्ट (डिफ़ॉल्ट) थ्रेड स्टैक आकार 512Kbytes या 1Mbytes है। यदि आपके पास महत्वपूर्ण संख्या में थ्रेड हैं, तो मेमोरी का उपयोग महत्वपूर्ण हो सकता है।
प्रत्येक सक्रिय धागा ढेर में कई वस्तुओं को संदर्भित करेगा। यह पहुंच योग्य वस्तुओं के कामकाजी सेट को बढ़ाता है, जो कचरा संग्रह और भौतिक स्मृति उपयोग पर प्रभाव डालता है।
थ्रेड्स के बीच स्विच करने का ओवरहेड्स गैर-तुच्छ है। यह आमतौर पर एक थ्रेड शेड्यूलिंग निर्णय लेने के लिए OS कर्नेल स्पेस में एक स्विच में प्रवेश करता है।
थ्रेड सिंक्रोनाइजेशन और इंटर-थ्रेड सिग्नलिंग (उदाहरण के लिए प्रतीक्षा) (, नोटिफ़िकेशन () / नोटिफिकेशन) के ओवरहेड्स महत्वपूर्ण हो सकते हैं ।
आपके आवेदन के विवरण के आधार पर, इन कारकों का आमतौर पर मतलब है कि थ्रेड्स की संख्या के लिए एक "मीठा स्थान" है। इसके अलावा, अधिक थ्रेड्स जोड़ने से न्यूनतम प्रदर्शन में सुधार होता है, और प्रदर्शन को बदतर बना सकता है।
यदि आपका एप्लिकेशन प्रत्येक नए कार्य के लिए बनाता है, तो कार्यभार में अप्रत्याशित वृद्धि (जैसे एक उच्च अनुरोध दर) आपत्तिजनक व्यवहार को जन्म दे सकती है।
इससे निपटने का एक बेहतर तरीका है कि आप बाउंडेड थ्रेड पूल का उपयोग करें जिसका आकार आप नियंत्रित (सांख्यिकीय या गतिशील रूप से) कर सकते हैं। जब बहुत अधिक काम करना होता है, तो एप्लिकेशन को अनुरोधों को कतारबद्ध करने की आवश्यकता होती है। यदि आप एक ExecutorService
उपयोग करते हैं, तो यह थ्रेड पूल प्रबंधन और कार्य कतार का ध्यान रखेगा।
नुकसान - धागा निर्माण अपेक्षाकृत महंगा है
इन दो माइक्रो-बेंचमार्क पर विचार करें:
पहला बेंचमार्क बस थ्रेड्स बनाता है, शुरू करता है और जुड़ता है। थ्रेड का Runnable
कोई काम नहीं करता है।
public class ThreadTest {
public static void main(String[] args) throws Exception {
while (true) {
long start = System.nanoTime();
for (int i = 0; i < 100_000; i++) {
Thread t = new Thread(new Runnable() {
public void run() {
}});
t.start();
t.join();
}
long end = System.nanoTime();
System.out.println((end - start) / 100_000.0);
}
}
}
$ java ThreadTest
34627.91355
33596.66021
33661.19084
33699.44895
33603.097
33759.3928
33671.5719
33619.46809
33679.92508
33500.32862
33409.70188
33475.70541
33925.87848
33672.89529
^C
64 बिट जावा 8 u101 के साथ एक सामान्य आधुनिक पीसी पर चलने वाले लिनक्स पर, यह बेंचमार्क 33.6 और 33.9 माइक्रोसेकंड के बीच के थ्रेड को बनाने, शुरू करने और शामिल होने के लिए लिया गया एक औसत समय दिखाता है।
दूसरा बेंचमार्क पहले के बराबर होता है, लेकिन कार्यों को प्रस्तुत करने के लिए एक ExecutorService
का उपयोग करता है और कार्य के अंत के साथ एक Future
लिए बेहतर है।
import java.util.concurrent.*;
public class ExecutorTest {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
while (true) {
long start = System.nanoTime();
for (int i = 0; i < 100_000; i++) {
Future<?> future = exec.submit(new Runnable() {
public void run() {
}
});
future.get();
}
long end = System.nanoTime();
System.out.println((end - start) / 100_000.0);
}
}
}
$ java ExecutorTest
6714.66053
5418.24901
5571.65213
5307.83651
5294.44132
5370.69978
5291.83493
5386.23932
5384.06842
5293.14126
5445.17405
5389.70685
^C
जैसा कि आप देख सकते हैं, औसत 5.3 और 5.6 माइक्रोसेकंड के बीच है।
जबकि वास्तविक समय कई कारकों पर निर्भर करेगा, इन दोनों परिणामों के बीच का अंतर महत्वपूर्ण है। थ्रेड्स को रीसायकल करने के लिए थ्रेड पूल का उपयोग करने से स्पष्ट रूप से तेज़ होता है कि नए थ्रेड बनाने के लिए।
नुकसान: साझा किए गए चर को उचित सिंक्रनाइज़ेशन की आवश्यकता होती है
इस उदाहरण पर विचार करें:
public class ThreadTest implements Runnable {
private boolean stop = false;
public void run() {
long counter = 0;
while (!stop) {
counter = counter + 1;
}
System.out.println("Counted " + counter);
}
public static void main(String[] args) {
ThreadTest tt = new ThreadTest();
new Thread(tt).start(); // Create and start child thread
Thread.sleep(1000);
tt.stop = true; // Tell child thread to stop.
}
}
इस कार्यक्रम का इरादा एक धागा शुरू करने का इरादा है, इसे 1000 मिलीसेकंड के लिए चलने दें, और फिर stop
फ्लैग सेट करके इसे रोक stop
।
क्या यह इच्छानुसार काम करेगा?
शायद हां, शायद नहीं।
main
विधि के वापस आने पर कोई एप्लिकेशन आवश्यक रूप से बंद नहीं होता है। यदि कोई अन्य थ्रेड बनाया गया है, और उस थ्रेड को डेमन थ्रेड के रूप में चिह्नित नहीं किया गया है, तो मुख्य थ्रेड समाप्त होने के बाद अनुप्रयोग चलता रहेगा। इस उदाहरण में, इसका मतलब है कि आवेदन तब तक चलता रहेगा जब तक कि बच्चा धागा समाप्त नहीं हो जाता। यह तब होता है जब tt.stop
सही पर सेट होता true
।
लेकिन यह वास्तव में कड़ाई से सच नहीं है। वास्तव में, यह मूल्य true
होने के बाद stop
अवलोकन के बाद बच्चे का धागा बंद हो जाएगा। क्या ऐसा होगा? शायद हां, शायद नहीं।
जावा लैंग्वेज स्पेसिफिकेशन गारंटी देता है कि मेमोरी और थ्रेड में किए गए लेखन उस थ्रेड को दिखाई देते हैं, जैसा कि सोर्स कोड में स्टेटमेंट्स के अनुसार है। हालाँकि, सामान्य तौर पर, यह गारंटी नहीं दी जाती है जब एक धागा लिखता है और दूसरा धागा (बाद में) पढ़ता है। गारंटीकृत दृश्यता प्राप्त करने के लिए, किसी लेखन और उसके बाद के पढ़ने के बीच होने वाले संबंधों से पहले होने वाली श्रृंखला होनी चाहिए। ऊपर दिए गए उदाहरण में, stop
फ़्लैग के लिए अद्यतन के लिए ऐसी कोई श्रृंखला नहीं है, और इसलिए यह गारंटी नहीं है कि बच्चा थ्रेड stop
परिवर्तन को true
देखेगा।
(लेखकों पर ध्यान दें: गहरी तकनीकी जानकारी में जाने के लिए जावा मेमोरी मॉडल पर एक अलग विषय होना चाहिए।)
हम समस्या को कैसे ठीक करते हैं?
इस स्थिति में, यह सुनिश्चित करने के दो सरल तरीके हैं कि stop
अपडेट दिखाई दे:
volatile
होने की घोषणाstop
; अर्थातprivate volatile boolean stop = false;
एक
volatile
चर के लिए, जेएलएस निर्दिष्ट करता है कि एक थ्रेड द्वारा एक लेखन और बाद में एक दूसरे थ्रेड द्वारा पढ़े जाने के बीच संबंध होता है ।निम्नानुसार सिंक्रनाइज़ करने के लिए एक म्यूटेक्स का उपयोग करें:
public class ThreadTest implements Runnable {
private boolean stop = false;
public void run() {
long counter = 0;
while (true) {
synchronize (this) {
if (stop) {
break;
}
}
counter = counter + 1;
}
System.out.println("Counted " + counter);
}
public static void main(String[] args) {
ThreadTest tt = new ThreadTest();
new Thread(tt).start(); // Create and start child thread
Thread.sleep(1000);
synchronize (tt) {
tt.stop = true; // Tell child thread to stop.
}
}
}
यह सुनिश्चित करने के अलावा कि पारस्परिक बहिष्करण है, जेएलएस निर्दिष्ट करता है कि एक थ्रेड में एक म्यूटेक्स जारी करने और दूसरे थ्रेड में एक ही म्यूटेक्स प्राप्त करने के बीच संबंध होता है ।
लेकिन काम परमाणु नहीं है?
हाँ यही है!
हालांकि, उस तथ्य का मतलब यह नहीं है कि अद्यतन के प्रभाव सभी थ्रेड्स के लिए एक साथ दिखाई देंगे। होने से पहले ही संबंधों की एक उचित श्रृंखला की गारंटी होगी।
उन्होंने ऐसा क्यों किया?
मेमोरी मॉडल को चुनौती देने वाले पहली बार जावा में मल्टी-थ्रेडेड प्रोग्रामिंग करने वाले प्रोग्रामर। कार्यक्रम एक अनपेक्षित तरीके से व्यवहार करते हैं क्योंकि स्वाभाविक अपेक्षा यह है कि लेखन समान रूप से दिखाई देते हैं। तो क्यों जावा डिजाइनर इस तरह से मेमोरी मॉडल डिजाइन करते हैं।
यह वास्तव में प्रदर्शन और उपयोग में आसानी (प्रोग्रामर के लिए) के बीच एक समझौता है।
एक आधुनिक कंप्यूटर वास्तुकला में व्यक्तिगत रजिस्टर सेट के साथ कई प्रोसेसर (कोर) होते हैं। मुख्य मेमोरी या तो सभी प्रोसेसर या प्रोसेसर के समूहों के लिए सुलभ है। आधुनिक कंप्यूटर हार्डवेयर की एक और संपत्ति यह है कि रजिस्टरों तक पहुंच आमतौर पर मुख्य मेमोरी तक पहुंच की तुलना में तेजी से परिमाण के आदेश हैं। जैसे-जैसे कोर की संख्या बढ़ती जाती है, यह देखना आसान होता है कि मुख्य मेमोरी में पढ़ना और लिखना सिस्टम का मुख्य प्रदर्शन अड़चन बन सकता है।
यह बेमेल प्रोसेसर कोर और मुख्य मेमोरी के बीच मेमोरी कैशिंग के एक या अधिक स्तरों को लागू करके संबोधित किया जाता है। प्रत्येक कोर अपने कैश के माध्यम से मेमोरी सेल्स को एक्सेस करता है। आम तौर पर, पढ़ी जाने वाली मुख्य मेमोरी केवल तब होती है जब कैश मिस होता है, और मुख्य मेमोरी राइट केवल तब होता है जब कैश लाइन को स्कैन करना पड़ता है। ऐसे एप्लिकेशन के लिए जहां मेमोरी के प्रत्येक कोर का कार्य स्थान उसके कैश में फिट होगा, कोर स्पीड अब मुख्य मेमोरी स्पीड / बैंडविड्थ तक सीमित नहीं है।
लेकिन यह हमें एक नई समस्या देता है जब कई कोर साझा चर पढ़ और लिख रहे हैं। एक चर का नवीनतम संस्करण एक कोर के कैश में बैठ सकता है। जब तक कि कोर मुख्य मेमोरी में कैश लाइन को फ्लश नहीं करता है, और अन्य कोर पुराने संस्करणों की अपनी कैश्ड कॉपी को अमान्य करते हैं, उनमें से कुछ चर के बासी संस्करणों को देखने के लिए उत्तरदायी हैं। लेकिन अगर कैश को हर बार मेमोरी में फ्लश कर दिया जाता है, तो कैश लिखना होता है ("बस के मामले में" एक अन्य कोर द्वारा पढ़ा गया था) जो मुख्य मेमोरी बैंडविड्थ का अनावश्यक रूप से उपभोग करेगा।
हार्डवेयर इंस्ट्रक्शन सेट स्तर पर उपयोग किए जाने वाले मानक समाधान कैश अमान्यकरण और कैश-राइट के माध्यम से निर्देश प्रदान करना है, और उन्हें कब उपयोग करना है यह तय करने के लिए संकलक पर छोड़ दें।
जावा में लौट रहा है। मेमोरी मॉडल को डिज़ाइन किया गया है ताकि जावा कंपाइलरों को कैश अमान्यकरण जारी करने और निर्देशों के माध्यम से लिखने की आवश्यकता न हो, जहां उन्हें वास्तव में ज़रूरत नहीं है। धारणा यह है कि प्रोग्रामर एक उपयुक्त सिंक्रोनाइज़ेशन तंत्र (जैसे कि आदिम म्यूटेक्स, volatile
, उच्च-स्तरीय संगामिति वर्ग और इसी तरह) का उपयोग करेगा यह इंगित करने के लिए कि उसे मेमोरी दृश्यता की आवश्यकता है। रिलेशन से पहले की अनुपस्थिति में, जावा कंपाइलर यह मानने के लिए स्वतंत्र हैं कि कोई कैश ऑपरेशन (या समान) की आवश्यकता नहीं है।
बहु-थ्रेडेड अनुप्रयोगों के लिए इसके महत्वपूर्ण प्रदर्शन लाभ हैं, लेकिन नकारात्मक पक्ष यह है कि सही बहु-थ्रेडेड अनुप्रयोग लिखना एक साधारण मामला नहीं है। प्रोग्रामर को समझने के लिए वह या वह क्या कर रहा है है।
मैं इसे क्यों नहीं दोहरा सकता?
ऐसे कई कारण हैं जिनकी वजह से इस तरह की समस्याएं पैदा करना मुश्किल है:
जैसा कि ऊपर बताया गया है, मेमोरी विजिबिलिटी की समस्याओं से ठीक से नहीं निपटने का नतीजा आम तौर पर यह होता है कि आपका संकलित एप्लिकेशन मेमोरी कैश को सही तरीके से हैंडल नहीं करता है। हालाँकि, जैसा कि हमने ऊपर बताया था, मेमोरी कैश अक्सर किसी भी तरह से फ्लश हो जाता है।
जब आप हार्डवेयर प्लेटफ़ॉर्म बदलते हैं, तो मेमोरी कैश की विशेषताएँ बदल सकती हैं। यदि आपका एप्लिकेशन सही ढंग से सिंक्रनाइज़ नहीं होता है, तो यह अलग व्यवहार का कारण बन सकता है।
आप सीरेंडिपिटस सिंक्रोनाइज़ेशन के प्रभावों का अवलोकन कर सकते हैं। उदाहरण के लिए, यदि आप ट्रेसप्रिंट्स जोड़ते हैं, तो आमतौर पर I / O स्ट्रीम में पर्दे के पीछे कुछ सिंक्रनाइज़ेशन हो रहा है जो कैश बुश का कारण बनता है। इसलिए ट्रेसप्रिंट्स को जोड़ने से अक्सर एप्लिकेशन अलग व्यवहार करता है।
डीबगर के तहत किसी एप्लिकेशन को चलाने से JIT कंपाइलर द्वारा इसे अलग तरीके से संकलित किया जाता है। ब्रेकपॉइंट्स और सिंगल स्टेपिंग इसे बढ़ाते हैं। ये प्रभाव अक्सर एक आवेदन व्यवहार के तरीके को बदल देंगे।
ये चीजें ऐसी बग बनाती हैं जो अपर्याप्त सिंक्रनाइज़ेशन के कारण होती हैं जिन्हें विशेष रूप से हल करना मुश्किल होता है।