Java provides several mechanisms for creating and managing multithreaded applications. This guide covers all official approaches, from basic to advanced, with practical examples and comparison.
Overview of Implementation Methods
Java defines two fundamental approaches for multithreaded programming, though real-world applications typically use one of several衍生 forms:
| Approach | Core Mechanism | Since | Key Characteristics |
|---|---|---|---|
| Extend Thread class | Override run() method | JDK 1.0+ | Simple, but imposes single inheritance limitasion |
| Implement Runnable | Override run(), decouple task from thread | JDK 1.0+ | Flexible, recommended approach |
| Implement Callable + FutureTask | Generic return type, exception handling | JDK 1.5+ | Supports return values and checked exceptions |
| Thread Pool (Executor) | Reusable threads, task queuing | JDK 1.5+ | High performance, production-ready |
| Timer/TimerTask | Scheduled task execution | JDK 1.3+ | Simple scheduling, single-threaded |
Approach 1: Extending the Thread Class
How It Works
The Thread class represents a thread of execution in Java. By extending Thread and overriding the run() method, you define the code that executes when the thread starts. Calling start() invokes the JVM's thread scheduling mechanism.
Implementation Example
class WorkerThread extends Thread {
private final String taskId;
public WorkerThread(String taskId) {
this.taskId = taskId;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + " processing " + taskId + " iteration: " + i);
try {
sleep(100); // Pause execution
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public class ThreadBasicDemo {
public static void main(String[] args) {
WorkerThread t1 = new WorkerThread("A");
WorkerThread t2 = new WorkerThread("B");
t1.setName("Worker-1");
t2.setName("Worker-2");
t1.start();
t2.start();
// Never call run() directly - it executes in the main thread
}
}
Sample Output
Worker-1 processing A iteration: 0
Worker-2 processing B iteration: 0
Worker-1 processing A iteration: 1
Worker-2 processing B iteration: 1
...
Advantages and Disadvantages
- Advantages: Straightforward API, direct control over thread lifecycle
- Disadvantages: Single inheritance constraint, tight coupling between task and thread execution
Approach 2: Implementing Runnable Interface (Recommended)
How It Works
Runnable separates the task definition from the execution mechanism. The task logic resides in Runnable.run(), while Thread handles the actual execution. This design enables task reuse and avoids inheritance limitations.
Implementation Example
class ProcessingTask implements Runnable {
private final int iterations;
public ProcessingTask(int iterations) {
this.iterations = iterations;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < iterations; i++) {
System.out.println(threadName + " - count: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableBasicDemo {
public static void main(String[] args) {
ProcessingTask task = new ProcessingTask(5);
Thread worker1 = new Thread(task, "Processor-1");
Thread worker2 = new Thread(task, "Processor-2");
worker1.start();
worker2.start();
}
}
Lambda Expression Shortcut (JDK 8+)
public class LambdaRunnableDemo {
public static void main(String[] args) {
Runnable job = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " working: " + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
};
new Thread(job, "Lambda-Worker-1").start();
new Thread(job, "Lambda-Worker-2").start();
}
}
Advantages and Disadvantages
- Advantages: No inheritance constraint, loose coupling between task and execution context
- Disadvantages: run() cannot return values, checked exceptions cannot be thrown
Approach 3: Callable with FutureTask (Return Values)
How It Works
Callable provides enhanced capabilities over Runnable: it supports generic return types and can throw checked exceptions. FutureTask acts as a bridge, implementing both Runnable and Future, enabling result retrieval and cancellation.
Implementation Example
import java.util.concurrent.*;
class SumCalculator implements Callable<Integer> {
private final int limit;
public SumCalculator(int limit) {
this.limit = limit;
}
@Override
public Integer call() throws InterruptedException {
int total = 0;
String threadName = Thread.currentThread().getName();
for (int i = 1; i <= limit; i++) {
total += i;
System.out.println(threadName + " computing: " + i);
Thread.sleep(100);
}
return total;
}
}
public class CallableWithFutureDemo {
public static void main(String[] args) {
SumCalculator calculator = new SumCalculator(5);
FutureTask<Integer> futureTask = new FutureTask<>(calculator);
Thread computationThread = new Thread(futureTask, "Calculator");
computationThread.start();
try {
// Blocks until result is available
Integer result = futureTask.get();
System.out.println("Sum result: " + result); // Outputs 15
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
Key Methods
- get(): Blocks until computation completes
- cancel(boolean mayInterrupt): Attempts to cancel execution
- isDone(): Checks if computation finished
Advantages and Disadvantages
- Advantages: Return values, checked exception support, non-blocking result checking
- Disadvantages: get() blocks execution, requires timeout handling
Approach 4: Thread Pools (Executor Framework)
How It Works
Thread pools eliminate the overhead of creating and destroying threads by maintaining a pool of reusable worker threads. Tasks are submitted to a queue and executed by available threads. This is the preferred approach for production systems.
Pre-built Thread Pool Types
| Pool Type | Characteristics | Best Use Case |
|---|---|---|
| FixedThreadPool | Constant core size, unbounded max | Steady concurrent workload |
| CachedThreadPool | Zero core threads, unlimited max | Short-lived, high-frequency tasks |
| SingleThreadExecutor | Single worker thread | Sequential task execution |
| ScheduledThreadPool | Supports scheduling | Delayed and periodic tasks |
Fixed Thread Pool Example
import java.util.concurrent.*;
public class ExecutorServiceDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
// Submit Runnable tasks
for (int i = 0; i < 5; i++) {
final int taskId = i;
pool.submit(() -> {
System.out.println(Thread.currentThread().getName() +
" handling task " + taskId);
try { Thread.sleep(200); } catch (InterruptedException e) {}
});
}
// Submit Callable tasks with return values
pool.submit(() -> {
int sum = 0;
for (int i = 1; i <= 3; i++) sum += i;
System.out.println("Computed sum: " + sum);
return sum;
});
pool.shutdown(); // Allow submitted tasks to complete
}
}
Custom Thread Pool Configuration
import java.util.concurrent.*;
public class CustomPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // core pool size
4, // maximum pool size
60, // keep-alive time
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 7; i++) {
final int jobId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() +
" processing job " + jobId);
try { Thread.sleep(100); } catch (InterruptedException e) {}
});
}
executor.shutdown();
}
}
Advantages and Disadvantages
- Advantages: Thread reuse, controlled concurrency, task queue management, resource tuning
- Disadvantages: Requires parameter tuning, potential for resource exhaustion if misconfigured
Approach 5: Timer and TimerTask (Scheduling)
How It Works
Timer provides legacy scheduling capabilities for deferred and periodic task execution. It runs on a single daemon thread, executing TimerTask (which implements Runnable) at specified intervals.
Implementation Example
import java.util.Timer;
import java.util.TimerTask;
public class TimerSchedulingDemo {
public static void main(String[] args) {
Timer scheduler = new Timer("Scheduled-Task-Thread");
// Execute after 1 second delay, then every 2 seconds
scheduler.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +
" scheduled execution: " + System.currentTimeMillis());
}
}, 1000, 2000);
// Terminate after 5 seconds
try {
Thread.sleep(5000);
scheduler.cancel();
System.out.println("Scheduler terminated");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Important Considerations
Timer executes on a single thread, meaning an uncaught exception terminates all scheduled tasks. Production applications should use ScheduledThreadPoolExecutor instead.
Comparison and Selection Guidelines
| Approach | Return Value | Expection Handling | Inheritance | Performance | Recommended Use |
|---|---|---|---|---|---|
| Thread subclass | None | Catch only | Yes (limitation) | Moderate | Learning, simple demos |
| Runnable | None | Catch only | No | Good | Standard tasks without return values |
| Callable+FutureTask | Yes (generic) | Can throw | No | Good | Tasks requiring results |
| Thread Pool | Optional | Configurable | No | Optimal | Production systems |
| Timer/TimerTask | None | Catch only | No | Poor | Simple scheduling |
Selection Principles
- Production systems: Always prefer thread pools (custom configuration offers best control)
- Results needed: Combine Callable with thread pool submission
- Learning/simple tests: Runnable or Thread for quick prototyping
- Scheduling requirements: Use ScheduledThreadPoolExecutor, not Timer
Key Takeaways
Java multithreading fundamentallly relies on extending Thread or implementing Runnable. Callable+FutureTask extends these with return value support, while thread pools address performance and resource management challenges.
Production environments should exclusively use thread pools with carefully tuned parameters. The critical distinctions between approaches center on return value handling, exception propagation, and thread lifecycle management—selection should align with specific application requirements.