Mastering Java Multithreading Thread Pools, Callable, Future, and Concurrency Utilities
Java multithreading becomes more powerful and efficient when you start using thread pools, Callable, Future, and built-in concurrency utilities. These features help manage multiple threads better, especially in large applications.
What is a Thread Pool?
A Thread Pool is a group of pre-created threads that can be reused to perform tasks. It avoids the overhead of creating a new thread every time and improves performance.
https://images.app.goo.gl/1CkeahkMgjNLhFh88
A thread pool is a group of worker threads managed by the Java runtime that are reused to execute multiple tasks, improving performance and resource management.
Using ExecutorService to Run Multiple Tasks
package ayshriv;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MasteringBackend {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Task 1 running"));
executor.submit(() -> System.out.println("Task 2 running"));
executor.submit(() -> System.out.println("Task 3 running"));
executor.shutdown();
}
}
Output:
Task 1 running
Task 2 running
Task 3 running
(Note: Order may vary)
In this above code, we created a thread pool with 2 threads using Executors.newFixedThreadPool(2). We submitted 3 tasks, and the thread pool handles them efficiently using available threads. The third task waits if both threads are busy.
What is Callable and Future in Java?
Callable is like Runnable but returns a result.
Future is used to get the result of a Callable after it's done.
Callable
is like a Runnable
but can return a result or throw an exception. Future
is used to retrieve the result of the Callable
once it's done.
Using Callable and Future
package ayshriv;
import java.util.concurrent.*;
public class MasteringBackend {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> task = () -> {
Thread.sleep(1000);
return "Result from Callable";
};
Future<String> future = executor.submit(task);
System.out.println("Doing other work...");
String result = future.get(); // Waits for Callable to finish
System.out.println("Callable Result: " + result);
executor.shutdown();
}
}
Output:
Doing other work...
Callable Result: Result from Callable
In this above code, we used Callable to return a string result after a delay. While the callable runs in the background, the main thread does other work. The future.get() method waits and fetches the result once the task is complete.
Using ScheduledExecutorService
You can schedule tasks to run after a delay or repeatedly.
ScheduledExecutorService with Delay
package ayshriv;
import java.util.concurrent.*;
public class MasteringBackend {
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Task executed after delay");
scheduler.schedule(task, 2, TimeUnit.SECONDS);
scheduler.shutdown();
}
}
Output:
Task executed after delay
(Appears after approximately 2 seconds)
In this above code, we used ScheduledExecutorService to schedule a task after 2 seconds. This is useful for reminders, retries, or timeout actions in real-time applications.
Using CountDownLatch
CountDownLatch is a utility that waits until all required tasks are finished before moving forward.
Waiting for 3 Threads to Finish
package ayshriv;
import java.util.concurrent.CountDownLatch;
public class MasteringBackend {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " completed");
latch.countDown();
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
latch.await(); // Wait for all 3 threads
System.out.println("All tasks finished. Proceeding...");
}
}
Output:
Thread-0 completed
Thread-1 completed
Thread-2 completed
All tasks finished. Proceeding...
In this above code, we used CountDownLatch(3) to wait until 3 threads complete their tasks. Only after all threads call countDown(), the main thread resumes and prints the final message.
Using CyclicBarrier
CyclicBarrier allows multiple threads to wait for each other to reach a common barrier point.
A synchronization tool that allows a group of threads to wait for each other to reach a common barrier point before continuing execution.
Threads Sync at Barrier
package ayshriv;
import java.util.concurrent.*;
public class MasteringBackend {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () ->
System.out.println("All threads reached barrier. Running final task."));
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " is waiting at barrier");
try {
barrier.await(); // Wait for all threads
} catch (Exception e) {
e.printStackTrace();
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
Output:
Thread-0 is waiting at barrier
Thread-1 is waiting at barrier
Thread-2 is waiting at barrier
All threads reached barrier. Running final task.
In this above code, all 3 threads wait at the barrier. When all threads reach the barrier, the barrier action runs. This is useful in parallel processing where you want all threads to finish a phase together before moving to the next.