Java Threads: Basics and Modern Usage


1. Basics of Threads
A thread in Java is a lightweight process that enables concurrent execution of code. It allows multiple tasks to run in parallel within a single application.
Key Concepts:
Thread Lifecycle: New → Runnable → Running → Terminated
Creating Threads:
By extending
Thread
and overridingrun()
.By implementing
Runnable
and passing it to aThread
instance.
// Extending Thread
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
// Using Runnable
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable thread is running");
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
Thread t2 = new Thread(new MyRunnable());
t2.start();
}
}
2. Modern Approach to Threads
Thread Pools (via
ExecutorService
) A thread pool manages a fixed number of reusable threads to execute tasks. This avoids the overhead of creating new threads for each task.import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 1; i <= 5; i++) { int task = i; executor.submit(() -> { System.out.println("Executing Task " + task + " by " + Thread.currentThread().getName()); }); } executor.shutdown(); } }
- Virtual Threads (Project Loom) Introduced in Java 19 as a preview, virtual threads are lightweight threads designed for high scalability. They simplify concurrent programming by reducing the cost of thread creation.
import java.util.concurrent.Executors;
public class VirtualThreadExample {
public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 1; i <= 5; i++) {
int task = i;
executor.submit(() -> {
System.out.println("Task " + task + " executed by " + Thread.currentThread());
});
}
}
}
}
3. Real-Life Example: Web Scraper
Imagine you need to scrape multiple web pages simultaneously.
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WebScraper {
public static void main(String[] args) {
String[] urls = {
"https://example.com",
"https://example.org",
"https://example.net"
};
// Step 1: Create a Thread Pool with 3 threads
//The fixed thread pool ensures there are exactly 3 reusable threads.
// Each thread is capable of executing one task at a time.
ExecutorService executor = Executors.newFixedThreadPool(3);
// Step 2: Submit tasks (scraping each URL) to the thread pool
for (String url : urls) {
// The submit method places the tasks in a task queue. The thread pool assigns these tasks to the available threads
executor.submit(() -> {
try {
// Step 3: Establish an HTTP connection to the URL
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("GET");
// Step 4: Fetch the response code to ensure the page is reachable
int responseCode = connection.getResponseCode();
// Step 5: Print success message
System.out.println("Fetched " + url + " with response code: " + responseCode);
} catch (Exception e) {
// Step 6: Handle errors gracefully
System.err.println("Failed to fetch " + url + ": " + e.getMessage());
}
});
}
// Step 7: Gracefully shut down the thread pool
executor.shutdown();
}
}
Interaction Between Threads:
Each thread operates independently and handles one URL at a time.
Since the thread pool has 3 threads, up to 3 tasks can run concurrently. If more tasks are submitted, they wait in the queue until a thread becomes available.
Interaction Between Threads
Concurrency:
The thread pool ensures multiple tasks (up to 3 in this case) run concurrently.
For example:
Thread 1 processes
https://example.com
.Thread 2 processes
https://example.org
.Thread 3 processes
https://example.net
.
Independence:
Each thread works independently and does not share state or variables with other threads.
However, if shared resources (e.g., a shared file or list) were used, proper synchronization mechanisms (like
synchronized
blocks orLock
objects) would be needed to avoid conflicts.
Task Queuing:
- If there were more URLs than threads, the additional tasks would be queued. As threads complete their current tasks, they pick up the next task from the queue.
Scalability:
- This approach is scalable since the thread pool can be adjusted to handle more threads if needed.
Improvements & Extensions
Handling Large Numbers of URLs:
- If you have hundreds of URLs, consider increasing the thread pool size or using virtual threads to avoid exhausting system resources.
Storing Results:
- Instead of printing the response code, you could store it in a thread-safe collection like
ConcurrentHashMap
.
- Instead of printing the response code, you could store it in a thread-safe collection like
Timeouts:
You can set timeouts for the HTTP connection to avoid threads hanging indefinitely:
connection.setConnectTimeout(5000); // 5 seconds connection.setReadTimeout(5000); // 5 seconds
Key Takeaways:
Basic Threads are simple but can lead to resource issues in large applications.
Thread Pools improve performance and resource management.
Virtual Threads provide a more scalable and modern approach to concurrency.
Using threads effectively requires understanding the task's nature and scalability needs.
Subscribe to my newsletter
Read articles from Mihai Popescu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
