কনকারেন্সি (Concurrency) এবং মাল্টিথ্রেডিং (Multithreading) হল এমন দুটি গুরুত্বপূর্ণ ধারণা যা কম্পিউটার প্রোগ্রামিং এবং সফটওয়্যার ডেভেলপমেন্টে খুবই গুরুত্বপূর্ণ। এগুলি বিভিন্ন কাজ বা প্রসেস একযোগভাবে চালানোর ক্ষমতা বৃদ্ধি করে, যা প্রোগ্রামের কর্মক্ষমতা এবং দক্ষতা উন্নত করতে সাহায্য করে।
Java এ, Concurrency এবং Multithreading একে অপরের সাথে সম্পর্কিত কিন্তু আলাদা ধারণা। আমরা এই ধারণাগুলি বুঝতে, এবং সেগুলি কিভাবে বাস্তবায়ন করতে হয় তা জানব।
1. কনকারেন্সি (Concurrency)
কনকারেন্সি একটি প্রোগ্রাম বা সিস্টেমের ক্ষমতা যাতে একাধিক কাজ একযোগে বা সমান্তরালে করা হয়। কনকারেন্সি আসলে CPU এর সময় ভাগ করে কাজ করার একটি পদ্ধতি, যেখানে একাধিক কাজ (processes) বা থ্রেড একে অপরের সাথে parallel বা interleave হয়ে সম্পন্ন হয়।
কনকারেন্সি একাধিক কাজের মধ্যে সমন্বয় সাধন করে এবং বিভিন্ন কাজগুলি একে অপরের সাথে নির্বিঘ্নে চলতে থাকে, তবে একে একসাথে সম্পাদনা করা হয় না। কনকারেন্সি অর্থাৎ একাধিক কাজের যৌথ কার্যক্রম পরিচালনা করতে পারে কিন্তু সেগুলি একসাথে সম্পন্ন হয় কিনা, তা নির্ভর করে কম্পিউটিং রিসোর্স (যেমন CPU কোর) এর উপর।
2. মাল্টিথ্রেডিং (Multithreading)
মাল্টিথ্রেডিং কনকারেন্সি একটি বাস্তবায়ন পদ্ধতি, যেখানে একাধিক থ্রেডকে একই সময়ে চালানো হয়। একটি থ্রেড (Thread) হল একটি প্রোগ্রামের একক কার্যনির্বাহী ইউনিট। মাল্টিথ্রেডিং এর মাধ্যমে একাধিক থ্রেড একই সময়ে CPU রিসোর্স শেয়ার করতে পারে, ফলে প্রোগ্রামটি আরো দ্রুত এবং কার্যকরীভাবে কাজ করতে সক্ষম হয়।
মাল্টিথ্রেডিং সুবিধা:
- ডাউনটাইম কমানো: CPU সম্পূর্ণ সময় ব্যবহৃত হয়, অন্যথায় একাধিক থ্রেড থাকার কারণে সময় অপচয় কম হয়।
- কাজের উন্নতি: একাধিক কাজ একসাথে চলতে থাকলে, অ্যাপ্লিকেশন দ্রুত কাজ করতে পারে।
- প্রতিক্রিয়া গতি বৃদ্ধি: মাল্টিথ্রেডিং প্রয়োগ করলে UI এবং ব্যাকগ্রাউন্ড কাজের মধ্যে সমন্বয় ভাল হয় এবং প্রোগ্রামের প্রতিক্রিয়া গতি বাড়ে।
3. থ্রেড ক্লাস এবং Runnable ইন্টারফেস (Thread Class and Runnable Interface)
Java তে থ্রেড তৈরি করতে দুটি প্রধান উপায় রয়েছে:
- Thread Class ব্যবহার করে থ্রেড তৈরি করা।
- Runnable Interface ব্যবহার করে থ্রেড তৈরি করা।
থ্রেড ক্লাস (Thread Class) ব্যবহার করে থ্রেড তৈরি করা:
class MyThread extends Thread {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value: " + i);
try {
Thread.sleep(500); // Sleep for 500 ms
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // Starting thread t1
MyThread t2 = new MyThread();
t2.start(); // Starting thread t2
}
}
ব্যাখ্যা:
MyThreadক্লাসThreadক্লাস থেকে উত্তরাধিকারী।run()মেথডে থ্রেডের কার্যক্রম নির্ধারণ করা হয়।start()মেথডটি থ্রেডকে চালু করে এবং এটিrun()মেথডকে কল করে।
আউটপুট:
1 Value: 1
1 Value: 2
2 Value: 1
2 Value: 2
1 Value: 3
2 Value: 3
...
Runnable Interface ব্যবহার করে থ্রেড তৈরি করা:
class MyRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value: " + i);
try {
Thread.sleep(500); // Sleep for 500 ms
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
public static void main(String[] args) {
MyRunnable obj = new MyRunnable();
Thread t1 = new Thread(obj);
t1.start(); // Starting thread t1
Thread t2 = new Thread(obj);
t2.start(); // Starting thread t2
}
}
ব্যাখ্যা:
- এখানে
Runnableইন্টারফেস ইমপ্লিমেন্ট করা হয়েছে এবংrun()মেথডে থ্রেডের কার্যক্রম লেখা হয়েছে। Threadক্লাসের একটি অবজেক্ট তৈরি করেRunnableঅবজেক্টকে পাস করা হয়েছে।
আউটপুট:
1 Value: 1
1 Value: 2
2 Value: 1
2 Value: 2
1 Value: 3
2 Value: 3
...
4. থ্রেড সিঙ্ক্রোনাইজেশন (Thread Synchronization)
যখন একাধিক থ্রেড একে অপরের রিসোর্স ব্যবহার করে, তখন ডেটা ইনকনসিস্টেন্সি সমস্যা তৈরি হতে পারে। এর থেকে রক্ষা পেতে থ্রেড সিঙ্ক্রোনাইজেশন প্রয়োজন। সিঙ্ক্রোনাইজেশন নিশ্চিত করে যে, একই সময় একাধিক থ্রেড একই রিসোর্স অ্যাক্সেস করতে পারবে না।
থ্রেড সিঙ্ক্রোনাইজেশন উদাহরণ:
class Counter {
private int count = 0;
// Synchronized method to ensure thread safety
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
class MyThread extends Thread {
Counter counter;
MyThread(Counter counter) {
this.counter = counter;
}
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment(); // Increment counter
}
}
}
public class ThreadSynchronizationExample {
public static void main(String[] args) {
Counter counter = new Counter();
// Creating multiple threads
MyThread t1 = new MyThread(counter);
MyThread t2 = new MyThread(counter);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
System.out.println(e);
}
System.out.println("Final Count: " + counter.getCount());
}
}
ব্যাখ্যা:
increment()মেথডটি সিঙ্ক্রোনাইজড, যাতে এক সময়ে একাধিক থ্রেড এটি এক্সিকিউট না করতে পারে।join()মেথড ব্যবহার করা হয়েছে যাতেmain()থ্রেড সব থ্রেড সম্পন্ন হওয়ার পরই পরবর্তী কাজ শুরু করে।
আউটপুট:
Final Count: 2000
5. থ্রেড পুল (Thread Pool)
থ্রেড পুল একটি কনসেপ্ট যা থ্রেডগুলির একটি নির্দিষ্ট সংখ্যা সংরক্ষণ করে, যেগুলি বিভিন্ন কাজ পরিচালনা করার জন্য পুনঃব্যবহৃত হয়। Java তে থ্রেড পুল ব্যবহারের জন্য ExecutorService ব্যবহার করা হয়। এটি থ্রেড সৃষ্টির খরচ কমায় এবং অ্যাপ্লিকেশনের কর্মক্ষমতা উন্নত করতে সহায়ক।
থ্রেড পুল উদাহরণ:
import java.util.concurrent.*;
class Task implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + " is executing task.");
}
}
public class ThreadPoolExample {
public static void main(String[] args) {
// Create a thread pool with 3 threads
ExecutorService executorService = Executors.newFixedThreadPool(3);
// Submit tasks to the thread pool
for (int i = 0; i < 5; i++) {
executorService.submit(new Task());
}
// Shutdown the executor service
executorService.shutdown();
}
}
ব্যাখ্যা:
- এখানে,
ExecutorServiceব্যবহার করে একটি থ্রেড পুল তৈরি করা হয়েছে যার আকার 3। - আমরা 5টি কাজ (task) পুলে জমা দিয়েছি এবং এগুলি থ্রেড পুলের মাধ্যমে এক্সিকিউট হবে।
আউটপুট:
pool-1-thread-1 is executing task.
pool-1-thread-2 is executing task.
pool-1-thread-3 is executing task.
pool-1-thread-1 is executing task.
pool-1-thread-2 is executing task.
6. মাল্টিথ্রেডিংয়ের সুবিধা এবং চ্যালেঞ্জ
সুবিধা:
- কর্মক্ষমতা বৃদ্ধি: মাল্টিথ্রেডিং একাধিক কাজ একযোগে পরিচালনা করতে সক্ষম হওয়ায় কর্মক্ষমতা বৃদ্ধি পায়।
- রেসপন্সিভনেস: ইউজার ইন্টারফেসে ব্যাকগ্রাউন্ড কাজ করার মাধ্যমে অ্যাপ্লিকেশন আরও রেসপন্সিভ হয়।
- নেটওয়ার্কিং: মাল্টিথ্রেডিং নেটওয়ার্ক অ্যাপ্লিকেশন তৈরি করতে সহায়ক, যেমন চ্যাট অ্যাপ্লিকেশন।
চ্যালেঞ্জ:
- থ্রেড সিঙ্ক্রোনাইজেশন: একাধিক থ্রেড একসাথে একই রিসোর্স অ্যাক্সেস করলে ডেটা রেস (data race) ঘটতে পারে।
- ডেডলক: একাধিক থ্রেড একে অপরকে আটকে রেখে কাজ করতে পারে, যা প্রোগ্রামের ডেডলক সৃষ্টি করে।
- থ্রেড ম্যানেজমেন্ট: থ্রেড তৈরি ও ব্যবস্থাপনা কঠিন হতে পারে এবং এর জন্য অতিরিক্ত মেমরি ও প্রসেসর ব্যবহৃত হয়।
সারাংশ
কনকারেন্সি এবং মাল্টিথ্রেডিং হল একটি প্রোগ্রামে একাধিক কাজ বা প্রসেসের কার্যক্রম একযোগে চালানোর প্রক্রিয়া। Java তে থ্রেড তৈরি করতে Thread Class এবং Runnable Interface ব্যবহার করা হয়। সিঙ্ক্রোনাইজেশন নিশ্চিত করে যে একাধিক থ্রেড একই সময়ে একই রিসোর্স ব্যবহার না করে। থ্রেড পুল ব্যবহারের মাধ্যমে থ্রেডগুলির পুনঃব্যবহার করা যায়, যা কর্মক্ষমতা বৃদ্ধি করে। মাল্টিথ্রেডিং কার্যকরী এবং দ্রুত প্রোগ্রাম উন্নয়নের জন্য অপরিহার্য, তবে এটি সঠিকভাবে ব্যবস্থাপনা করতে হলে ডেডলক এবং রেস কন্ডিশনের মতো চ্যালেঞ্জগুলি মোকাবেলা করতে হবে।
Multithreading হল একটি প্রোগ্রামিং প্যারাডাইম যা একাধিক থ্রেড (threads) ব্যবহার করে একটি প্রোগ্রামকে একসাথে একাধিক কাজ সম্পাদন করতে সক্ষম করে। জাভাতে মল্টিথ্রেডিং ব্যবহার করার মাধ্যমে, একটি প্রোগ্রাম একাধিক টাস্ককে সমান্তরালে (parallel) সম্পাদন করতে পারে, ফলে সিস্টেমের কর্মক্ষমতা এবং দ্রুততা বাড়ে। এটি বিশেষত তখন প্রয়োজনীয় হয় যখন একটি বড় অ্যাপ্লিকেশন বা সার্ভার একাধিক কাজ বা সার্ভিস একসাথে পরিচালনা করছে।
Multithreading এর বেসিক ধারণা
একটি থ্রেড হল একটি প্রোগ্রামের একক এক্সিকিউশন রুট। একাধিক থ্রেড একসাথে কাজ করলে সেই প্রোগ্রামটি মল্টিথ্রেডেড (multithreaded) হয়ে ওঠে। Multithreading এর সাহায্যে, একসাথে একাধিক কাজ সম্পাদিত হতে পারে, যা সাধারণত কম্পিউটিংয়ের গতি এবং দক্ষতা বাড়াতে সাহায্য করে।
Multithreading এর সুবিধা:
- সম্পাদনার গতি বাড়ানো: একাধিক থ্রেডের মাধ্যমে একসাথে কাজ করা যায়, ফলে সময়ের সাশ্রয় হয়।
- প্রোগ্রামের উন্নত প্রতিক্রিয়া: একাধিক থ্রেডের মাধ্যমে প্রোগ্রাম ইন্টারফেসে অনেক বেশি দ্রুত প্রতিক্রিয়া প্রদান করতে পারে।
- উন্নত রিসোর্স ব্যবস্থাপনা: CPU ব্যবহারের মাধ্যমে একাধিক কাজকে সমান্তরালে চালানো যায়, যার ফলে সিস্টেমের রিসোর্স গুলি আরও কার্যকরীভাবে ব্যবহৃত হয়।
Java তে Multithreading
জাভাতে থ্রেড তৈরি এবং পরিচালনার জন্য দুটি প্রধান পদ্ধতি আছে:
- Thread ক্লাসের মাধ্যমে থ্রেড তৈরি করা:
Threadক্লাস একটি থ্রেডের কাজ পরিচালনা করে। থ্রেড ক্লাসেরrun()মেথডে থ্রেডের কাজ সংজ্ঞায়িত করা হয়।
- Runnable ইন্টারফেসের মাধ্যমে থ্রেড তৈরি করা:
Runnableইন্টারফেস ব্যবহার করে, থ্রেডে কাজ করার জন্যrun()মেথডকে ইমপ্লিমেন্ট করা হয়। এটি থ্রেড ক্লাসের একটি সাবক্লাস না হয়ে, একটি স্বতন্ত্র ইন্টারফেস হিসেবে কাজ করে।
1. Thread ক্লাসের মাধ্যমে Multithreading
জাভায় থ্রেড তৈরি করার জন্য Thread ক্লাসের একটি সাবক্লাস তৈরি করা হয়, এবং run() মেথডের মধ্যে সেই থ্রেডের কার্যক্রম উল্লেখ করা হয়। থ্রেডটি শুরু করার জন্য start() মেথড কল করা হয়।
উদাহরণ:
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
public static void main(String[] args) {
MyThread t1 = new MyThread(); // Create thread object
t1.start(); // Start the thread
}
}
এখানে, MyThread ক্লাসটি Thread ক্লাস থেকে ইনহেরিট করেছে, এবং run() মেথডে থ্রেডের কার্যকলাপ সংজ্ঞায়িত করা হয়েছে। start() মেথডটি থ্রেডটি চালু করে এবং run() মেথডটি বাস্তবায়ন হয়।
2. Runnable ইন্টারফেসের মাধ্যমে Multithreading
Runnable ইন্টারফেসও থ্রেড তৈরি করার জন্য ব্যবহৃত হয়, তবে এটি কম বেশি Thread ক্লাসের উপর ভিত্তি করে তৈরি করা হয়। এখানে run() মেথডে থ্রেডের কার্যক্রম সংজ্ঞায়িত করা হয় এবং তারপর Thread ক্লাসে এই Runnable ইন্টারফেসটি পাস করা হয়।
উদাহরণ:
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running using Runnable");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable); // Create thread object
t1.start(); // Start the thread
}
}
এখানে, MyRunnable ক্লাসটি Runnable ইন্টারফেস ইমপ্লিমেন্ট করেছে এবং run() মেথডে কার্যক্রম উল্লেখ করা হয়েছে। তারপর, Thread ক্লাসে এই Runnable অবজেক্ট পাস করে থ্রেড তৈরি করা হয়েছে এবং start() মেথড কল করে থ্রেডটি চালু করা হয়েছে।
Thread এর Lifecycle
জাভায় থ্রেডের একটি নির্দিষ্ট lifecycle থাকে, যা কয়েকটি ধাপে বিভক্ত:
- New: থ্রেড তৈরি হয়েছে, তবে চালু হয়নি।
- Runnable: থ্রেড চালু হয়েছে এবং CPU থেকে প্রক্রিয়ার জন্য প্রস্তুত।
- Blocked: থ্রেড কোন রিসোর্সের জন্য অপেক্ষা করছে (যেমন, ফাইল বা ডেটাবেস অ্যাক্সেস)।
- Waiting: থ্রেড অন্য থ্রেডের মধ্যে কোন সিগন্যাল বা সিঙ্ক্রোনাইজেশন প্রক্রিয়ার জন্য অপেক্ষা করছে।
- Terminated: থ্রেড তার কাজ সম্পন্ন করেছে এবং বন্ধ হয়ে গেছে।
Thread Synchronization
যখন একাধিক থ্রেড একই রিসোর্সে অ্যাক্সেস করে, তখন Thread Synchronization প্রয়োজন হয়। যদি একাধিক থ্রেড একে অপরকে প্রভাবিত না করে, তাহলে race conditions ঘটতে পারে, যেখানে এক থ্রেডের কাজ অপর থ্রেডের কাজের উপর প্রভাব ফেলতে পারে। এই সমস্যা সমাধান করতে synchronized কীওয়ার্ড ব্যবহার করা হয়, যা শুধুমাত্র একটি থ্রেডকে একই সময়ে রিসোর্স অ্যাক্সেস করতে দেয়।
উদাহরণ:
class Counter {
private int count = 0;
// Synchronize method to prevent race condition
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
এখানে, increment() মেথডটি synchronized করা হয়েছে, যাতে একে একে শুধুমাত্র একটি থ্রেড এটিতে অ্যাক্সেস করতে পারে এবং race condition থেকে রক্ষা পায়।
সারাংশ
Multithreading হল এমন একটি প্রোগ্রামিং কৌশল যা একাধিক থ্রেডের মাধ্যমে একসাথে একাধিক কাজ সম্পাদন করার সুযোগ দেয়। জাভাতে থ্রেড তৈরি করা হয় Thread ক্লাস অথবা Runnable ইন্টারফেসের মাধ্যমে। থ্রেডের Lifecycle বিভিন্ন ধাপে বিভক্ত থাকে এবং Synchronization ব্যবহার করে একাধিক থ্রেডের মাঝে রিসোর্সের সঠিক ব্যবস্থাপনা করা হয়। Multithreading সিস্টেমের কর্মক্ষমতা এবং প্রতিক্রিয়া বাড়াতে সাহায্য করে, বিশেষ করে যখন একাধিক টাস্ক একসাথে সম্পাদন করা প্রয়োজন।
Synchronized Data Structures (সিঙ্ক্রোনাইজড ডাটা স্ট্রাকচার) এমন ডাটা স্ট্রাকচার যা একাধিক থ্রেডের মধ্যে একযোগে নিরাপদভাবে কাজ করার জন্য ব্যবহৃত হয়। থ্রেড নিরাপত্তা নিশ্চিত করার জন্য সিঙ্ক্রোনাইজড ডাটা স্ট্রাকচারগুলি খুবই গুরুত্বপূর্ণ, বিশেষত মাল্টি-থ্রেডিং অ্যাপ্লিকেশন তৈরি করার সময়।
জাভা অনেক বিল্ট-ইন সিঙ্ক্রোনাইজড ডাটা স্ট্রাকচার সরবরাহ করে, যেমন Vector, Hashtable, এবং Concurrent Collections। এই ডাটা স্ট্রাকচারগুলি একাধিক থ্রেড দ্বারা অ্যাক্সেস করা হলে তাদের মধ্যে কোন ধরণের ডেটা ইন্টিগ্রিটি সমস্যা না হয়, সেই জন্য এগুলোর মধ্যে সিঙ্ক্রোনাইজেশন ব্যবহৃত হয়।
১. Synchronized Collections in Java
Vector (সিঙ্ক্রোনাইজড ArrayList)
Vector হলো একটি সিঙ্ক্রোনাইজড ডাইনামিক অ্যারে যা মাল্টি-থ্রেড অ্যাক্সেসের জন্য নিরাপদ। এটি ArrayList এর মতোই, তবে এর সমস্ত অপারেশন সিঙ্ক্রোনাইজড।
উদাহরণ: Vector
import java.util.*;
public class SynchronizedVector {
public static void main(String[] args) {
// Synchronized Vector
Vector<Integer> vector = new Vector<>();
// Multiple threads adding elements to vector
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
vector.add(i);
System.out.println("Thread 1 added: " + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 10; i < 20; i++) {
vector.add(i);
System.out.println("Thread 2 added: " + i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Print the vector contents
System.out.println("Vector Contents: " + vector);
}
}
ব্যাখ্যা:
- Vector ক্লাস একটি সিঙ্ক্রোনাইজড ডাটা স্ট্রাকচার। যখন একাধিক থ্রেড একই সময়ে
vector.add()মেথড কল করে, তখন সিঙ্ক্রোনাইজেশন নিশ্চিত করে যে শুধুমাত্র একটি থ্রেড একসাথেadd()মেথড কার্যকর করবে, যাতে ডেটা ক্ষতিগ্রস্ত না হয়।
আউটপুট:
Thread 1 added: 0
Thread 2 added: 10
Thread 1 added: 1
Thread 2 added: 11
...
Vector Contents: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
২. Hashtable (Synchronized HashMap)
Hashtable হল একটি সিঙ্ক্রোনাইজড key-value স্টোর যা HashMap এর মতো কাজ করে, তবে এটি মাল্টি-থ্রেড নিরাপত্তা নিশ্চিত করে। এতে সিঙ্ক্রোনাইজড মেথডগুলি অন্তর্ভুক্ত থাকে যাতে একাধিক থ্রেড একই সময়ে সিস্টেমের ডেটা অ্যাক্সেস করতে না পারে।
উদাহরণ: Hashtable
import java.util.*;
public class SynchronizedHashtable {
public static void main(String[] args) {
// Synchronized Hashtable
Hashtable<String, Integer> hashtable = new Hashtable<>();
// Multiple threads adding elements to hashtable
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
hashtable.put("Thread1-" + i, i);
System.out.println("Thread1 put: Thread1-" + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 5; i < 10; i++) {
hashtable.put("Thread2-" + i, i);
System.out.println("Thread2 put: Thread2-" + i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Print the hashtable contents
System.out.println("Hashtable Contents: " + hashtable);
}
}
ব্যাখ্যা:
- Hashtable সিঙ্ক্রোনাইজড ডাটা স্ট্রাকচার হিসেবে কাজ করে, যার ফলে মাল্টি-থ্রেডের মধ্যে একই
putঅপারেশন একসাথে কাজ না করে। Hashtable.put()মেথড সিঙ্ক্রোনাইজড, তাই একাধিক থ্রেড একই সময়ে মান সংযুক্ত করতে পারবে না।
আউটপুট:
Thread1 put: Thread1-0
Thread2 put: Thread2-5
Thread1 put: Thread1-1
Thread2 put: Thread2-6
...
Hashtable Contents: {Thread1-0=0, Thread1-1=1, Thread1-2=2, ..., Thread2-9=9}
৩. Concurrent Collections in Java
Java 5 থেকে Concurrent Collections সংযোজিত হয়েছে, যা মাল্টি-থ্রেডিং পরিবেশে পারফরম্যান্স বাড়ানোর জন্য ডিজাইন করা হয়েছে। এগুলোর মধ্যে ConcurrentHashMap এবং CopyOnWriteArrayList প্রধান।
ConcurrentHashMap:
এটি একটি Thread-safe ম্যাপ, তবে এতে ভিন্নভাবে সিঙ্ক্রোনাইজেশন প্রয়োগ করা হয়েছে। এটি মডিফিকেশনগুলির জন্য উপযুক্ত, এবং এটি একাধিক থ্রেডকে একই সময়ে একাধিক সেগমেন্টে কাজ করার অনুমতি দেয়।
উদাহরণ: ConcurrentHashMap
import java.util.concurrent.*;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// ConcurrentHashMap ব্যবহার করা
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Multiple threads adding elements to ConcurrentHashMap
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
map.put("Thread1-" + i, i);
System.out.println("Thread1 added: Thread1-" + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 5; i < 10; i++) {
map.put("Thread2-" + i, i);
System.out.println("Thread2 added: Thread2-" + i);
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Print the ConcurrentHashMap contents
System.out.println("ConcurrentHashMap Contents: " + map);
}
}
ব্যাখ্যা:
- ConcurrentHashMap একই সময়ে অনেক থ্রেডের কাছে নিরাপদভাবে ডেটা অ্যাক্সেস করার সুযোগ দেয়, তবে এটি একটি সেগমেন্ট-ভিত্তিক সিঙ্ক্রোনাইজেশন কৌশল ব্যবহার করে যা পারফরম্যান্স উন্নত করে।
আউটপুট:
Thread1 added: Thread1-0
Thread2 added: Thread2-5
Thread1 added: Thread1-1
Thread2 added: Thread2-6
...
ConcurrentHashMap Contents: {Thread1-0=0, Thread1-1=1, ..., Thread2-9=9}
সারাংশ
Synchronized Data Structures মাল্টি-থ্রেডিং পরিস্থিতিতে ডেটা নিরাপত্তা নিশ্চিত করতে ব্যবহৃত হয়। Java তে সিঙ্ক্রোনাইজড ডাটা স্ট্রাকচার ব্যবহৃত হয় বিশেষত একাধিক থ্রেড যখন একই ডেটাতে কাজ করে তখন, যাতে কোন ডেটা ক্ষতিগ্রস্ত না হয়।
গুরুত্বপূর্ণ পয়েন্টস:
- Vector এবং Hashtable সিঙ্ক্রোনাইজড ডাটা স্ট্রাকচার যা থ্রেড সেফ। এগুলির প্রতিটি অপারেশন সিঙ্ক্রোনাইজড, তাই একাধিক থ্রেড সেগুলিকে একসাথে অ্যাক্সেস করতে পারে না।
- Concurrent Collections (যেমন ConcurrentHashMap) মাল্টি-থ্রেড পরিবেশে ভালো পারফরম্যান্স নিশ্চিত করে এবং বিভিন্ন সেগমেন্টে একাধিক থ্রেডকে কাজ করতে দেয়।
- Synchronized Collections প্রধানত পুরানো জাভা সংস্করণের জন্য ব্যবহৃত হয়, তবে Concurrent Collections আধুনিক জাভা অ্যাপ্লিকেশনগুলির জন্য আরও ভালো পারফরম্যান্স প্রদান করে।
এসব ডাটা স্ট্রাকচার ও তাদের সিঙ্ক্রোনাইজেশন থ্রেড সেফ ডেটা ম্যানেজমেন্ট নিশ্চিত করে এবং একই সময়ে একাধিক থ্রেডকে ডেটা অ্যাক্সেস করতে সাহায্য করে।
Concurrency Control এবং Deadlock হল multithreading এবং parallel processing এ অত্যন্ত গুরুত্বপূর্ণ ধারণা। এগুলি বিভিন্ন থ্রেডের মধ্যে সিঙ্ক্রোনাইজেশন এবং নির্দিষ্ট পরিস্থিতিতে বিভিন্ন থ্রেডের মধ্যে সমন্বয় প্রক্রিয়া নিয়ন্ত্রণ করে।
১. Concurrency Control (সংকেত নিয়ন্ত্রণ)
Concurrency Control হল এমন একটি প্রক্রিয়া যা একাধিক থ্রেড বা প্রসেসের মধ্যে সিঙ্ক্রোনাইজেশন এবং ডেটা এক্সেসের সময়ের সঠিক সমন্বয় নিশ্চিত করে। যখন একাধিক থ্রেড একই ডেটা স্ট্রাকচার বা রিসোর্সের উপর একযোগে কাজ করে, তখন ডেটার অখণ্ডতা (data integrity) বজায় রাখা অত্যন্ত গুরুত্বপূর্ণ। Concurrency Control সঠিকভাবে পরিচালিত না হলে data inconsistency এবং race conditions হতে পারে।
Concurrency Control এর প্রধান উদ্দেশ্য হল:
- একাধিক থ্রেড বা প্রসেসের মধ্যে সঠিক সমন্বয় নিশ্চিত করা।
- ডেটার একাধিক থ্রেডের দ্বারা পরিবর্তনের সময় সমস্যা এড়ানো (যেমন, Race Condition এবং Atomicity রক্ষা করা)।
- একাধিক থ্রেড বা প্রসেসের মধ্যে সমন্বয় করা যাতে তারা নির্ভরযোগ্যভাবে কাজ করতে পারে।
Concurrency Control এর জন্য ব্যবহৃত পদ্ধতিসমূহ:
- Mutexes (Mutual Exclusion): একাধিক থ্রেড একই সময়ে একটি নির্দিষ্ট রিসোর্স অ্যাক্সেস করতে না পারে, সেজন্য তাদের মধ্যে mutexes ব্যবহার করা হয়।
- Locks: Locking ব্যবহার করে থ্রেড গুলোকে একে একে রিসোর্স অ্যাক্সেস করতে দেওয়া হয়।
- Reentrant Lock: জাভার
ReentrantLockক্লাস, যা আরও বেশি নিয়ন্ত্রণ এবং সিঙ্ক্রোনাইজেশনের সুবিধা দেয়।
- Reentrant Lock: জাভার
- Semaphore: এটি একটি কনকারেন্ট প্রোগ্রামিং কনসেপ্ট, যা একটি নির্দিষ্ট সংখ্যক থ্রেডকে একই রিসোর্স অ্যাক্সেস করতে দেয়।
- Synchronized Blocks: একটি ব্লক বা মেথড শুধুমাত্র এক থ্রেডের দ্বারা এক্সিকিউট করার জন্য synchronized কীওয়ার্ড ব্যবহার করা হয়।
উদাহরণ: Synchronized Method in Java
class Counter {
private int count = 0;
// Synchronized method to ensure only one thread accesses it at a time
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public int getCount() {
return count;
}
}
public class ConcurrencyExample {
public static void main(String[] args) {
Counter counter = new Counter();
// Thread 1 - Incrementing counter
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
// Thread 2 - Decrementing counter
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.decrement();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Final count after both threads have completed their execution
System.out.println("Final count: " + counter.getCount());
}
}
ব্যাখ্যা:
synchronizedkeyword: এটি নিশ্চিত করে যে শুধুমাত্র একটি থ্রেড একে একে increment() এবং decrement() মেথড অ্যাক্সেস করবে।join()method: এটি থ্রেডের এক্সিকিউশনের শেষে মেইন থ্রেডে ফিরে আসার আগে অপেক্ষা করে।
২. Deadlock (ডেডলক)
Deadlock হল একটি পরিস্থিতি যেখানে দুটি বা তার বেশি থ্রেড একে অপরের জন্য অপেক্ষা করে, এবং কোন থ্রেডই সম্পূর্ণ হতে পারে না। সাধারণভাবে, deadlock ঘটতে পারে যদি দুটি বা তার বেশি থ্রেড এমনভাবে লক পান যে তারা একে অপরের রিসোর্সের জন্য অপেক্ষা করে, কিন্তু তাদেরকে মুক্তি দেওয়া হয় না।
Deadlock এর সাধারণ শর্তসমূহ (Conditions for Deadlock):
- Mutual Exclusion: প্রতিটি রিসোর্স শুধুমাত্র এক থ্রেডের দ্বারা ব্যবহৃত হতে পারে।
- Hold and Wait: একটি থ্রেড এক বা একাধিক রিসোর্স ধরে রাখে এবং অন্য রিসোর্সের জন্য অপেক্ষা করে।
- No Preemption: একবার একটি থ্রেড একটি রিসোর্স নিয়ে নেয়, তখন অন্য থ্রেড সেই রিসোর্সটি ছিনিয়ে নিতে পারে না।
- Circular Wait: একাধিক থ্রেডের মধ্যে একটি চক্রের সৃষ্টি হয়, যেখানে প্রত্যেক থ্রেড পরবর্তী থ্রেডের জন্য অপেক্ষা করে।
উদাহরণ: Deadlock in Java
class A {
synchronized void methodA(B b) {
System.out.println("Thread 1: Locked A, waiting for B");
b.last();
}
synchronized void last() {
System.out.println("Inside A's last method");
}
}
class B {
synchronized void methodB(A a) {
System.out.println("Thread 2: Locked B, waiting for A");
a.last();
}
synchronized void last() {
System.out.println("Inside B's last method");
}
}
public class DeadlockExample {
public static void main(String[] args) {
final A a = new A();
final B b = new B();
// Thread 1
Thread t1 = new Thread(() -> a.methodA(b));
// Thread 2
Thread t2 = new Thread(() -> b.methodB(a));
t1.start();
t2.start();
}
}
ব্যাখ্যা:
- এখানে দুটি থ্রেড একে অপরের রিসোর্সের জন্য অপেক্ষা করছে। থ্রেড 1
methodA()তেAলক করে এবং তারপরBএর জন্য অপেক্ষা করে। একইভাবে, থ্রেড 2methodB()তেBলক করে এবং তারপরAএর জন্য অপেক্ষা করে। এই কারণে deadlock ঘটে, এবং কোন থ্রেডই এগোতে পারে না।
৩. Deadlock Prevention and Resolution
Deadlock প্রতিরোধ এবং সমাধানের জন্য কিছু কৌশল রয়েছে:
- Deadlock Prevention: Deadlock প্রতিরোধ করতে, উপরের শর্তগুলির মধ্যে কোনো একটি শর্ত এড়িয়ে যেতে হবে। যেমন:
- Avoid Hold and Wait: থ্রেডগুলি সম্পূর্ণভাবে সমস্ত রিসোর্স গ্রহণ না করা পর্যন্ত কোনও রিসোর্স গ্রহণ করতে পারে না।
- Avoid Circular Wait: রিসোর্সগুলিকে একটি নির্দিষ্ট ক্রমে বরাদ্দ করতে হবে যাতে circular wait এর পরিস্থিতি না ঘটে।
- Deadlock Detection: Deadlock ঘটে গেলে সিস্টেম এটি শনাক্ত করতে পারে এবং থ্রেডগুলিকে পুনরায় পুনরায় চালাতে পারে।
- Deadlock Recovery: Deadlock ঘটলে, সিস্টেম থ্রেডগুলির মধ্যে একটিকে সিলেক্ট করে তাকে বাতিল করতে পারে বা থ্রেডগুলির একটির এক্সিকিউশন থামিয়ে দেয়।
সারাংশ
- Concurrency Control: একাধিক থ্রেডের মধ্যে সিঙ্ক্রোনাইজেশন নিশ্চিত করার জন্য locks, mutexes, semaphores, এবং synchronized blocks ব্যবহার করা হয়।
- Deadlock: একটি অবস্থা যেখানে দুটি বা ততোধিক থ্রেড একে অপরের রিসোর্সের জন্য অপেক্ষা করে, এবং কোন থ্রেডই শেষ করতে পারে না। Deadlock এড়ানোর জন্য deadlock prevention, detection, এবং recovery কৌশল প্রয়োগ করা হয়।
Parallel Algorithms এমন অ্যালগরিদম যা একটি কাজ বা সমস্যা সমাধান করতে একাধিক প্রসেসর বা থ্রেড ব্যবহার করে। এর মাধ্যমে সমস্যার সমাধান দ্রুত এবং অধিক কার্যকরীভাবে করা যায়, বিশেষত যখন সমস্যার আকার বড় এবং জটিল হয়। সাধারণত Divide and Conquer ধরনের অ্যালগরিদমগুলিতে Parallel Processing খুব কার্যকরী হয়, যেখানে কাজকে ছোট ছোট অংশে ভাগ করা হয় এবং প্রতিটি অংশ একসাথে সমাধান করা হয়।
Java তে, Parallel Algorithms বাস্তবায়ন করার জন্য Java Concurrency API, Streams API, এবং ForkJoinPool এর মতো বিভিন্ন টুলস এবং লাইব্রেরি ব্যবহৃত হয়। এই টিউটোরিয়ালে, আমরা Parallel Algorithms এবং তাদের Efficiency নিয়ে আলোচনা করব, এবং কিভাবে Java তে এসব অ্যালগরিদম বাস্তবায়ন করা যায় তা দেখব।
1. Parallel Algorithms এর মৌলিক ধারণা
Parallel Algorithm এমন একটি অ্যালগরিদম যা একাধিক কাজ বা সাব-অপারেশন একযোগে (concurrently) সম্পন্ন করে। মূলত একাধিক প্রসেসর বা থ্রেড ব্যবহার করে একে অপরের সাথে সমন্বয় করে কাজ করা হয়। এর ফলে একক থ্রেডের তুলনায় সমস্যার সমাধান দ্রুত হয়।
Parallel Algorithms এর প্রধান উপকারিতা:
- Performance Improvement: বড় সমস্যাগুলি দ্রুত সমাধান করা যায়।
- Scalability: প্রসেসরের সংখ্যা বাড়ানোর সাথে সাথে অ্যালগরিদমের কার্যকারিতা উন্নত হয়।
- Resource Utilization: কম্পিউটার সিস্টেমের সমস্ত রিসোর্স সঠিকভাবে ব্যবহার করা যায়।
2. Java তে Parallel Algorithms বাস্তবায়ন
Java তে Parallel Algorithms বাস্তবায়ন করতে বিভিন্ন পদ্ধতি রয়েছে:
- Multithreading: একাধিক থ্রেড ব্যবহার করে কাজ ভাগ করে দেওয়া।
- ForkJoinPool: এটি divide and conquer স্টাইলের কাজের জন্য ব্যবহৃত হয়, যেখানে বড় কাজ ছোট ছোট টাস্কে ভাগ করা হয়।
- Streams API: Streams API তে parallelStream() ব্যবহার করে সহজেই একাধিক থ্রেডে কাজ ভাগ করা যায়।
3. Parallel Sorting Algorithm (Merge Sort)
Merge Sort হল একটি divide and conquer অ্যালগরিদম যা O(n log n) সময় জটিলতায় কাজ করে। এই অ্যালগরিদমটিকে parallel করা হলে, বড় অ্যারে গুলো দ্রুত সজ্জিত করা যায়।
উদাহরণ: Parallel Merge Sort in Java
import java.util.Arrays;
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
public class ParallelMergeSort {
public static void mergeSort(int[] arr) {
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new MergeSortTask(arr, 0, arr.length - 1));
}
// Recursive task to perform merge sort
static class MergeSortTask extends RecursiveTask<Void> {
int[] arr;
int left, right;
MergeSortTask(int[] arr, int left, int right) {
this.arr = arr;
this.left = left;
this.right = right;
}
@Override
protected Void compute() {
if (left < right) {
int mid = (left + right) / 2;
// Split the task into two subtasks
MergeSortTask leftTask = new MergeSortTask(arr, left, mid);
MergeSortTask rightTask = new MergeSortTask(arr, mid + 1, right);
// Fork the tasks
leftTask.fork();
rightTask.fork();
// Wait for the subtasks to complete
leftTask.join();
rightTask.join();
// Merge the sorted halves
merge(arr, left, mid, right);
}
return null;
}
// Merging two halves
private void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
System.arraycopy(temp, 0, arr, left, temp.length);
}
}
public static void main(String[] args) {
int[] arr = {9, 3, 7, 1, 6, 4, 8, 2, 5};
System.out.println("Original Array: " + Arrays.toString(arr));
mergeSort(arr);
System.out.println("Sorted Array: " + Arrays.toString(arr));
}
}
ব্যাখ্যা:
- MergeSortTask: এটি একটি RecursiveTask যা ForkJoinPool এর মাধ্যমে কাজের ভাগ করে দেয়।
- ForkJoinPool: এটি একটি পুল যা কাজের বিভাজন এবং সমন্বয় করে, এবং মুলত ছোট ছোট টাস্কগুলোকে সমান্তরালভাবে সম্পন্ন করতে সহায়তা করে।
আউটপুট:
Original Array: [9, 3, 7, 1, 6, 4, 8, 2, 5]
Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8, 9]
4. Parallel Searching Algorithm (Binary Search)
Binary Search হল একটি সাধারণ অনুসন্ধান অ্যালগরিদম যা O(log n) সময় জটিলতায় কাজ করে, তবে এটি শুধুমাত্র সাজানো অ্যারের জন্য কার্যকরী। যখন parallel করা হয়, তখন একাধিক থ্রেড ব্যবহার করে বিভিন্ন অংশে দ্রুত অনুসন্ধান করা যায়।
উদাহরণ: Parallel Binary Search in Java
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
public class ParallelBinarySearch {
public static int binarySearch(int[] arr, int key) {
ForkJoinPool pool = new ForkJoinPool();
return pool.invoke(new BinarySearchTask(arr, 0, arr.length - 1, key));
}
static class BinarySearchTask extends RecursiveTask<Integer> {
int[] arr;
int left, right, key;
BinarySearchTask(int[] arr, int left, int right, int key) {
this.arr = arr;
this.left = left;
this.right = right;
this.key = key;
}
@Override
protected Integer compute() {
if (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == key) {
return mid; // Key found
}
if (arr[mid] > key) {
return new BinarySearchTask(arr, left, mid - 1, key).fork().join(); // Search left
} else {
return new BinarySearchTask(arr, mid + 1, right, key).fork().join(); // Search right
}
}
return -1; // Key not found
}
}
public static void main(String[] args) {
int[] arr = {1, 3, 5, 7, 9, 11, 13, 15, 17};
int key = 7;
int index = binarySearch(arr, key);
if (index != -1) {
System.out.println("Element found at index: " + index);
} else {
System.out.println("Element not found");
}
}
}
ব্যাখ্যা:
- BinarySearchTask: এটি একটি RecursiveTask যা ইনপুট অ্যারে এবং কী-এর জন্য দুইটি সাব-অপারেশন ভাগ করে দেয় এবং fork এবং join মেথড ব্যবহার করে একে একে দুই অংশে অনুসন্ধান করে।
- ForkJoinPool: এটি parallel প্রক্রিয়াগুলিকে সমন্বয় করে এবং সকল সাব-অপারেশন সমাপ্ত হওয়া পর্যন্ত অপেক্ষা করে।
আউটপুট:
Element found at index: 3
5. Parallel Algorithms এর Efficiency
Parallel Algorithms এর Efficiency অনেকাংশে নির্ভর করে:
- Number of Processors: যদি প্রসেসরের সংখ্যা বেশি থাকে, তবে কাজ আরও দ্রুত সমাধান হতে পারে। তবে এর সাথে সিস্টেমের সীমাবদ্ধতা এবং কনটেক্সট সুইচিংয়ের সময় বৃদ্ধি পায়।
- Overhead: ছোট ছোট টাস্কগুলোর মধ্যে সমন্বয় এবং পারস্পরিক যোগাযোগের খরচ বেড়ে যায়, যা কার্যকারিতা কমিয়ে দিতে পারে।
- Task Division: কাজের সঠিকভাবে ভাগ করা হলে, কাজের সময় কমে যায় এবং কার্যকারিতা বৃদ্ধি পায়।
Parallel Algorithm Efficiency (General Case):
- Speedup: ব্যবহারিক ভাবে, speedup নির্ভর করে কাজের ভাগের প্রক্রিয়া এবং প্রসেসরের সংখ্যা (Amdahl's Law)।
- Scalability: সিস্টেমে আরও প্রসেসর যোগ করলে কর্মক্ষমতা কতটা বাড়ে সেটি scalability নির্দেশ করে।
Read more