Java Multithreading: Complete Implementation Guide with Examples

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

  1. Production systems: Always prefer thread pools (custom configuration offers best control)
  2. Results needed: Combine Callable with thread pool submission
  3. Learning/simple tests: Runnable or Thread for quick prototyping
  4. 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.

Tags: java multithreading Concurrency Thread Pool Executor Framework

Posted on Thu, 18 Jun 2026 18:01:36 +0000 by cirko