মাল্টিথ্রেডিং এবং কনকারেন্সি সঠিকভাবে কাজ করছে কিনা তা যাচাই করা এবং সমস্যা সনাক্ত করার জন্য পরীক্ষা এবং ডিবাগিং গুরুত্বপূর্ণ। কনকারেন্সির ক্ষেত্রে সাধারণ সমস্যা যেমন ডেডলক, রেস কন্ডিশন, এবং থ্রেড সিঙ্ক্রোনাইজেশন ঠিকভাবে মোকাবিলা করতে সঠিক টেস্টিং এবং ডিবাগিং কৌশল ব্যবহার করা প্রয়োজন।
থ্রেড সেফটি নিশ্চিত করার জন্য টেস্ট করা:
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getValue() {
return count.get();
}
}
public class ThreadSafetyTest {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final Counter Value: " + counter.getValue());
}
}
একই সময়ে একাধিক থ্রেড চালিয়ে সিস্টেমে উচ্চ চাপ সৃষ্টি করা। উদাহরণস্বরূপ:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StressTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
Runnable task = () -> {
System.out.println("Task executed by: " + Thread.currentThread().getName());
};
for (int i = 0; i < 100; i++) {
executor.submit(task);
}
executor.shutdown();
}
}
রেস কন্ডিশন সনাক্ত করার জন্য টেস্ট করা:
class SharedResource {
private int counter = 0;
public void increment() {
counter++;
}
public int getCounter() {
return counter;
}
}
public class RaceConditionTest {
public static void main(String[] args) throws InterruptedException {
SharedResource resource = new SharedResource();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
resource.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final Counter Value: " + resource.getCounter());
}
}
সমস্যা সমাধান: synchronized
ব্লক ব্যবহার করে রেস কন্ডিশন রোধ করুন।
Thread Dump ব্যবহার করে ডেডলক সনাক্ত করা।
কমান্ড:
jstack <process_id>
ডেডলক এবং থ্রেড স্টেট পর্যবেক্ষণের জন্য:
Logging Frameworks (যেমন SLF4J
বা Log4j
) ব্যবহার করুন:
import java.util.logging.Logger;
public class LoggingExample {
private static final Logger logger = Logger.getLogger(LoggingExample.class.getName());
public static void main(String[] args) {
Thread thread = new Thread(() -> {
logger.info("Thread started: " + Thread.currentThread().getName());
});
thread.start();
}
}
JUnit এর মাধ্যমে মাল্টিথ্রেডেড টেস্ট পরিচালনা করা:
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.junit.jupiter.api.Assertions.assertEquals;
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getValue() {
return count;
}
}
public class JUnitConcurrencyTest {
@Test
public void testConcurrentIncrement() throws InterruptedException {
Counter counter = new Counter();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executor.submit(counter::increment);
}
executor.shutdown();
// Wait for all threads to finish
while (!executor.isTerminated()) {}
assertEquals(100, counter.getValue());
}
}
Thread Weaver একটি ওপেন সোর্স টুল, যা কনকারেন্সি টেস্টিং সহজ করে।
কৌশল | বৈশিষ্ট্য |
---|---|
Thread Safety Test | প্রতিটি থ্রেড সঠিকভাবে কাজ করছে কিনা যাচাই। |
Stress Test | উচ্চ চাপের পরিস্থিতিতে পারফরম্যান্স পরীক্ষা। |
Deadlock Debugging | থ্রেড ডাম্প এবং ডিবাগার দিয়ে ডেডলক সনাক্ত করা। |
Logging | সমস্যা পর্যবেক্ষণ এবং সনাক্ত করার জন্য লগিং ব্যবহার। |
কনকারেন্সি সম্পর্কিত সমস্যা সমাধানের জন্য সঠিক টেস্টিং এবং ডিবাগিং টুল এবং কৌশল ব্যবহার অত্যন্ত গুরুত্বপূর্ণ। এটি নিশ্চিত করে যে মাল্টিথ্রেডিং অ্যাপ্লিকেশন সঠিক, কার্যকর এবং নির্ভরযোগ্য।
জাভা কনকারেন্সি (Java Concurrency) প্রোগ্রামের জন্য Unit Testing অনেক বেশি চ্যালেঞ্জিং হতে পারে, কারণ এতে মাল্টিথ্রেডিং, সিঙ্ক্রোনাইজেশন, এবং টাইমিং সমস্যাগুলি রয়েছে। এই সমস্যাগুলো শনাক্ত ও সমাধান করার জন্য কিছু বিশেষ টেকনিক এবং টুল ব্যবহার করা হয়।
উদাহরণ:
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ThreadSafeCounter {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}
public int getCount() {
return counter.get();
}
}
public class ThreadSafeCounterTest {
@Test
void testThreadSafeIncrement() throws InterruptedException {
ThreadSafeCounter counter = new ThreadSafeCounter();
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 1000; i++) {
executor.submit(counter::increment);
}
executor.shutdown();
while (!executor.isTerminated()) {
// Wait for all threads to complete
}
assertEquals(1000, counter.getCount());
}
}
উদাহরণ:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static java.time.Duration.ofSeconds;
public class TimeoutTest {
@Test
void testWithTimeout() {
assertTimeout(ofSeconds(5), () -> {
// টাইম সীমার মধ্যে কাজ শেষ করতে হবে
Thread.sleep(4000);
});
}
}
উদাহরণ:
import org.junit.jupiter.api.RepeatedTest;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class RepeatedExecutionTest {
private volatile boolean sharedResource = false;
@RepeatedTest(10)
void testSharedResourceAccess() {
Thread thread1 = new Thread(() -> sharedResource = true);
Thread thread2 = new Thread(() -> assertTrue(sharedResource));
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Mockito উদাহরণ:
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import java.util.concurrent.ExecutorService;
public class MockingTest {
@Test
void testExecutorService() {
ExecutorService executor = mock(ExecutorService.class);
executor.submit(() -> System.out.println("Task executed"));
verify(executor, times(1)).submit(any(Runnable.class));
}
}
Deadlock টেস্ট উদাহরণ:
import org.junit.jupiter.api.Test;
public class DeadlockTest {
@Test
void testDeadlock() throws InterruptedException {
final Object lock1 = new Object();
final Object lock2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lock2) {
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
synchronized (lock1) {
}
}
});
thread1.start();
thread2.start();
thread1.join(1000);
thread2.join(1000);
System.out.println("Test completed");
}
}
উদাহরণ:
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StressTest {
@Test
void testStress() throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
// Simulate workload
System.out.println(Thread.currentThread().getName() + " working");
});
}
executor.shutdown();
while (!executor.isTerminated()) {
// Wait for all threads to finish
}
}
}
উদাহরণ:
import org.junit.jupiter.api.Test;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ForkJoinTest {
static class SumTask extends RecursiveTask<Integer> {
private final int[] numbers;
private final int start, end;
public SumTask(int[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= 2) {
return numbers[start] + numbers[end - 1];
} else {
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(numbers, start, mid);
SumTask rightTask = new SumTask(numbers, mid, end);
leftTask.fork();
return rightTask.compute() + leftTask.join();
}
}
}
@Test
void testForkJoinPool() {
ForkJoinPool pool = new ForkJoinPool();
int[] numbers = {1, 2, 3, 4, 5, 6};
SumTask task = new SumTask(numbers, 0, numbers.length);
int result = pool.invoke(task);
assertEquals(21, result);
}
}
timeout
এবং পর্যাপ্ত লজিক রাখুন।java.util.concurrent
প্যাকেজের ক্লাসগুলো পরীক্ষা করুন।জাভা কনকারেন্সি প্রোগ্রামের জন্য Unit Testing চ্যালেঞ্জিং, তবে সঠিক টেকনিক এবং টুল ব্যবহার করলে এটি কার্যকরীভাবে করা সম্ভব। Repeated Execution, Mocking, Stress Testing, এবং ForkJoinPool ভিত্তিক টেস্ট দিয়ে কনকারেন্ট কোডের সমস্যা সনাক্ত এবং সমাধান করা যায়।
মাল্টিথ্রেডেড কোড টেস্ট করা চ্যালেঞ্জিং কারণ এটি থ্রেড ইন্টারঅ্যাকশন এবং কনকারেন্সি সমস্যা যেমন Deadlock, Race Condition, এবং Thread-Safety নিশ্চিত করতে হয়। JUnit এবং Mockito একত্রে ব্যবহার করে মাল্টিথ্রেডেড কোড সহজেই টেস্ট করা সম্ভব।
একটি Counter
ক্লাস টেস্ট করা যেটি থ্রেড-সেফ।
Counter Class:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
JUnit টেস্ট:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CounterTest {
@Test
public void testCounterWithMultipleThreads() throws InterruptedException {
Counter counter = new Counter();
Runnable task = counter::increment;
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
assertEquals(2, counter.getCount());
}
}
একটি কোডে Race Condition রয়েছে কিনা তা টেস্ট করা।
Unsynchronized Counter:
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
JUnit টেস্ট:
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class UnsafeCounterTest {
@Test
public void testRaceCondition() throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(counter::increment);
}
executor.shutdown();
Thread.sleep(100); // অপেক্ষা করুন সব টাস্ক শেষ হওয়া পর্যন্ত
// Race Condition এর কারণে ফলাফল অপ্রত্যাশিত হতে পারে
assertNotEquals(1000, counter.getCount());
}
}
Service Class:
public class MyService {
public void performTask() {
System.out.println("Task performed by " + Thread.currentThread().getName());
}
}
Test Class:
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class MyServiceTest {
@Test
public void testPerformTaskWithMockito() throws InterruptedException {
MyService myService = Mockito.mock(MyService.class);
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.submit(myService::performTask);
}
executor.shutdown();
Thread.sleep(100); // অপেক্ষা করুন টাস্ক শেষ হওয়া পর্যন্ত
// Verify that performTask was called 5 times
verify(myService, times(5)).performTask();
}
}
CompletableFuture Example:
import java.util.concurrent.CompletableFuture;
public class AsyncService {
public CompletableFuture<String> fetchData() {
return CompletableFuture.supplyAsync(() -> "Data");
}
}
JUnit টেস্ট:
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class AsyncServiceTest {
@Test
public void testFetchData() throws ExecutionException, InterruptedException {
AsyncService service = new AsyncService();
CompletableFuture<String> future = service.fetchData();
// Wait for the result and validate
assertEquals("Data", future.get());
}
}
join()
বা ExecutorService.shutdown()
ব্যবহার করে থ্রেড শেষ হওয়া নিশ্চিত করুন।Thread.sleep()
বা await()
ব্যবহার করুন।CompletableFuture
বা কলব্যাক মেকানিজম সহজেই টেস্ট করা যায়।JUnit এবং Mockito ব্যবহার করে মাল্টিথ্রেডেড কোড টেস্ট করা কার্যকর এবং সহজ। এটি ডেডলক, রেস কন্ডিশন এবং থ্রেড-সেফটি ইস্যু সনাক্ত করতে সাহায্য করে। সঠিকভাবে টেস্ট কাঠামো সেটআপ করলে মাল্টিথ্রেডেড অ্যাপ্লিকেশন উন্নত এবং নির্ভরযোগ্য হয়।
জাভার মাল্টিথ্রেডেড প্রোগ্রামিংয়ে Deadlock, Race Condition, এবং Performance Issues সাধারণ সমস্যা। এগুলো সঠিকভাবে চিহ্নিত করা এবং প্রতিরোধ করা উন্নত ও কার্যকর প্রোগ্রামিংয়ের জন্য গুরুত্বপূর্ণ।
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1: Acquired lock 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread 2: Acquired lock 1");
}
}
});
thread1.start();
thread2.start();
}
}
আউটপুট:
Thread 1: Holding lock 1...
Thread 2: Holding lock 2...
থ্রেডগুলো একে অপরের সম্পদের জন্য অপেক্ষা করে এবং অগ্রসর হয় না।
tryLock
ব্যবহার করা: টাইমআউট সেট করে।import java.util.concurrent.locks.ReentrantLock;
public class AvoidDeadlock {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
if (lock1.tryLock() && lock2.tryLock()) {
System.out.println("Thread 1: Acquired both locks");
}
} finally {
lock1.unlock();
lock2.unlock();
}
});
Thread thread2 = new Thread(() -> {
try {
if (lock2.tryLock() && lock1.tryLock()) {
System.out.println("Thread 2: Acquired both locks");
}
} finally {
lock2.unlock();
lock1.unlock();
}
});
thread1.start();
thread2.start();
}
}
public class RaceConditionExample {
private static int counter = 0;
public static void main(String[] args) {
Runnable incrementTask = () -> {
for (int i = 0; i < 1000; i++) {
counter++;
}
};
Thread thread1 = new Thread(incrementTask);
Thread thread2 = new Thread(incrementTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Counter: " + counter);
}
}
সম্ভাব্য আউটপুট:
Final Counter: < 2000
কারণ, থ্রেডগুলোর অপারেশন একটি নির্দিষ্ট ক্রম মেনে চলে না।
synchronized
ব্যবহার করা।AtomicInteger
ব্যবহার করে।ReentrantLock
ব্যবহার করা।import java.util.concurrent.atomic.AtomicInteger;
public class FixedRaceCondition {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
Runnable incrementTask = () -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
};
Thread thread1 = new Thread(incrementTask);
Thread thread2 = new Thread(incrementTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Counter: " + counter.get());
}
}
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static int sharedData = 0;
public static void main(String[] args) {
Runnable writeTask = () -> {
lock.writeLock().lock();
try {
sharedData++;
System.out.println("Data written: " + sharedData);
} finally {
lock.writeLock().unlock();
}
};
Runnable readTask = () -> {
lock.readLock().lock();
try {
System.out.println("Data read: " + sharedData);
} finally {
lock.readLock().unlock();
}
};
new Thread(writeTask).start();
new Thread(readTask).start();
}
}
ExecutorService
ব্যবহার করে থ্রেড ম্যানেজমেন্ট উন্নত করা।import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
Runnable task = () -> {
System.out.println("Task executed by: " + Thread.currentThread().getName());
};
for (int i = 0; i < 10; i++) {
executor.submit(task);
}
executor.shutdown();
}
}
tryLock
ব্যবহার করে এড়ানো যায়।Atomic
ভ্যারিয়েবল বা লক ব্যবহার করে সমাধান করা যায়।ExecutorService
ব্যবহার।এই সমস্যা চিহ্নিত করা ও সমাধান করার মাধ্যমে জাভা অ্যাপ্লিকেশনের কার্যকারিতা ও নির্ভরযোগ্যতা বাড়ানো সম্ভব।
জাভার কনকারেন্সি অ্যাপ্লিকেশন তৈরি করতে গিয়ে বিভিন্ন সমস্যা যেমন Deadlocks, Race Conditions, এবং Thread Safety Issues হতে পারে। এগুলো সনাক্ত ও সমাধান করার জন্য সঠিক টুলস এবং কৌশল প্রয়োজন।
Thread Dump চলমান অ্যাপ্লিকেশনের সমস্ত থ্রেডের অবস্থা এবং স্ট্যাক ট্রেস প্রদান করে। এটি Deadlock বা Thread Blocking সমস্যাগুলি চিহ্নিত করতে কার্যকর।
jstack
Command ব্যবহার:
jstack <pid>
কোডের মাধ্যমে Thread Dump তৈরি:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class ThreadDumpExample {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, true);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.toString());
}
}
}
Deadlock শনাক্ত করতে ThreadMXBean
এবং jstack
ব্যবহার করতে পারেন।
Deadlock চেক করার কোড:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DeadlockDetection {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
System.out.println("Deadlocked Threads:");
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.toString());
}
} else {
System.out.println("No Deadlocks detected.");
}
}
}
Proper Logging: থ্রেডের গুরুত্বপূর্ণ অপারেশনগুলোর আগে ও পরে লগ রাখুন।
System.out.println(Thread.currentThread().getName() + " is acquiring lock");
ডিবাগিং টুল যেমন IntelliJ IDEA বা Eclipse ব্যবহার করে ব্রেকপয়েন্ট সেট করুন।
Thread Debugging Tips:
Race Condition সনাক্ত করার জন্য নিচের টুলস এবং কৌশল ব্যবহার করুন:
ConcurrentHashMap
, CopyOnWriteArrayList
এর মতো থ্রেড-সেফ ডেটা স্ট্রাকচার ব্যবহার করুন।synchronized
ব্লক ব্যবহার করুন।Race Condition উদাহরণ:
public class RaceConditionExample {
private int counter = 0;
public void increment() {
counter++;
}
public static void main(String[] args) throws InterruptedException {
RaceConditionExample example = new RaceConditionExample();
Thread thread1 = new Thread(example::increment);
Thread thread2 = new Thread(example::increment);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Counter: " + example.counter); // অপ্রত্যাশিত ফলাফল
}
}
সমাধান:
AtomicInteger
ব্যবহার করুন।public synchronized void increment() {
counter++;
}
ReentrantLock
এবং tryLock()
ব্যবহার করে লকিং উন্নত করুন।import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
Runnable task = () -> {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock");
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " could not acquire the lock");
}
};
new Thread(task).start();
new Thread(task).start();
}
}
Thread Pool এর সাথে সঠিক কনফিগারেশন নিশ্চিত করুন:
newFixedThreadPool(10)
)।jstack
, VisualVM, এবং Logging ব্যবহার করে থ্রেডের অবস্থা বিশ্লেষণ।synchronized
, ReentrantLock
, এবং Thread-safe Collections ব্যবহার।উপরোক্ত কৌশল ও টুলস ব্যবহার করে জাভা কনকারেন্সি অ্যাপ্লিকেশনের সমস্যা কার্যকরভাবে ডিবাগ ও ট্রাবলশুট করা যায়।
Read more