Practical multithreading best practices: Java संस्करण — virtual thread युग की practices
· अद्यतन तिथि: · Go Komura · multithreading, Java, business app, bug investigation, design
संशोधन इतिहास (पहला संस्करण, 2 Aug 2026 को प्रकाशित)
- पहला प्रकाशन
इस लेख का हवाला दें(DOI: 10.5281/zenodo.22175932)
यह लेख Zenodo पर संग्रहीत है। नीचे वह DOI है जो हमेशा नवीनतम संस्करण की ओर ले जाता है, और वह DOI भी जो आपके पढ़े जा रहे संस्करण से बँधा है।
Go Komura (2026). Practical multithreading best practices: Java संस्करण — virtual thread युग की practices. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22175932 https://comcomponent.com/hi/blog/multithreading-best-practices-java/
- DOI (नवीनतम संस्करण)
- 10.5281/zenodo.22175932
- DOI (यह संस्करण)
- 10.5281/zenodo.22175933
«हम Java में एक business batch कार्य parallel करना चाहते हैं।» «हमारे Spring web app में shared cache कभी-कभी खराब हो जाता है।» «हमें new Thread से भरा पुराना Swing app विरासत में मिला।» — Java में JDK 1.0 से भाषा में multithreading बनी हुई है, java.util.concurrent के toolbox तक परिपक्व हुई है, और JDK 21 के virtual threads से concurrent programming की पारंपरिक समझ फिर लिखी है। Tools इतने समृद्ध हैं, इसलिए आप जो tools चुनते हैं वही design की गुणवत्ता बन जाता है।
यह लेख multithreading-व्यवहार श्रृंखला का Java संस्करण है। उन developers के लिए जो Java में business systems, batch कार्य और server applications लिखते हैं, यह multithreading design के सिद्धांतों — thread सीधे कभी न बनाएँ, shared mutable state घटाएँ, locking में discipline, सबसे पहले रोकने का design — को Java के tools पर map करता है (मुख्यतः LTS releases, JDK 21 और बाद), और अगस्त 2026 तक के primary sources पर virtual-thread युग के चुनाव तथा Java-विशिष्ट pitfalls समेटता है। इसे अकेले पढ़ा जा सके, ऐसा लिखा गया है। अन्य भाषाओं पर लागू वही सिद्धांत साथी लेखों «.NET संस्करण», «C++ संस्करण» और «C संस्करण» में हैं।
1. निष्कर्ष पहले
- Business code में
new Threadन लिखें — यही नियम Java में भी लागू होता है। कार्यExecutorServiceको सौंपें और thread lifecycle management library पर छोड़ें।1 - जो कार्य मुख्यतः I/O की wait करते हैं उन्हें virtual threads पर भेजें। JDK 21 में formal virtual threads प्रति कार्य एक इस्तेमाल होते हैं और कभी pool नहीं होने चाहिए। Concurrency
Semaphoreसे सीमित करें, pool size से नहीं।23 - Virtual threads throughput का औज़ार हैं, computation तेज़ करने का नहीं। CPU-bound parallelism अभी भी लगभग core count के platform threads का काम है — स्थिर pool या parallel stream, पहले जैसा।3
private finallock object पर lock करें, या dedicatedReentrantLockपर।synchronized(this)और सार्वजनिक रूप से उजागर object पर lock बाहरी code से टकरा सकता है। Timed acquire (tryLock) चाहिए तोReentrantLock।- JDK 21-23 में
synchronizedblock के अंदर blocking virtual thread pin करता था; JDK 24 (JEP 491) ने ठीक किया। पुरानी चेतावनियों और वर्तमान वास्तविकता में भेद करें।34 volatilevisibility और ordering की guarantee देता है, atomicity की नहीं। Counter के लिएAtomicInteger/LongAdder, combined state के लिए lock।5- Interruption से cooperative stop thread रोकने का एकमात्र सही तरीका है।
Thread.stop/suspend/resumeअबUnsupportedOperationExceptionफेंकते हैं।InterruptedExceptionन निगलें — status restore करें या ऊपर फेंकें।6 ExecutorServiceको दो-चरण pattern से रोकें: shutdown → awaitTermination → shutdownNow।shutdownNowbest-effort है (standard implementation interruption से), इसलिए यह मानता है कि आपके कार्य interruption का जवाब देते हैं।1- Swing का UI विशेष रूप से EDT (Event Dispatch Thread) का है। अन्य threads से update
SwingUtilities.invokeLaterसे माँगें।7
2. Multithreading कठिन क्यों है — race condition, deadlock, और memory model
Multithreading जो समस्याएँ लाती है, भाषा चाहे जो हो, उबालें तो दो तरह की हैं।
Race condition वह bug है जिसमें परिणाम इस बात पर depend करता है कि कई threads किसी code खंड तक किस क्रम में पहुँचते हैं। Classic उदाहरण shared counter है: एक अभिव्यक्ति count++ वास्तव में तीन चरणों में टूटती है — पढ़ो, जोड़ो, वापस लिखो। यदि दो threads इन तीन चरणों में एक साथ प्रवेश करें, एक thread की increment दूसरे के लिखने पर overwrite होकर खो जाती है। हर run पर परिणाम बदलता है, और कौन-सा परिणाम मिलेगा, अनुमान नहीं लग सकता।
sequenceDiagram
participant A as Thread A
participant M as Shared variable count
participant B as Thread B
Note over M: count = 10
A->>M: पढ़ना (10)
B->>M: पढ़ना (10)
A->>A: Local जोड़ (11)
B->>B: Local जोड़ (11)
A->>M: वापस लिखना (11)
B->>M: वापस लिखना (11)
Note over M: दो increment हुईं,<br/>पर count = 11 — Thread A की increment खो गई
चित्र 1: Classic race condition जिसमें shared counter पर increment खो जाती है। यदि count++ के तीन चरणों के दौरान दूसरा thread बीच में आ जाए, तो जो write सबसे बाद होता है वह दूसरे को overwrite कर देता है
Deadlock वह state है जिसमें दो threads प्रत्येक उस lock की wait करते हैं जो दूसरा पकड़े हुए है, इसलिए कोई आगे नहीं बढ़ सकता। Thread A lock 1 पकड़े lock 2 की wait करता है; Thread B lock 2 पकड़े lock 1 की wait करता है — इतना ही दोनों को हमेशा के लिए रोकने के लिए काफी है।
flowchart LR
A["Thread A<br/>Lock 1 पकड़े"] -->|"Lock 2 के मुक्त होने की wait"| B["Thread B<br/>Lock 2 पकड़े"]
B -->|"Lock 1 के मुक्त होने की wait"| A
चित्र 2: Deadlock की circular wait। जिस क्षण wait तीर loop बनाते हैं, उस loop के अंदर हर thread हमेशा के लिए रुक जाता है
दोनों time-dependent हैं। वह interleaving जो development मशीन पर दसियों हज़ार runs में एक बार दिखे, अलग core count और load वाले production server पर रोज़ हो सकता है। «Debugger लगाने पर reproduce बंद» और «log जोड़ने पर गायब» दोनों इसलिए कि observation timing बदल देता है — race bug का classic व्यवहार। ठीक इसलिए इस लेख का हर सिद्धांत सही synchronize करने की चिंता से पहले, synchronization वाले स्थान घटाने की दिशा में है।
2.1. Java-विशिष्ट आधार — memory model और happens-before
उसके ऊपर, Java-विशिष्ट यह है कि shared data कैसे दिखता है Java Memory Model (JMM) के happens-before relationships से परिभाषित होता है।
बिना synchronization shared variable पढ़ना-लिखना C++-शैली «undefined behavior» नहीं बनता, पर वैध रूप से पुराने values दिखते रहना, या writes क्रम से बाहर दिखना हो सकता है। Memory consistency error जिसमें «loop boolean flag देख रहा है, पर दूसरे thread का बदला value कभी नहीं दिखता» JMM द्वारा अनुमत व्यवहार है, JVM bug नहीं।5 इससे बचाने वाले tools वे तंत्र हैं जो happens-before बनाते हैं — synchronized, volatile, और java.util.concurrent की classes। Concurrent collections सही इस्तेमाल करें तो library «update operation और बाद की retrieval के बीच happens-before» की guarantee देती है।8
अर्थात् Java का व्यावहारिक मार्गदर्शन यह है: कच्चे shared variables से चतुराई न करें। Share करने के लिए java.util.concurrent के tools इस्तेमाल करें, और happens-before library बनवाएँ।
3. Thread कैसे बनाएँ — ExecutorService और virtual threads
3.1. कार्य को चलाने के तरीके से अलग करना
ExecutorService Java में «thread स्वयं न बनाएँ» सिद्धांत को ढोता है। यह काम (Runnable / Callable) को इस से अलग करता है कि वह कैसे चले — कितने threads, कौन-सी queue — और thread निर्माण, reuse, disposal library पर छोड़ता है।1
JDK 21 से, कैसे चलाना चुनना सरल binary चुनाव बन गया है।23
flowchart TB
S["कार्य जिसे concurrently चलाना है"] --> Q1{"कार्य को क्या चलाता है?"}
Q1 -->|"मुख्यतः I/O wait<br/>HTTP calls, DB, फ़ाइलें"| VT["Virtual thread<br/>Executors.newVirtualThreadPerTaskExecutor<br/>प्रति कार्य एक, कभी pool नहीं"]
Q1 -->|"CPU-bound computation"| PT["Platform threads का स्थिर pool<br/>Executors.newFixedThreadPool - लगभग core जितने<br/>या parallel stream"]
VT --> LIMIT["बाहरी सेवाओं की concurrency<br/>Semaphore से सीमित करें, pool size से नहीं"]
चित्र 3: JDK 21 से कार्य चलाने का चुनाव। पहले रेखा खींचें — «I/O की wait बदलें, CPU के लिए parallel करें» — फिर I/O-bound काम virtual threads को और CPU-bound काम पारंपरिक pool को सौंपें
CPU-bound पक्ष पर एक सावधानी है। Executors.newFixedThreadPool thread संख्या सीमित करता है, पर उसकी queue unbounded है। लंबे समय चलने वाली सेवा में जहाँ submissions processing से आगे निकलती रहें, केवल threads core count तक सीमित हैं — queue में जमा कार्य और उनके data memory खाते रहते हैं। उस तरह के setup में, या तो ThreadPoolExecutor सीधे इस्तेमाल कर bounded queue प्लस rejection policy configure करें, या submission पक्ष पर Semaphore जैसी admission control रखें ताकि backpressure लगा सकें (खंड 4 की queue चर्चा जैसा ही सिद्धांत)।
3.2. Virtual threads का दुरुपयोग न करें
Virtual threads OS threads से अलग हल्के threads हैं: JDK में blocking operations (standard library में I/O, locking, sleep आदि) के दौरान वे अपना OS thread छोड़ देते हैं, इसलिए एक JVM लाखों चला सकती है। फिर भी वे हर तरह के blocking पर नहीं छोड़ते। यदि virtual thread native code (JNI) या foreign function चलाते हुए block हो, तो वह अपने carrier thread से pin रहता है। जो JDK 24 (JEP 491, नीचे) ने ठीक किया वह synchronized से pinning थी; native सीमा पर pinning रहती है, इसलिए JNI driver या device API से लंबे blocking वाले operations भारी संख्या में virtual threads पर लादना carrier threads खत्म कर देगा। पर official guide ज़ोर देता है, वे «तेज़ threads» नहीं हैं। Code execution गति नहीं बदलती — जो वे देते हैं वह पैमाना (throughput) है।3
इस्तेमाल के तीन discipline हैं।3
- उन्हें pool न करें। Virtual threads सस्ते और disposable हैं; «कार्यों की संख्या = virtual threads की संख्या» सही state है।
newFixedThreadPoolमें virtual threads डालना ग़लती है — रूपtry (var executor = Executors.newVirtualThreadPerTaskExecutor())इस्तेमाल करें। - Concurrency
Semaphoreसे सीमित करें। «बाहरी API से अधिकतम 10 concurrent connections» जैसा अवरोध semaphore से व्यक्त करें, pool size से नहीं। - CPU-bound काम के लिए इस्तेमाल न करें। लगभग core count के platform threads computation parallel करने के लिए पहले की तरह सही औज़ार हैं।
ध्यान दें कि virtual threads के अंदर साधारण synchronous code चलता है। .NET के async/await की तरह code फिर लिखने के बजाय, virtual threads के पीछे design दर्शन यह है कि आप सीधे «प्रति request एक thread» code, अपरिवर्तित, विशाल पैमाने पर चला सकें।2
3.3. आम ग़लतफ़हमी — «pooling की ज़रूरत नहीं» केवल virtual threads पर लागू होता है
«उन्हें pool न करें» discipline को यह न पढ़ें कि «Java में thread pool जैसी चीज़ नहीं (या वह अक्षम है)»। वास्तविकता उलटी है: Java के pools JDK 5 (2004) से standard library का परिपक्व हिस्सा हैं। बारीक configure होने वाला सामान्य pool ThreadPoolExecutor (Executors की विभिन्न factories से बना), work-stealing वाला ForkJoinPool (जिसका shared instance commonPool() parallel stream और CompletableFuture का default execution target है), और periodic execution के लिए ScheduledThreadPoolExecutor — CPU-bound काम में ये अभी भी प्रमुख हैं।
Pool मूलतः इस पर आधारित optimization है कि «OS thread बनाना और पकड़े रखना महँगा है, इसलिए reuse करें»। Virtual threads निर्माण लागत लगभग शून्य कर इस आधार को मिटाते हैं, इसलिए उन्हें reuse करने का कारण नहीं रहा — सटीक समझ यह नहीं कि pooling अक्षम हो गई, बल्कि threads इतने हल्के हो गए कि pooling optimization अनावश्यक है। और virtual threads के नीचे, JDK का scheduler लगभग core count के carrier threads (OS threads) work-stealing ForkJoinPool के रूप में चलाता है।2 अर्थात् «OS threads के छोटे pool से विशाल concurrent काम सँभालना» रूप बचा रहता है; केवल उस pool का management developer के हाथ से JVM को गया। कहना उचित है कि Java उसी मंज़िल पर पहुँचती है जहाँ .NET का async/await await पर thread pool को लौटाता है, code का रूप बदले बिना।
4. Shared mutable state घटाना — partitioning, immutability, concurrent collections, और queues
Contention तभी उठती है जब «कई threads» और «shared mutable data» दोनों हों। Thread संख्या requirements तय करती हैं, इसलिए design जो काट सकता है वह shared हिस्सा है। तीन परिवार की तकनीकें हैं — partitioning, immutable बनाना, और data hand-off — और Java में उन्हें ऐसे लिखते हैं।
Partition करें। Parallel aggregation में, हर thread के shared sum variable पर लिखने के बजाय, हर thread partial परिणाम बनाए और अंत में मिलाएँ। Parallel stream के reduce / collect ठीक यही रूप ढाँचे के रूप में देते हैं, और नीचे चर्चित LongAdder भी partitioning strategy का implementation है — internally cells में बाँटकर contention फैलाता है, और पढ़ते समय जोड़ता है। Shared state पर कितनी बार लिखें, यह घटाना सही synchronize लिखने से पहले आता है।
Immutable बनाएँ। record और immutable collections (List.copyOf / Map.copyOf) से ऐसा data बनाएँ जो निर्माण के बाद फिर न लिखा जाए, और बिना synchronization share कर सकते हैं। Configuration और master data के लिए standard pattern: जब बदलना हो, नया object बनाएँ और volatile reference बदलें। पर «केवल-पठन दिखता है» और «immutable है» अलग चीज़ें हैं। record के accessors अपने components के कच्चे references लौटाते हैं, और List.copyOf की copy भी shallow है (element objects नहीं दोहराती), इसलिए यदि elements mutable हैं, alias पकड़े कोई भी सामग्री फिर लिख सकता है, और contention रहती है। बिना synchronization share करना तभी सुरक्षित है जब पूरा object graph — elements सहित — immutable हो। यदि mutable elements शामिल हैं, या तो deep copy दें या elements को भी record / immutable types की ओर धकेलें।
Concurrent collections की combined operations इस्तेमाल करें। ConcurrentHashMap के «यदि अनुपस्थित हो तो बनाएँ, फिर डालें» pattern के लिए computeIfAbsent। यह method पूरी call atomically execute करती है, और यदि key अनुपस्थित हो तो mapping function उस एक call के भीतर ठीक एक बार बुलाया जाता है।8 Guarantee .NET के ConcurrentDictionary.GetOrAdd से भिन्न है (जिसकी factory contention में एक से अधिक बार चल सकती है) — वह बिंदु जो दोनों भाषाओं के बीच आने-जाने वाले आसानी से मिलाते हैं। पर यह «key के जीवन में ठीक एक बार» नहीं है। यदि function null लौटाए या फेंके, कोई mapping register नहीं होती, और बाद की call पर function फिर चलता है (registration के बाद entry हटाने पर भी यही)। यदि आपकी initialization दोहराए गए side effects सहन नहीं कर सकती, design करें कि function सफल हो और non-null लौटाए। Atomic होने की कीमत पर, computation चलते कुछ अन्य threads के updates block होते हैं, इसलिए mapping function छोटा रखें, और function के भीतर इसी map को कभी update न करें (पहचाना गया recursive update IllegalStateException फेंक सकता है)।8
// Frequency counter का standard pattern: computeIfAbsent + LongAdder
ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>();
freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
Queue से data hand-off करें। Threads के बीच data flow BlockingQueue से भेजें। क्षमता दिए ArrayBlockingQueue पर, भरा होने पर put block होता है, स्वाभाविक backpressure देता है — .NET संस्करण के bounded channel जैसा ही रूप। Virtual-thread युग में भी, producer और consumer के बीच स्पष्ट सीमा खींचने का यह design प्रभावी रहता है।
5. Locking discipline — synchronized और ReentrantLock
5.1. किस पर lock करें, और lock पकड़े क्या न करें
Locking की इकाई «code खंड» नहीं बल्कि «data» सोचें। हर mutable data समुच्चय की रक्षा के लिए एक lock object map करें, और उस data को छूने वाले हर स्थान पर वही lock लें — race bug की वास्तविकता आमतौर पर यह है कि यह mapping कहीं टूट गई। synchronized(this) और synchronized(SomeClass.class) से बचें, क्योंकि बाहरी code उसी object पर lock कर सकता है; इसके बजाय, रक्षा किए जाने वाले data को एक-से-एक private final Object lock = new Object(); जो बाहर कभी न उजागर हो से जोड़ें।
उस पर दो और discipline। पहला, lock पकड़े कुछ धीमा न करें, न कुछ जो बाहरी दुनिया छुए। Lock अभी पकड़े I/O, listener बुलाना, या unknown code चलाना पकड़ की अवधि बढ़ाता है और जोखिम है कि बुलाया गया दूसरा lock लेने की कोशिश करे, चित्र 2 की circular wait बनाए। दूसरा, कई locks के लिए acquire क्रम स्थिर करें। जहाँ दो या अधिक lock लें, नियम बनाएँ कि हर thread उन्हें उसी क्रम में ले, और जहाँ क्रम guarantee नहीं दे सकते, नीचे चर्चित tryLock(timeout) से «न मिले तो छोड़कर फिर कोशिश» पथ तैयार करें।
synchronized «छोटे, सरल mutual exclusion» के लिए पर्याप्त है। निम्न चाहिए तो ReentrantLock पर जाएँ।
tryLock(timeout)से timed acquire (स्थायी hang को ऐसे failure में बदलना जिसे log और सँभाल सकें)- Fairness policies, कई
Condition, या जब lock acquire और release अलग methods में बाँटना चाहें
ReentrantLock इस्तेमाल करते lock() के तुरंत बाद try और finally में unlock() का pattern न तोड़ें (Java में C++ के RAII जैसा कुछ नहीं, इसलिए यही pattern पूरा discipline है)।
5.2. Virtual threads और pinning — JDK 24 में क्या बदला
जब virtual threads पहली बार आए (JDK 21-23), बाधा यह थी कि synchronized block के अंदर blocking virtual thread को उसके OS thread से pin करता था (OS thread नहीं छोड़ सकता, पैमाने का लाभ खोता), और बार-बार या लंबे blocking स्थलों को ReentrantLock से बदलने की सिफ़ारिश थी।3 वह बाधा JDK 24 के JEP 491 ने monitor implementation फिर लिखकर हल की, और synchronized अब virtual threads pin नहीं करता।4 यदि आप JDK 24 या बाद पर हैं, pinning के प्रतिकार के रूप में synchronized यंत्रवत् बदलना अब आवश्यक नहीं। जाँचना ज़रूरी है कि आपके संगठन के पुराने दिशानिर्देश अभी भी JDK 21-युग की चेतावनी पर तो नहीं अटके।
5.3. Atomic और volatile कहाँ बैठते हैं
एक variable के atomic updates AtomicInteger / AtomicLong / AtomicReference सँभालते हैं (या, केवल उच्च आवृत्ति पर बढ़ने वाले आँकड़ों के लिए, contention-resistant LongAdder)। volatile visibility और ordering (happens-before) की guarantee देता है, combined operations की atomicity नहीं।5 .NET और C++ संस्करणों जैसा ही निष्कर्ष Java में भी है: flags और एकल values के लिए atomic, combined state के लिए lock, और volatile अकेले से न करवाएँ।
6. रोकने का design — interruption एक साझा भाषा के रूप में
6.1. interrupt की शिष्टाचार
Java में रोक और cancellation interruption के इर्द-गिर्द एकीकृत हैं। t.interrupt() target thread की interrupt status सेट करता है, और यदि target sleep / wait / join आदि में blocked हो, तो InterruptedException फेंककर तुरंत जगाता है (तब interrupt status साफ़ होती है)।6 Thread.stop / suspend / resume, अतीत के ज़बरदस्ती तंत्र, मूलतः unsafe हैं, इसलिए अब call करने पर UnsupportedOperationException आता है।6
flowchart TB
OWNER["Caller रोकता है - t.interrupt call करता है"] --> ST["Interrupt status सेट होती है"]
ST --> A["Computation कर रहा thread:<br/>loop में Thread.interrupted जाँचता है"]
ST --> B["sleep / wait / join में blocked:<br/>InterruptedException चलता है और तुरंत जगाता है<br/>status साफ़ होती है"]
A --> E["साफ़ कर स्वयं समाप्त करता है"]
B --> C{"catch क्या करता है?"}
C -->|"स्वयं समाप्त कर सकता है"| E
C -->|"समाप्त नहीं कर सकता - जैसे library में"| R["Thread.currentThread.interrupt<br/>status restore कर संकेत छोड़ता है"]
R --> E
चित्र 4: Interruption से cooperative stop। InterruptedException निगलने से रुकने का संकेत गायब होता है — पकड़ने के बाद चुनाव «समाप्त» या «restore» है
व्यवहार में याद रखने योग्य एक ही discipline है: ऐसा code न लिखें जो InterruptedException पकड़े और कुछ न करे। यदि अपनी ज़िम्मेदारी में समाप्त कर सकते हैं, वहीं समाप्त करें; यदि नहीं, Thread.currentThread().interrupt() से status restore करें और संकेत caller को दें (FAQ देखें)।
6.2. ExecutorService का दो-चरण shutdown
ExecutorService के shutdown API interruption model पर बैठते हैं। shutdown() नए कार्य स्वीकारना बंद करता है और पहले से submitted कार्यों को completion तक चलने देता है; shutdownNow() चल रहे कार्यों को रोकने की कोशिश करता है। Interface specification के रूप में यह best-effort है, और स्पष्ट रूप से documented है कि standard implementations (जैसे ThreadPoolExecutor) आमतौर पर Thread.interrupt() से cancel करता है — अर्थात् जो कार्य interruption का जवाब न दे वह shutdownNow से भी नहीं रुकेगा, और यदि custom Executor implementation इस्तेमाल कर रहे हैं, उसके documents में देखें वह कैसे cancel करता है (interrupt भेजता भी है या नहीं)।1 Official documents द्वारा दिखाया standard stop pattern निम्न दो-चरण pattern है।1
/** Shutdown पूरा होने पर true। यह false रहते shared resources मुक्त करने न बढ़ें। */
boolean shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown(); // चरण 1: नए कार्य स्वीकारना बंद और completion की wait
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
// चरण 2: Cancellation माँगें। चलने से पहले गिराए गए कार्य
// लौटते हैं, इसलिए उनके Future cancel चिह्नित करें ताकि get() पर रुके caller जगें
pool.shutdownNow().forEach(r -> {
if (r instanceof Future<?> f) f.cancel(false);
});
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Pool did not terminate");
return false; // Shutdown अधूरा। Success से अलग रूप में बताएँ
}
}
return true;
} catch (InterruptedException ex) {
pool.shutdownNow().forEach(r -> {
if (r instanceof Future<?> f) f.cancel(false);
});
Thread.currentThread().interrupt(); // इस thread की interrupt status भी restore करें
return false; // यह पथ भी shutdown अधूरा छोड़ सकता है
}
}
flowchart TB
S["shutdown<br/>नए कार्य स्वीकारना बंद"] --> W1{"awaitTermination<br/>completion की wait"}
W1 -->|"time limit में समाप्त"| DONE["Shutdown पूरा"]
W1 -->|"समय समाप्त"| NOW["shutdownNow<br/>चल रहे कार्यों को interrupt भेजता है<br/>जवाब कार्य पर depend"]
NOW --> W2{"awaitTermination<br/>फिर wait"}
W2 -->|"पूर्ण"| DONE
W2 -->|"अभी भी अधूरा"| LOG["Anomaly के रूप में log<br/>interrupt ignore करने वाले कार्य का संदेह"]
चित्र 5: ExecutorService का दो-चरण shutdown। चरणबद्ध design «विनम्रता से wait → interruption से माँग → फिर भी न रुके तो anomaly के रूप में देखें»
shutdownNow() द्वारा लौटाए Runnable cancel करने की एक सीमा है। जो लौटता है वह execution queue में बैठा object है — सादे submit के लिए वह वही FutureTask है जो caller को दिया गया, पर ExecutorCompletionService जैसे wrapper से submitted कार्यों के लिए वह queue के अंदर का wrapper है, caller के Future से अलग object। उस विन्यास में ऊपर का cancellation caller का Future पूरा नहीं करेगा, इसलिए submission समय अपनी Future सूची रखें और shutdown पर उन्हें cancel करें (या गिराए गए कार्य मालिक को लौटाएँ)।
close() (AutoCloseable), JDK 19 से उपलब्ध, «shutdown call कर completion तक wait» को try-with-resources से लिखने योग्य रूप में पैक करता है, और virtual-thread newVirtualThreadPerTaskExecutor के साथ try (var executor = ...) आधुनिक मूल रूप है।1 पर close() ऊपर के दो-चरण pattern का विकल्प नहीं। क्योंकि वह बिना timeout completion की wait करता है, यदि एक भी कार्य interruption का जवाब न दे या कभी न रुके, बंद करने वाला thread हमेशा के लिए block होता है। यह उन दायरों के लिए औज़ार है जहाँ कार्य सीमित हैं और completion guaranteed है (वहीं submit करें, वहीं wait करें); application के shutdown पथ जैसे स्थानों पर, जहाँ आप चाहते हैं कि हमेशा सीमित समय में समाप्त हो, timed दो-चरण pattern इस्तेमाल करें। व्यक्तिगत कार्य cancel करना भी interruption से होता है, Future.cancel(true) से।
7. UI thread — Swing का EDT
Desktop applications, भाषा या framework चाहे जो हो, इस नियम का पालन करते हैं कि UI उस thread का विशेष है जो उसे manage करता है। Swing में वह विशेष thread Event Dispatch Thread (EDT) है: Swing component methods, नियमतः, thread-safe नहीं हैं, और कई threads से छूना thread interference और memory consistency errors आमंत्रित करता है। अन्य threads से screen update SwingUtilities.invokeLater से EDT को माँगें, और उलटा, क्योंकि EDT पर लंबे operations UI जमा देते हैं, भारी काम SwingWorker आदि से worker thread पर निकालें।7 JavaFX वही रूप अपनाता है: UI updates Platform.runLater से application thread पर माँगे जाते हैं।
8. Verification और debug — thread dump एक हथियार के रूप में
Testing से race bugs मिलने की अपेक्षा न करें। साधारण tests उस run को success गिनते हैं जिसमें contention संयोग से नहीं हुई। रक्षा तीन परतों में सोचें।
पहली रक्षा रेखा design है। Review में तालिका से जाँचें: कौन-सा mutable data shared है, प्रत्येक मद कौन-सा lock सुरक्षित करता है (खंड 5.1 की mapping), क्या lock acquire क्रम unique है, कोई catch block InterruptedException तो नहीं निगल रहा, और क्या stop पथ (shutdown/interruption) हर कार्य तक पहुँचता है।
दूसरा, thread dump का अच्छा इस्तेमाल करें। Java के पास «इस जमे क्षण हर thread की state» पकड़ने का standard औज़ार है: jstack (या jcmd <pid> Thread.print) stack traces छापता है, और -l option lock जानकारी भी जोड़ता है।9 ध्यान दें कि यह पारंपरिक dump format platform threads के लिए है; इसमें आपके application के virtual threads नहीं आते। Virtual threads वाले विन्यास (खंड 3) में blocked requests trace करते jcmd <pid> Thread.dump_to_file -format=json <file> इस्तेमाल करें, जो virtual threads भी dump कर सकता है।2 Hang जाँच की मूल प्रक्रिया कुछ सेकंड के अंतर पर दो-तीन dumps लेना और मिलान करना है कि प्रत्येक idle thread किस lock की wait कर रहा है, और वह lock कौन पकड़े है। यदि tryLock(timeout) timeout log करें (खंड 5.2), dump लेने का trigger स्वचालित भी कर सकते हैं।
तीसरा, load के नीचे हिलाएँ। Stress testing — cores से अधिक parallelism पर लंबे समय चलाना, processing क्रम random करना, कृत्रिम delay डालना — development मशीन पर race condition का «hit» खींचने की संभावना बढ़ाने का व्यावहारिक तरीका है। Release से पहले कम से कम एक test production-scale data मात्रा और thread संख्या से चलाएँ।
9. Java concurrency कहाँ जा रही है — Structured Concurrency
आधा कदम आगे एक त्वरित नज़र, समापन के लिए। Virtual threads की मान्यता पर बना Structured Concurrency (StructuredTaskScope) — जो कई subtasks को एक कार्य इकाई मानता है और failure प्रसार तथा cancellation संरचित करता है — विकास में है और अगस्त 2026 तक अभी preview सुविधा है। JDK 25 के पाँचवें preview (JEP 505) में इसे StructuredTaskScope.open() आधारित API रूप में संशोधित किया गया, और वर्तमान JDK 26 में छठे preview (JEP 525) के रूप में जारी है।1011 इस बीच Scoped Values, immutable context sharing जो ThreadLocal की समस्याएँ हल करता है, JDK 25 में अंतिम हुआ।12 इस लेख के सिद्धांत — स्पष्ट कार्य सीमाएँ, immutable sharing, cooperative stop — इन नए API की दिशा से भी मेल खाते हैं।
10. सारांश — Java checklist
- Business code में कोई
new Threadतो नहीं बचा (ExecutorService/ virtual threads पर बना है)? - I/O-bound और CPU-bound काम अलग execution तंत्रों पर भेजे गए हैं (चित्र 3 की शाखा)?
- Virtual threads pool नहीं हो रहे, और concurrency
Semaphoreसे सीमित है? - Shared data immutable है (
record/List.copyOf), याjava.util.concurrentके tools पर बना है? - कोई
synchronized(this)या सार्वजनिक रूप से उजागर object पर lock नहीं? ConcurrentHashMapकी combined operations (computeIfAbsentआदि) इस्तेमाल कर mapping function छोटा रख रहे हैं?volatileसे atomicity की अपेक्षा नहीं (counters Atomic classes /LongAdderइस्तेमाल करते हैं)?- एक भी catch block
InterruptedExceptionनहीं निगल रहा? ExecutorServiceरोकना timed दो-चरण pattern का पालन करता है (औरclose()वाले स्थान उन दायरों तक सीमित हैं जहाँ कार्य completion guaranteed है)?- Swing/JavaFX UI updates EDT / application thread पर समेकित हैं?
Java concurrency tools से सर्वाधिक सुसज्जित भाषाओं में से एक है, और virtual threads के आगमन ने सीधे synchronous code को फिर लिखे बिना scale करने का मार्ग खोला। ठीक इसलिए tools के बीच श्रम विभाजन सही पकड़ना — कौन throughput के लिए, कौन mutual exclusion के लिए, और रोक का संकेत क्या है — Java में multithreading design का सार है।
संबंधित लेख
- Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें
- Practical multithreading best practices: C++ संस्करण — RAII और jthread से structure द्वारा accidents मिटाना
- Practical multithreading best practices: C संस्करण — Win32 API तरीके से सुरक्षित लिखना
- A Practical Decision Table for C# async/await - Task.Run and ConfigureAwait
संबंधित परामर्श क्षेत्र
KomuraSoft LLC Java business systems और batch processing की multithreading design review, shared-state corruption और «कभी-कभी नहीं रुकता / hang» जैसे concurrency-संबंधी दोषों की root-cause investigation (thread dump analysis), तथा virtual threads अपनाने पर technical consulting सँभालता है।
संदर्भ लिंक
-
Oracle, ExecutorService (Java SE 21 & JDK 21 API). shutdown() के पहले से submitted कार्यों को completion तक चलने देते हुए नई submissions रोकने पर; shutdownNow() के चल रहे कार्यों को रोकने की कोशिश और execution की wait कर रहे कार्यों की सूची लौटाने पर, यद्यपि विशिष्ट implementation Thread.interrupt() से cancel करता है, best-effort से आगे कोई guarantee नहीं, इसलिए जो कार्य interruption का जवाब न दे वह समाप्त नहीं होगा; awaitTermination से completion की wait करने पर; close() (AutoCloseable, Java 19 से) के shutdown call कर completion की wait करने, try-with-resources के साथ प्रयोग योग्य होने पर; और shutdown → awaitTermination → shutdownNow दो-चरण shutdown के इस्तेमाल उदाहरण के रूप में दिखाए जाने पर। ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
OpenJDK, JEP 444: Virtual Threads. Virtual threads के JDK 21 में official सुविधा बनने पर; उनके हल्के threads होने पर जो उच्च-throughput concurrent applications लिखने, बनाए रखने और देखने का प्रयास नाटकीय रूप से घटाते हैं; सीधे «एक request, एक thread» synchronous code अपरिवर्तित scale करने के design दर्शन पर; JDK के virtual thread scheduler के FIFO mode में work-stealing ForkJoinPool होने पर, default parallelism उपलब्ध processor संख्या के बराबर; और नया thread dump format जो virtual threads शामिल करता है jcmd Thread.dump_to_file (सादे पाठ और JSON रूप में) के रूप में जोड़े जाने पर, जबकि पारंपरिक thread dump virtual threads शामिल नहीं करते। ↩ ↩2 ↩3 ↩4 ↩5
-
Oracle Java SE Core Libraries, Virtual Threads. Virtual threads के Java runtime द्वारा implemented हल्के threads होने पर जो blocking I/O के दौरान अपना OS thread छोड़ते हैं; उनके गति (latency) के बजाय पैमाने (throughput) की सुविधा होने और CPU-intensive processing के लिए अनुपयुक्त होने पर; virtual threads कभी न pool करने और प्रति कार्य एक इस्तेमाल करने पर (newVirtualThreadPerTaskExecutor); concurrency सीमित करने के लिए thread pool के बजाय Semaphore इस्तेमाल करने पर; JDK 21 तक synchronized के अंदर blocking के OS thread से pinning करने पर, जिसके लिए बार-बार या लंबे स्थलों को ReentrantLock से बदलने की सलाह थी; और -Djdk.tracePinnedThreads से pinning पहचानने पर। ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
OpenJDK, JEP 491: Synchronize Virtual Threads without Pinning. JDK 24 के लिए JVM के monitor implementation के virtual thread support हेतु फिर लिखे जाने पर, ताकि synchronized block या method के अंदर blocking अब virtual thread को उसके carrier thread से pin न करे; और इसके अर्थ पर कि JDK 21-23 युग का «synchronized को ReentrantLock से बदलें» प्रतिकार सिद्धांततः अब आवश्यक नहीं। ↩ ↩2
-
Oracle, The Java Tutorials, Memory Consistency Errors. Memory consistency errors के तब उठने पर जब कई threads का उसी data का inconsistent view हो; उनसे बचने की कुंजी happens-before relationship होने पर (guarantee कि एक statement का memory write दूसरे statement को दिखे); और synchronized, volatile, तथा Thread.start / join आदि के happens-before बनाने पर। ↩ ↩2 ↩3
-
Oracle, Thread (Java SE 21 & JDK 21 API). Thread.stop / suspend / resume के मूलतः unsafe होने पर (locks inconsistent state में मुक्त होते हैं और टूटे objects दिखते हैं; suspend deadlock आमंत्रित कर सकता है), उन्हें हटाने के लिए discourage बनाने, और अब call पर UnsupportedOperationException फेंकने पर; interrupt() के interrupt status सेट करने और sleep / wait / join में blocked thread को InterruptedException फेंककर जगाने पर (जो interrupt status साफ़ करता है); और interrupted() तथा isInterrupted() उस status से कैसे व्यवहार करते हैं, इस अंतर पर। ↩ ↩2 ↩3
-
Oracle, The Java Tutorials, The Event Dispatch Thread. Swing के event-handling code के Event Dispatch Thread (EDT) पर चलने पर; अधिकांश Swing object methods के thread-safe न होने पर, ताकि कई threads से call thread interference और memory consistency errors आमंत्रित करें, अर्थात् Swing components तक पहुँच नियमतः EDT पर होनी चाहिए; अन्य threads से SwingUtilities.invokeLater / invokeAndWait से EDT पर कार्य माँगने पर; और EDT पर चलने वाले कार्यों के शीघ्र समाप्त होने की आवश्यकता पर। ↩ ↩2
-
Oracle, ConcurrentHashMap (Java SE 21 & JDK 21 API). computeIfAbsent की पूरी method call atomically execute होने, key अनुपस्थित होने पर mapping function ठीक एक बार बुलाए जाने पर; computation के दौरान अन्य threads की कुछ update operations block होने पर, इसलिए उसे छोटा और सरल रखना चाहिए; mapping function के इस map को स्वयं बदलने पर प्रतिबंध, पहचाने गए recursive update के IllegalStateException पर; और retrieval operation (get) के block न होने, किसी key के update और बाद की retrieval के बीच happens-before relationship होने पर। ↩ ↩2 ↩3
-
Oracle, The jstack Command (Java SE 21 Tools Reference). jstack के निर्दिष्ट Java process के हर thread के stack traces (class name, method name, line number) छापने पर; -l option के अतिरिक्त lock जानकारी सहित विस्तृत प्रदर्शन सक्षम करने पर; और jcmd जैसे अन्य निदान tools के साथ इस्तेमाल होने पर। ↩
-
OpenJDK, JEP 505: Structured Concurrency (Fifth Preview). Structured Concurrency API के related subtasks के समूह को एक कार्य इकाई मानने और error प्रसार तथा cancellation संरचित करने पर; StructuredTaskScope के static factory method (open) से खोले जाने वाले रूप में संशोधित होने पर; और JDK 25 तक इसके पाँचवाँ preview होने, अभी अंतिम सुविधा न होने पर। ↩
-
OpenJDK, JEP 525: Structured Concurrency (Sixth Preview). Structured Concurrency के JDK 26 में भी छठे preview के रूप में जारी रहने पर — अर्थात् अगस्त 2026 तक वर्तमान JDK में अभी preview सुविधा, इस्तेमाल के लिए preview features सक्षम करनी पड़ती हैं। ↩
-
OpenJDK, JEP 506: Scoped Values. Scoped Values के JDK 25 में अंतिम होने पर; और उनके threads के भीतर तथा पार immutable context data सुरक्षित और कुशलता से share करने के तंत्र होने, ThreadLocal की समस्याओं (mutability, lifecycle management, और inheritable लागत) का समाधान देने पर। ↩
संबंधित लेख
निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।
Practical multithreading best practices: C संस्करण — Win32 API तरीके से सुरक्षित लिखना
Win32 के साथ C में multithreading का स्थापित तरीका _beginthreadex से thread बनाना, SRW lock और condition variable, Interlocked functions,...
Practical multithreading best practices: C++ संस्करण — RAII और jthread से structure द्वारा accidents मिटाना
C++ में multithreading वह संसार है जहाँ data race undefined behavior बन जाता है। यह लेख std::thread के destructor का pitfall, jthread और ...
Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें
Multithreaded .NET/C# code को कभी-कभी crash या hang होने से बचाने वाले design नियमों का practical सार: thread स्वयं न बनाकर Task पर चलें,...
Volume Shadow Copy (VSS) की संरचना और व्यवहार — in-use फ़ाइल का backup क्यों लिया जा सकता है
In-use फ़ाइलें sharing violation से copy नहीं हो पातीं, फिर backup software उन्हें कैसे ले लेता है? Volume Shadow Copy (VSS) में requeste...
Win32 Thread Pool API — CreateThreadpoolWork से thread बनाए बिना concurrency
क्या आपके native code में CreateThread calls बिखरी पड़ी हैं? यह लेख Vista में redesign की गई Win32 thread pool API — work, timer, wait और...
संबंधित विषय
ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।
Windows के तकनीकी विषय
Windows विकास, बग जाँच और मौजूदा संपत्तियों के उपयोग का प्रवेश-द्वार।
अक्सर पूछे जाने वाले प्रश्न
इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।
- अब virtual threads हैं, तो क्या thread pool (ExecutorService) की अब ज़रूरत नहीं?
- यह इस्तेमाल पर depend करता है। Virtual threads बहुत बड़ी संख्या में I/O-wait प्रधान कार्यों को चलाने का तंत्र हैं; वे code तेज़ नहीं करते, वे throughput बढ़ाते हैं। I/O-bound काम के लिए प्रति कार्य एक virtual thread इस्तेमाल करें (Executors.newVirtualThreadPerTaskExecutor) और virtual threads को कभी pool न करें। दूसरी ओर, CPU को पूरा भरने वाली computation के parallelism के लिए, लगभग core count तक सीमित platform threads का pool (या parallel stream) पहले की तरह सही औज़ार है। और यदि आप किसी बाहरी सेवा से concurrent connections की संख्या सीमित करना चाहते हैं, तो virtual-thread युग की सिफ़ारिश pool size से नहीं बल्कि Semaphore से सीमित करना है।
- synchronized इस्तेमाल करूँ या ReentrantLock?
- छोटे, सरल mutual exclusion के लिए synchronized काफी है, और code संक्षिप्त रहता है। ReentrantLock तब चुनें जब tryLock से timed acquire, fairness policy, या कई Condition जैसी सुविधाएँ चाहिए। Virtual threads के साथ जोड़ने पर एक ऐतिहासिक सावधानी है: JDK 21-23 में समस्या थी कि synchronized block के अंदर blocking virtual thread को उसके OS thread से pin कर देता था, इसलिए बार-बार या लंबे blocking स्थलों को ReentrantLock से बदलने की सिफ़ारिश थी। JDK 24 (JEP 491) ने monitor implementation फिर लिखा और यह बाधा हल कर दी। JDK 24 से pinning के कारण synchronized बदलने की ज़रूरत नहीं।
- volatile जोड़ने से कुछ thread-safe हो जाता है क्या?
- नहीं। Java का volatile उस variable पर लिखने और पढ़ने के बीच happens-before relationship बनाता है, visibility (कि latest write अन्य threads को दिखे) और ordering की guarantee देता है, पर «पढ़ो, compute करो, वापस लिखो» जैसी combined operation की atomicity की guarantee नहीं देता। कई threads से volatile int counter पर ++ करें तो जोड़ खो जाते हैं। Counter के लिए AtomicInteger / AtomicLong (उच्च-आवृत्ति aggregation के लिए LongAdder) इस्तेमाल करें, और कई variables एक साथ सुरक्षित करने पर lock। volatile लगभग केवल सरल state-flag जैसी स्थितियों में उपयुक्त है — जहाँ एक thread लिखता है और बाकी केवल पढ़ते हैं।
- InterruptedException पकड़कर ignore कर सकते हैं?
- नहीं। Interruption Java का standard रुकने और cancel करने का संकेत है, और उसे निगलना एक ऐसा thread बनाता है जो रुकेगा नहीं। InterruptedException फेंके जाने तक interrupt status पहले ही साफ़ हो चुकी होती है, इसलिए यदि आप काम स्वयं पूरा नहीं कर सकते, तो Thread.currentThread().interrupt() से status restore करें ताकि caller के लिए संकेत रहे, या exception ज्यों-का-त्यों ऊपर फेंकें। खाली catch block जो कुछ नहीं करता, उन bugs का classic कारण है जहाँ shutdown लागू नहीं होता या shutdownNow ignore हो जाता है।
- Thread.stop से thread नहीं रोक सकते?
- नहीं, नहीं रोक सकते। Thread.stop मूलतः unsafe है (lock inconsistent state में छोड़कर मुक्त करता है, टूटे objects अन्य threads को दिखाता है), इसलिए लंबे समय से discouraged है, और वर्तमान Java में call करने पर UnsupportedOperationException फेंकता है। Thread.suspend / resume पर भी यही लागू होता है। Thread रोकने का एकमात्र वैध तरीका interruption से cooperative stop है। यदि ExecutorService इस्तेमाल कर रहे हैं, तो shutdown केवल नए कार्य स्वीकारना बंद करता है और completion की wait करता है — चल रहे कार्यों को interrupt नहीं भेजता। चल रहे कार्यों को रोकने की कोशिश shutdownNow करता है, और वह best-effort है (standard implementation interruption से काम करता है)।