在开始探讨多线程实现方式之前,我们需要先明确两个最基础的概念:线程和进程。很多初学者容易混淆这两者,但实际上它们的区别和联系非常清晰。
进程是操作系统进行资源分配和调度的基本单位。当你启动一个Java程序时,操作系统就会为它创建一个进程。这个进程拥有独立的内存空间、文件句柄和其他系统资源。可以把进程想象成一个独立的工厂,拥有自己的厂房、原料仓库和生产线。
线程则是进程内部的执行单元,是CPU调度的最小单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。继续用工厂的比喻,线程就像是工厂里的工人,多个工人可以在同一个厂房内协同工作。
关键区别:进程间相互隔离,线程间共享内存。这也是多线程编程需要特别注意线程安全问题的根本原因。
在实际开发中,我们通常会遇到以下几种线程使用场景:
很多开发者容易混淆并发(Concurrency)和并行(Parallelism)这两个概念,但它们实际上描述了不同的执行模式。
并发指的是在单核CPU上,通过快速切换执行不同线程,给人造成"同时"执行的假象。就像杂技演员轮流抛接多个球,实际上每个时刻他只能处理一个球,但通过快速切换给人同时操作的印象。
并行则是真正的物理同时执行,需要多核CPU的支持。每个核心可以独立执行一个线程,就像有多位杂技演员同时抛接各自的球。
现代Java程序通常会同时利用这两种机制:
java复制Runtime.getRuntime().availableProcessors() // 获取可用CPU核心数
这个简单的代码可以帮助你了解程序可用的并行资源。在我的开发经验中,合理设置线程数需要考虑这个值 - 通常CPU密集型任务线程数不超过核心数,IO密集型可以适当增加。
这是Java中最直观的多线程实现方式,适合简单的多线程场景。具体实现分为三个步骤:
这里有一个完整的示例:
java复制public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的具体逻辑
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " executing task " + i);
}
}
}
// 使用方式
MyThread thread1 = new MyThread();
thread1.setName("Worker-1");
thread1.start();
在实际操作中,初学者常会遇到各种环境配置问题。根据我的经验,最常见的有两类:
问题一:JDK未正确配置
症状:报错"没有为模块指定JDK"
解决方案:
问题二:输出路径未指定
症状:报错"没有为模块指定输出路径"
解决方案有两种:
项目级统一输出路径(适合简单项目)
模块独立输出路径(推荐多模块项目)
虽然继承Thread类简单直接,但在实际项目中存在明显缺陷:
在我的项目经验中,只有在非常简单的工具类或测试代码中才会使用这种方式。生产环境更推荐下面介绍的实现Runnable接口的方式。
这是Java中最推荐的多线程实现方式,也是实际项目中最常用的。实现步骤为:
示例代码:
java复制public class Task implements Runnable {
@Override
public void run() {
// 使用Thread.currentThread()获取当前线程
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(threadName + " processing item " + i);
}
}
}
// 使用方式
Task task = new Task();
Thread worker1 = new Thread(task, "Worker-1");
Thread worker2 = new Thread(task, "Worker-2");
worker1.start();
worker2.start();
实现Runnable接口相比继承Thread有几个显著优势:
在实际项目中,我遇到过一个典型场景:需要处理大量相似任务,但创建数千个线程开销太大。使用Runnable方式,可以创建少量线程重复执行同一个任务逻辑,大大降低了资源消耗。
当多个线程共享同一个Runnable实例时,需要特别注意线程安全问题:
java复制public class Counter implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++; // 非原子操作,存在竞态条件
}
}
public int getCount() { return count; }
}
// 测试代码
Counter counter = new Counter();
Thread t1 = new Thread(counter);
Thread t2 = new Thread(counter);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(counter.getCount()); // 结果可能小于2000
这个例子展示了典型的线程安全问题。解决方案包括:
前两种方式的run()方法都没有返回值,而实际开发中我们经常需要获取线程执行的结果。Java 5.0引入的Callable接口解决了这个问题。
基本使用模式:
示例代码:
java复制public class CalculationTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
Thread.sleep(10); // 模拟耗时操作
}
return sum;
}
}
// 使用方式
Callable<Integer> task = new CalculationTask();
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread worker = new Thread(futureTask);
worker.start();
// 获取结果(会阻塞直到计算完成)
try {
Integer result = futureTask.get();
System.out.println("Sum: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
Future接口提供了丰富的方法来控制异步任务:
在实际项目中,Future通常与线程池配合使用:
java复制ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(new CalculationTask());
// 可以继续执行其他任务
doSomethingElse();
// 需要结果时再获取
Integer result = future.get();
Callable的call()方法可以抛出异常,这与Runnable的run()不同。处理这些异常有几种常见模式:
在我的项目经验中,推荐在call()方法内部处理业务异常,只让系统异常抛出。这样可以使调用方的异常处理逻辑更清晰。
| 特性 | 继承Thread | 实现Runnable | 实现Callable |
|---|---|---|---|
| 是否有返回值 | 无 | 无 | 有 |
| 是否可抛出异常 | 不可 | 不可 | 可 |
| 是否支持继承 | 占用继承权 | 不影响 | 不影响 |
| 资源共享能力 | 差 | 好 | 好 |
| 线程池兼容性 | 差 | 好 | 好 |
| 代码复杂度 | 简单 | 中等 | 较高 |
根据我多年的开发经验,给出以下建议:
特别提醒:在现代Java开发中,直接操作Thread的情况已经越来越少,更多是使用各种高级并发工具(如线程池、ForkJoinPool等)。理解这些基础实现方式,是为了更好地掌握高级并发API。
调用run()而非start():
多次启动同一个线程:
java复制Thread t = new Thread(task);
t.start();
t.start(); // 抛出IllegalThreadStateException
线程一旦启动就不能重复启动,需要创建新实例
不处理InterruptedException:
当线程被中断时,应该合理清理资源并退出
java复制Thread t = new Thread(task, "Database-Connector");
java复制public class VisibilityIssue {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
// 空循环
}
System.out.println("Thread stopped");
}).start();
Thread.sleep(1000);
flag = false;
System.out.println("Main thread set flag to false");
}
}
这段代码在某些JVM实现下可能会出现无限循环,因为工作线程可能看不到主线程对flag的修改。解决方案:
java复制public class AtomicityIssue {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++;
}
});
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("Final count: " + counter); // 可能小于20000
}
}
解决方案:
死锁的四个必要条件:
预防策略:
Java 5.0引入的Executor框架是对原始线程API的重大改进:
java复制ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> {
// 任务逻辑
});
executor.shutdown();
优势:
Java 7引入的ForkJoinPool适合计算密集型任务:
java复制public class SumTask extends RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
@Override
protected Long compute() {
if (end - start < 1000) { // 阈值
return sequentialSum();
}
int mid = (start + end) / 2;
SumTask left = new SumTask(numbers, start, mid);
SumTask right = new SumTask(numbers, mid, end);
left.fork(); // 异步执行
return right.compute() + left.join(); // 等待结果
}
}
Java 8的CompletableFuture提供了更强大的异步编程能力:
java复制CompletableFuture.supplyAsync(() -> fetchDataFromDB())
.thenApply(data -> processData(data))
.thenAccept(result -> saveResult(result))
.exceptionally(ex -> {
log.error("Error occurred", ex);
return null;
});
这种链式调用可以构建复杂的异步工作流,是响应式编程的基础。
获取线程转储:
bash复制jstack <pid> > thread_dump.txt
分析要点:
Java自带的JVisualVM工具可以:
在我的项目经验中,多线程相关的Bug往往在测试阶段难以复现。建立完善的日志系统和监控机制是预防线上问题的关键。