π§΅ Java Thread Lifecycle and Creation: A Comprehensive Guide
π Introduction
In modern software development, the ability to execute multiple tasks simultaneously is crucial for creating efficient and responsive applications. Java's threading mechanism provides a powerful way to achieve concurrent execution, allowing programs to perform multiple operations in parallel.
Think of threads as independent workers in a factory - each can perform their task simultaneously while contributing to the same overall goal. This concurrent execution model is essential for:
- β‘ Improving application performance
- π₯οΈ Creating responsive user interfaces
- π Handling multiple network connections
- π Processing large datasets efficiently
In this comprehensive tutorial, we'll explore Java's thread lifecycle, different ways to create threads, and best practices for working with concurrent code. Whether you're building desktop applications, web servers, or data processing systems, understanding threads is a fundamental skill for any Java developer.
Let's begin our journey through the lifecycle of Java threads!
π The Six Thread States in Java
Java's Thread.State
enum defines six distinct states that a thread can be in during its lifecycle:
1. NEW
Thread thread = new Thread(); // Thread is in NEW state
- Description: A thread that has been created but not yet started is in the NEW state.
- Characteristics: The thread object exists, but no system resources have been allocated yet.
- Important: In this state, the thread's
start()
method has not been called.
2. RUNNABLE
thread.start(); // Thread moves from NEW to RUNNABLE
- Description: A thread that is ready to run is in the RUNNABLE state.
- Characteristics: The thread may be running or may be ready to run at any instant but is waiting for resource allocation from the system.
- Important: In Java, the RUNNABLE state combines what some other systems separate into "ready" and "running" states.
3. BLOCKED
// Thread becomes BLOCKED when trying to enter a synchronized block
// while another thread holds the lock
synchronized(sharedObject) {
// Critical section
}
- Description: A thread that is blocked waiting for a monitor lock is in the BLOCKED state.
- Characteristics: The thread is waiting to acquire a lock to enter or re-enter a synchronized block/method.
- Important: A thread transitions to this state when it attempts to enter a synchronized block but cannot obtain the lock because another thread holds it.
4. WAITING
// Thread enters WAITING state
object.wait();
// or
thread.join();
// or
LockSupport.park();
- Description: A thread that is waiting indefinitely for another thread to perform a particular action is in the WAITING state.
- Characteristics: The thread has called
wait()
,join()
(with no timeout), orLockSupport.park()
. - Important: A thread in this state is waiting without a timeout and will only continue when explicitly notified by another thread.
5. TIMED_WAITING
// Thread enters TIMED_WAITING state
Thread.sleep(1000);
// or
object.wait(1000);
// or
thread.join(1000);
// or
LockSupport.parkNanos(1000000000);
// or
LockSupport.parkUntil(deadline);
- Description: A thread that is waiting for another thread to perform an action for up to a specified waiting time is in the TIMED_WAITING state.
- Characteristics: The thread has called a method with a timeout parameter.
- Important: Unlike WAITING, a thread in TIMED_WAITING will automatically return to RUNNABLE after the specified timeout, even without notification.
6. TERMINATED
// Thread's run method completes or throws an exception
public void run() {
// Code completes execution
} // Thread enters TERMINATED state
- Description: A thread that has completed execution is in the TERMINATED state.
- Characteristics: The thread has either completed its
run()
method or thrown an exception that wasn't caught. - Important: Once a thread reaches this state, it cannot be restarted or brought back to any other state.
Thread State Transition Diagram
βββββββββββ
β NEW β
ββββββ¬βββββ
β start()
βΌ
βββββββββββββββββββ JVM scheduler βββββββββββββββ
β RUNNABLE βββββββββββββββββββββββΆβ RUNNING β
βββββββββ¬ββββββββββ ββββββββ¬ββββββββ
β² β
β β
β β
β β
β β
β β
βββββββββ΄ββββββββββ ββββββββΌββββββββ
β BLOCKED ββββββββββββββββββββββββ wait for lockβ
βββββββββββββββββββ ββββββββββββββββ
β² β
β β
β β
β β
βββββββββ΄ββββββββββ ββββββββΌββββββββ
β WAITING ββββββββββββββββββββββββ wait() β
βββββββββββββββββββ ββββββββββββββββ
β² β
β β
β β
β β
βββββββββ΄ββββββββββ ββββββββΌββββββββ
β TIMED_WAITING ββββββββββββββββββββββββ sleep(n) β
βββββββββββββββββββ ββββββββββββββββ
β
β
β
ββββββββΌββββββββ
β TERMINATED β
ββββββββββββββββ
State Transitions Example
Let's create a practical example that demonstrates the different thread states:
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
// Create a new thread (NEW state)
Thread thread = new Thread(() -> {
try {
// Simulate some work
Thread.sleep(1000); // TIMED_WAITING state
synchronized(ThreadStateDemo.class) {
// BLOCKED state if lock is held by another thread
ThreadStateDemo.class.wait(); // WAITING state
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Thread was interrupted");
}
});
// Print initial state
System.out.println("Initial state: " + thread.getState()); // NEW
// Start the thread (transition to RUNNABLE)
thread.start();
System.out.println("After start(): " + thread.getState()); // RUNNABLE
// Wait for thread to sleep
Thread.sleep(500);
System.out.println("During sleep(): " + thread.getState()); // TIMED_WAITING
// Create another thread that will acquire the lock
Thread lockingThread = new Thread(() -> {
synchronized(ThreadStateDemo.class) {
try {
Thread.sleep(2000); // Hold lock for 2 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// Wake up the first thread and make it try to acquire the lock
synchronized(ThreadStateDemo.class) {
lockingThread.start(); // Start thread that will hold the lock
Thread.sleep(100); // Give locking thread time to acquire lock
// Wake up the first thread from TIMED_WAITING
thread.interrupt();
Thread.sleep(100); // Give first thread time to try acquiring lock
System.out.println("Blocked on lock: " + thread.getState()); // BLOCKED
}
// Wait for threads to finish
lockingThread.join();
// Notify the waiting thread
synchronized(ThreadStateDemo.class) {
ThreadStateDemo.class.notify();
}
// Wait for thread to terminate
thread.join();
System.out.println("After completion: " + thread.getState()); // TERMINATED
}
}
This example demonstrates:
- Creating a thread (NEW state)
- Starting a thread (RUNNABLE state)
- Thread sleeping (TIMED_WAITING state)
- Thread waiting for a lock (BLOCKED state)
- Thread waiting indefinitely (WAITING state)
- Thread completion (TERMINATED state)
π οΈ Creating Threads in Java
Java provides multiple ways to create and work with threads. Let's explore each approach with detailed examples.
1. π§© Extending Thread Class
The most straightforward way to create a thread is by extending the Thread
class and overriding its run()
method.
Basic Example
public class ExtendThreadExample extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + getName());
// Perform task operations here
for (int i = 0; i < 5; i++) {
System.out.println(getName() + " count: " + i);
try {
// Pause for a bit to simulate work
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
return; // Exit the thread if interrupted
}
}
System.out.println(Thread.currentThread().getName() + " completed");
}
public static void main(String[] args) {
// Create thread instances
ExtendThreadExample thread1 = new ExtendThreadExample();
ExtendThreadExample thread2 = new ExtendThreadExample();
// Set thread names for better identification
thread1.setName("Thread-A");
thread2.setName("Thread-B");
// Start the threads
thread1.start(); // Calls the run() method in a new thread
thread2.start(); // Calls the run() method in another new thread
System.out.println("Main thread continues execution");
}
}
Detailed Explanation
- We create a custom class
ExtendThreadExample
that extends theThread
class - We override the
run()
method to define the code that will be executed in the thread - In the
main()
method, we create two instances of our custom thread class - We set names for the threads to make them easier to identify in the output
- We call the
start()
method on each thread, which creates a new thread of execution and calls therun()
method in that thread - The main thread continues execution independently of the two new threads
Advantages of Extending Thread
- Simple and straightforward approach
- Direct access to thread methods like
getName()
,getPriority()
, etc., without usingThread.currentThread()
- Good for simple, self-contained thread tasks
Disadvantages of Extending Thread
- Limited by Java's single inheritance model - if your class extends
Thread
, it cannot extend any other class - Less flexible for code reuse - the task code is tightly coupled with the thread code
- Cannot submit the same task to multiple threads or thread pools
2. π Implementing the Runnable Interface
The preferred way to create a thread in Java is by implementing the Runnable
interface. This approach separates the task code from the thread code, providing better flexibility and reusability.
Basic Example
public class RunnableDemo implements Runnable {
private final String name;
public RunnableDemo(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Runnable task is running in: " + Thread.currentThread().getName());
// Perform task operations here
for (int i = 0; i < 5; i++) {
System.out.println(name + " - count: " + i);
try {
// Pause for a bit to simulate work
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Task interrupted");
return; // Exit the task if interrupted
}
}
System.out.println(name + " task completed");
}
public static void main(String[] args) {
// Create Runnable task instances
Runnable task1 = new RunnableDemo("Task-A");
Runnable task2 = new RunnableDemo("Task-B");
// Create threads with the Runnable tasks
Thread thread1 = new Thread(task1, "Thread-1");
Thread thread2 = new Thread(task2, "Thread-2");
// Start the threads
thread1.start();
thread2.start();
System.out.println("Main thread continues execution");
// Using lambda expression (Java 8+)
Thread thread3 = new Thread(() -> {
System.out.println("Lambda Runnable running in: " + Thread.currentThread().getName());
for (int i = 0; i < 3; i++) {
System.out.println("Lambda - count: " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread-3");
thread3.start();
}
}
Detailed Explanation
- We create a class
RunnableDemo
that implements theRunnable
interface - We implement the
run()
method to define the task that will be executed in a thread - In the
main()
method, we create two instances of ourRunnable
implementation - We create
Thread
objects, passing theRunnable
instances to their constructors - We call the
start()
method on each thread, which creates a new thread of execution and calls therun()
method of the associatedRunnable
in that thread - We also demonstrate using a lambda expression to create a
Runnable
(available in Java 8+)
Advantages of Implementing Runnable
- Separates the task code from the thread code, promoting better design
- Not limited by Java's single inheritance model - your class can implement
Runnable
and still extend another class - More flexible for code reuse - the same
Runnable
can be submitted to multiple threads or thread pools - Better suited for concurrent designs and frameworks like the Executor Framework
- Can be implemented using lambda expressions in Java 8+
Disadvantages of Implementing Runnable
- Slightly more verbose than extending
Thread
(though lambda expressions mitigate this in modern Java) - No direct access to thread methods - must use
Thread.currentThread()
to access the current thread - Cannot return a result from the
run()
method (for this, useCallable
)
3. π Implementing Callable with Future
For tasks that need to return a result or throw a checked exception, Java provides the Callable
interface. Combined with the Future
interface, Callable
provides a powerful mechanism for asynchronous computation with results.
Basic Example
import java.util.concurrent.*;
public class CallableDemo {
public static void main(String[] args) {
// Create an ExecutorService with a fixed thread pool
ExecutorService executor = Executors.newFixedThreadPool(2);
// Create Callable tasks
Callable<Integer> task1 = () -> {
System.out.println("Task 1 executing in: " + Thread.currentThread().getName());
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
Thread.sleep(200); // Simulate work
}
return sum; // Return the result
};
Callable<String> task2 = () -> {
System.out.println("Task 2 executing in: " + Thread.currentThread().getName());
StringBuilder result = new StringBuilder();
for (char c = 'A'; c <= 'E'; c++) {
result.append(c);
Thread.sleep(300); // Simulate work
}
return result.toString(); // Return the result
};
try {
// Submit tasks to the executor and get Future objects
Future<Integer> future1 = executor.submit(task1);
Future<String> future2 = executor.submit(task2);
System.out.println("Tasks submitted, main thread continues...");
// Check if task1 is done (non-blocking)
System.out.println("Is task1 done? " + future1.isDone());
// Get result from task1 (blocking until complete)
Integer result1 = future1.get();
System.out.println("Task 1 result: " + result1);
// Get result from task2 with timeout
String result2 = future2.get(2, TimeUnit.SECONDS);
System.out.println("Task 2 result: " + result2);
} catch (InterruptedException e) {
System.out.println("Thread was interrupted");
} catch (ExecutionException e) {
System.out.println("Exception in task: " + e.getCause());
} catch (TimeoutException e) {
System.out.println("Timeout waiting for result");
} finally {
// Always shut down the executor
executor.shutdown();
}
}
}
Detailed Explanation
- We create an
ExecutorService
using theExecutors.newFixedThreadPool()
factory method - We define two
Callable
tasks using lambda expressions - one returning anInteger
and one returning aString
- We submit the tasks to the executor and receive
Future
objects that represent the pending results - We check if a task is done using
future.isDone()
(non-blocking) - We retrieve the results using
future.get()
(blocking until the task completes) - We demonstrate using a timeout with
future.get(timeout, unit)
- We properly handle exceptions and shut down the executor in a
finally
block
Advantages of Callable and Future
- Can return results from tasks
- Can throw checked exceptions from the
call()
method - Provides methods to check task status and control execution
- Integrates well with the Executor Framework
- Supports timeouts when waiting for results
Disadvantages of Callable and Future
- More complex than
Runnable
- Requires using an
ExecutorService
- Blocking on
get()
can cause responsiveness issues if not managed properly
4. π Using CompletableFuture (Java 8+)
For more advanced asynchronous programming, Java 8 introduced the CompletableFuture
class, which extends Future
and provides a rich set of methods for composing, combining, and handling asynchronous operations.
import java.util.concurrent.*;
public class CompletableFutureDemo {
public static void main(String[] args) {
// Create a CompletableFuture that runs asynchronously
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
System.out.println("Task running in: " + Thread.currentThread().getName());
Thread.sleep(1000); // Simulate work
return "Task completed successfully";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "Task was interrupted";
}
});
// Add a callback to be executed when the future completes
future.thenAccept(result -> {
System.out.println("Got result: " + result);
});
System.out.println("Main thread continues immediately...");
// Chain multiple operations
CompletableFuture<Integer> chainedFuture = CompletableFuture
.supplyAsync(() -> {
// First operation
return 10;
})
.thenApply(value -> {
// Transform the result
return value * 2;
})
.thenApply(value -> {
// Another transformation
return value + 5;
});
// Wait for the result
try {
Integer finalResult = chainedFuture.get(2, TimeUnit.SECONDS);
System.out.println("Final result: " + finalResult);
} catch (Exception e) {
e.printStackTrace();
}
// Combining multiple futures
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> combined = future1.thenCombine(future2, (result1, result2) -> {
return result1 + result2;
});
try {
System.out.println("Combined result: " + combined.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Advantages of CompletableFuture
- Non-blocking operations with callbacks
- Rich API for composing and combining asynchronous operations
- Exception handling with
exceptionally()
andhandle()
methods - Control over execution with custom Executor
- Better integration with functional programming style
π‘ Why Threading Matters: Real-World Use Cases
Understanding threading is crucial for developing efficient and responsive applications. Let's explore some common use cases where threading makes a significant difference.
1. π₯οΈ UI Responsiveness
One of the most common uses of threading is to keep user interfaces responsive while performing time-consuming operations.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ResponsiveUIExample extends Application {
@Override
public void start(Stage primaryStage) {
// UI components
Label statusLabel = new Label("Ready");
ProgressBar progressBar = new ProgressBar(0);
Button startButton = new Button("Start Long Task");
// Set up the action for the button
startButton.setOnAction(event -> {
// Disable the button while task is running
startButton.setDisable(true);
statusLabel.setText("Processing...");
// Run the long task in a background thread
new Thread(() -> {
try {
// Simulate a long-running task
for (int i = 0; i <= 100; i++) {
final int progress = i;
// Update UI on the JavaFX Application Thread
Platform.runLater(() -> {
progressBar.setProgress(progress / 100.0);
statusLabel.setText("Processing: " + progress + "%");
});
Thread.sleep(100); // Simulate work
}
// Task completed, update UI
Platform.runLater(() -> {
statusLabel.setText("Task completed!");
startButton.setDisable(false);
});
} catch (InterruptedException e) {
// Handle interruption
Platform.runLater(() -> {
statusLabel.setText("Task interrupted!");
startButton.setDisable(false);
});
Thread.currentThread().interrupt();
}
}).start();
});
// Create and show the scene
VBox root = new VBox(10, statusLabel, progressBar, startButton);
root.setPadding(new javafx.geometry.Insets(20));
primaryStage.setScene(new Scene(root, 300, 150));
primaryStage.setTitle("Responsive UI Example");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
In this example:
- The UI remains responsive while a long-running task executes in a background thread
- The progress is updated on the UI thread using
Platform.runLater()
- The user can still interact with the application during processing
2. π Network and I/O Operations
Threading is essential for handling network operations without blocking the main application flow.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class NetworkOperationsExample {
private final ExecutorService executor = Executors.newFixedThreadPool(5);
public Future<String> fetchWebContent(String urlString) {
return executor.submit(() -> {
HttpURLConnection connection = null;
try {
// Create connection
URL url = new URL(urlString);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
// Get response
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
throw new RuntimeException("HTTP error: " + responseCode);
}
// Read content
BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
reader.close();
return content.toString();
} finally {
if (connection != null) {
connection.disconnect();
}
}
});
}
public void shutdown() {
executor.shutdown();
}
public static void main(String[] args) {
NetworkOperationsExample example = new NetworkOperationsExample();
try {
// Fetch content from multiple URLs concurrently
Future<String> future1 = example.fetchWebContent("https://www.example.com");
Future<String> future2 = example.fetchWebContent("https://www.example.org");
// Process results as they become available
System.out.println("Waiting for results...");
String content1 = future1.get();
System.out.println("First website content length: " + content1.length());
String content2 = future2.get();
System.out.println("Second website content length: " + content2.length());
} catch (Exception e) {
e.printStackTrace();
} finally {
example.shutdown();
}
}
}
This example demonstrates:
- Using an
ExecutorService
to manage a pool of threads for network operations - Performing multiple HTTP requests concurrently
- Returning results asynchronously using
Future
- Proper resource cleanup with
shutdown()
3. π Data Processing
Threading can significantly improve performance when processing large datasets.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class ParallelDataProcessingExample {
// Simulate a large dataset
private static List<Integer> generateLargeDataset(int size) {
List<Integer> data = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
data.add(i);
}
return data;
}
// Process data sequentially
private static long processSequentially(List<Integer> data) {
long startTime = System.currentTimeMillis();
long sum = 0;
for (Integer value : data) {
// Simulate complex processing
sum += performComplexCalculation(value);
}
long endTime = System.currentTimeMillis();
System.out.println("Sequential processing result: " + sum);
return endTime - startTime;
}
// Process data in parallel
private static long processInParallel(List<Integer> data, int numThreads) {
long startTime = System.currentTimeMillis();
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
int chunkSize = data.size() / numThreads;
List<Future<Long>> futures = new ArrayList<>();
// Split the data into chunks and process each chunk in a separate thread
for (int i = 0; i < numThreads; i++) {
int startIndex = i * chunkSize;
int endIndex = (i == numThreads - 1) ? data.size() : (i + 1) * chunkSize;
List<Integer> chunk = data.subList(startIndex, endIndex);
futures.add(executor.submit(() -> {
long partialSum = 0;
for (Integer value : chunk) {
partialSum += performComplexCalculation(value);
}
return partialSum;
}));
}
// Combine results
long totalSum = 0;
try {
for (Future<Long> future : futures) {
totalSum += future.get();
}
} catch (Exception e) {
e.printStackTrace();
}
executor.shutdown();
long endTime = System.currentTimeMillis();
System.out.println("Parallel processing result: " + totalSum);
return endTime - startTime;
}
// Simulate a complex calculation
private static long performComplexCalculation(int value) {
// Simulate CPU-intensive work
long result = value;
for (int i = 0; i < 1000; i++) {
result = (result * 31 + i) % 1000000;
}
return result;
}
public static void main(String[] args) {
// Generate a large dataset
List<Integer> data = generateLargeDataset(100000);
// Process sequentially
long sequentialTime = processSequentially(data);
System.out.println("Sequential processing time: " + sequentialTime + " ms");
// Process in parallel
int numThreads = Runtime.getRuntime().availableProcessors();
System.out.println("Using " + numThreads + " threads");
long parallelTime = processInParallel(data, numThreads);
System.out.println("Parallel processing time: " + parallelTime + " ms");
// Calculate speedup
double speedup = (double) sequentialTime / parallelTime;
System.out.println("Speedup: " + String.format("%.2f", speedup) + "x");
}
}
This example shows:
- Splitting a large dataset into chunks for parallel processing
- Using multiple threads to process data concurrently
- Combining partial results to get the final result
- Measuring and comparing performance between sequential and parallel approaches
β Best Practices for Java Threading
Thread Creation and Management
-
Prefer Executor Framework over raw threads
// Avoid this new Thread(() -> performTask()).start(); // Prefer this ExecutorService executor = Executors.newFixedThreadPool(nThreads); executor.submit(() -> performTask());
-
Use thread pools appropriate for your workload
newFixedThreadPool
: Fixed number of threads, good for CPU-bound tasksnewCachedThreadPool
: Dynamically sized pool, good for I/O-bound tasks with variable loadnewSingleThreadExecutor
: Single worker thread, ensures sequential executionnewScheduledThreadPool
: For scheduled or periodic tasks
-
Name your threads for easier debugging
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat("worker-thread-%d") .build(); ExecutorService executor = Executors.newFixedThreadPool(10, namedThreadFactory);
-
Always shut down executors properly
try { // Use executor } finally { executor.shutdown(); try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } }
Thread Safety
-
Identify shared mutable state
- Any variable that can be accessed by multiple threads
- Fields in objects shared between threads
- Static variables
-
Use thread-safe collections
// Instead of Map<String, Data> dataMap = new HashMap<>(); // Use Map<String, Data> dataMap = new ConcurrentHashMap<>();
-
Minimize synchronization scope
// Bad: Locks the entire method public synchronized void processData() { // Method implementation } // Better: Locks only the critical section public void processData() { // Non-critical code here synchronized(this) { // Critical section only } // More non-critical code }
-
Prefer atomic classes for simple counters and flags
// Instead of private int counter = 0; public synchronized void increment() { counter++; } // Use private AtomicInteger counter = new AtomicInteger(0); public void increment() { counter.incrementAndGet(); }
-
Use immutable objects when possible
public final class ImmutableValue { private final int value; public ImmutableValue(int value) { this.value = value; } public int getValue() { return value; } public ImmutableValue add(int amount) { return new ImmutableValue(value + amount); } }
Exception Handling
-
Always handle InterruptedException properly
// Bad try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); // Swallows the interruption } // Good try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Restore the interrupt status // Handle the interruption appropriately }
-
Set uncaught exception handlers
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { System.err.println("Uncaught exception in thread " + thread.getName() + ": " + throwable.getMessage()); // Log the exception, notify monitoring systems, etc. });
-
Use try-finally for resource cleanup
Lock lock = new ReentrantLock(); try { lock.lock(); // Critical section } finally { lock.unlock(); // Always release the lock }
β οΈ Common Pitfalls and How to Avoid Them
1. Deadlocks
Deadlocks occur when two or more threads are blocked forever, each waiting for the other to release a lock.
Example of a Deadlock
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized(lock1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized(lock2) {
System.out.println("Thread 1: Holding lock 1 & 2...");
}
}
}
public void method2() {
synchronized(lock2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized(lock1) {
System.out.println("Thread 2: Holding lock 1 & 2...");
}
}
}
public static void main(String[] args) {
DeadlockExample deadlock = new DeadlockExample();
new Thread(() -> deadlock.method1()).start();
new Thread(() -> deadlock.method2()).start();
}
}
How to Avoid Deadlocks
-
Always acquire locks in the same order
// Fix the deadlock by ensuring consistent lock ordering public void method2Fixed() { synchronized(lock1) { synchronized(lock2) { // Critical section } } }
-
Use tryLock with timeout
Lock lock1 = new ReentrantLock(); Lock lock2 = new ReentrantLock(); public void executeTask() { try { if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) { try { if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) { try { // Critical section } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }
-
Avoid nested locks when possible
- Redesign your code to minimize the need for multiple locks
- Use higher-level concurrency utilities
2. Race Conditions
Race conditions occur when the behavior of a program depends on the relative timing of events, such as the order in which threads execute.
Example of a Race Condition
public class RaceConditionExample {
private int counter = 0;
public void increment() {
counter++; // This is not an atomic operation!
}
public int getCounter() {
return counter;
}
public static void main(String[] args) throws InterruptedException {
RaceConditionExample example = new RaceConditionExample();
// Create multiple threads that increment the counter
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increment();
}
});
threads[i].start();
}
// Wait for all threads to complete
for (Thread thread : threads) {
thread.join();
}
// The expected value is 1,000,000
System.out.println("Counter value: " + example.getCounter());
// Actual value will likely be less due to race conditions
}
}
How to Avoid Race Conditions
-
Use synchronization
public synchronized void increment() { counter++; }
-
Use atomic variables
private AtomicInteger counter = new AtomicInteger(0); public void increment() { counter.incrementAndGet(); }
-
Use thread-local variables when appropriate
private ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0); public void incrementLocal() { threadLocalCounter.set(threadLocalCounter.get() + 1); }
3. Thread Leaks
Thread leaks occur when threads are created but never terminated, consuming system resources.
Common Causes of Thread Leaks
- Not shutting down executor services
- Threads blocked indefinitely
- Threads with infinite loops
How to Prevent Thread Leaks
-
Always shut down executor services
ExecutorService executor = Executors.newFixedThreadPool(10); try { // Use the executor } finally { executor.shutdown(); }
-
Use daemon threads for background tasks
Thread daemon = new Thread(() -> { while (true) { // Background work } }); daemon.setDaemon(true); // JVM will exit when all non-daemon threads finish daemon.start();
-
Implement proper cancellation mechanisms
private volatile boolean running = true; public void stopTask() { running = false; } public void runTask() { new Thread(() -> { while (running) { // Task logic } }).start(); }
π Key Takeaways
-
Thread Lifecycle: Understand the six states of a thread (NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED) and how threads transition between them.
-
Thread Creation: Choose the appropriate method for creating threads based on your needs:
- Extend
Thread
for simple, self-contained threads - Implement
Runnable
for better design and flexibility - Use
Callable
withFuture
when you need to return results - Consider
CompletableFuture
for advanced asynchronous operations
- Extend
-
Thread Safety: Always identify shared mutable state and protect it using appropriate synchronization mechanisms:
- Synchronized blocks/methods
- Locks from
java.util.concurrent.locks
- Atomic variables
- Thread-safe collections
- Immutable objects
-
Resource Management: Properly manage thread resources to prevent leaks:
- Always shut down executor services
- Handle thread interruptions correctly
- Use try-finally blocks for cleanup
- Consider using daemon threads for background tasks
-
Concurrency Issues: Be aware of common concurrency issues and how to avoid them:
- Deadlocks: Acquire locks in a consistent order
- Race conditions: Use proper synchronization
- Thread starvation: Ensure fair access to resources
- Memory visibility: Understand the Java Memory Model
π― Exercises and Mini-Projects
Exercise 1: Thread State Observer
Create a program that demonstrates and monitors all thread states. The program should:
- Create a thread that goes through different states
- Have another thread that monitors and reports the state changes
- Demonstrate all six thread states
Solution
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadStateObserverExercise {
private static final Lock lock = new ReentrantLock();
private static final Object waiter = new Object();
private static volatile boolean keepRunning = true;
public static void main(String[] args) throws InterruptedException {
// Thread to be observed
Thread observedThread = new Thread(() -> {
try {
// Will go to TIMED_WAITING
Thread.sleep(1000);
// Will go to BLOCKED
synchronized(lock) {
System.out.println("Acquired the lock");
}
// Will go to WAITING
synchronized(waiter) {
waiter.wait();
}
// Will go back to RUNNABLE
while (keepRunning) {
// Busy work to stay in RUNNABLE
Math.random() * Math.random();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "ObservedThread");
// Observer thread
Thread observer = new Thread(() -> {
Thread.State previousState = null;
Thread.State currentState;
while (observedThread.isAlive() ||
observedThread.getState() != Thread.State.TERMINATED) {
currentState = observedThread.getState();
if (currentState != previousState) {
System.out.println(observedThread.getName() +
" state changed to: " + currentState);
previousState = currentState;
}
try {
Thread.sleep(100); // Check every 100ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "ObserverThread");
// Start the observer
observer.start();
// Print initial state
System.out.println("Initial state: " + observedThread.getState());
// Start the observed thread
observedThread.start();
// Acquire the lock to force BLOCKED state
lock.lock();
Thread.sleep(2000); // Give time for observed thread to try acquiring the lock
lock.unlock();
// Wait for the thread to enter WAITING state
Thread.sleep(1000);
// Notify to exit WAITING state
synchronized(waiter) {
waiter.notify();
}
// Let it run for a bit
Thread.sleep(1000);
// Signal to terminate
keepRunning = false;
// Wait for threads to finish
observedThread.join();
observer.join();
System.out.println("Exercise completed");
}
}
Exercise 2: Parallel File Processor
Create a program that reads multiple text files in parallel, counts the occurrences of a specific word in each file, and aggregates the results.
Requirements:
- Accept a list of file paths and a target word
- Process each file in a separate thread
- Count occurrences of the target word in each file
- Aggregate and display the total count and per-file counts
- Implement proper exception handling and resource management
Solution
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class ParallelFileProcessorExercise {
public static void main(String[] args) {
// Sample files to process
List<String> filePaths = new ArrayList<>();
filePaths.add("file1.txt");
filePaths.add("file2.txt");
filePaths.add("file3.txt");
String targetWord = "Java";
// Create sample files with content for testing
createSampleFiles(filePaths);
// Process files and get results
Map<String, Integer> results = processFiles(filePaths, targetWord);
// Display results
System.out.println("Word count results for '" + targetWord + "':");
int totalCount = 0;
for (Map.Entry<String, Integer> entry : results.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue() + " occurrences");
totalCount += entry.getValue();
}
System.out.println("Total occurrences: " + totalCount);
}
public static Map<String, Integer> processFiles(List<String> filePaths, String targetWord) {
ExecutorService executor = Executors.newFixedThreadPool(
Math.min(filePaths.size(), Runtime.getRuntime().availableProcessors()));
Map<String, Future<Integer>> futures = new ConcurrentHashMap<>();
// Submit tasks to count word occurrences in each file
for (String filePath : filePaths) {
futures.put(filePath, executor.submit(() -> countWordOccurrences(filePath, targetWord)));
}
// Collect results
Map<String, Integer> results = new ConcurrentHashMap<>();
for (Map.Entry<String, Future<Integer>> entry : futures.entrySet()) {
try {
results.put(entry.getKey(), entry.getValue().get());
} catch (InterruptedException | ExecutionException e) {
results.put(entry.getKey(), -1); // Indicate error
System.err.println("Error processing file " + entry.getKey() + ": " + e.getMessage());
}
}
// Shutdown the executor
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
return results;
}
private static int countWordOccurrences(String filePath, String targetWord) {
int count = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
// Simple word counting (could be improved with regex for better word boundary detection)
String[] words = line.split("\\s+");
for (String word : words) {
if (word.equalsIgnoreCase(targetWord)) {
count++;
}
}
}
} catch (IOException e) {
System.err.println("Error reading file " + filePath + ": " + e.getMessage());
throw new RuntimeException("File processing failed", e);
}
return count;
}
// Helper method to create sample files for testing
private static void createSampleFiles(List<String> filePaths) {
String[] contents = {
"Java is a programming language.\nJava is widely used.\nJava runs on many platforms.",
"Python and Java are popular languages.\nJava has strong typing.",
"C++ is faster than Java.\nBut Java has garbage collection.\nMany developers prefer Java."
};
for (int i = 0; i < filePaths.size(); i++) {
try {
Path path = Paths.get(filePaths.get(i));
Files.write(path, contents[i].getBytes());
} catch (IOException e) {
System.err.println("Error creating sample file: " + e.getMessage());
}
}
}
}