1. 线程创建方式概述
在Java并发编程中,线程是最基础的执行单元。创建线程主要有三种经典方式:继承Thread类、实现Runnable接口和实现Callable接口。这三种方式各有特点,适用于不同的业务场景。
我见过不少开发者习惯性地直接继承Thread类,这其实是个值得商榷的做法。在实际项目中,更推荐使用Runnable和Callable这两种方式,原因我们后面会详细分析。先来看一个简单的对比表格:
| 创建方式 | 返回值 | 异常处理 | 线程复用 | 适用场景 |
|---|---|---|---|---|
| 继承Thread | 无 | 受限 | 不支持 | 简单任务 |
| 实现Runnable | 无 | 受限 | 支持 | 大多数常规异步任务 |
| 实现Callable | 有 | 完善 | 支持 | 需要返回结果的异步任务 |
提示:在Java 8之后,还可以通过Lambda表达式简化Runnable和Callable的实现,这是现代Java开发中更推荐的写法。
2. 继承Thread类方式
2.1 基本实现方式
继承Thread类是最原始的线程创建方式。我们通过重写run()方法来定义线程要执行的任务:
class MyThread extends Thread { @Override public void run() { System.out.println("线程执行: " + Thread.currentThread().getName()); } } // 使用方式 MyThread thread = new MyThread(); thread.start();这种方式看似简单直接,但在实际项目中却存在几个明显问题:
- Java是单继承语言,继承了Thread后无法再继承其他类
- 线程与任务耦合在一起,不利于代码复用
- 无法获取执行结果
- 异常处理受限
2.2 适用场景与注意事项
虽然这种方式不太推荐,但在某些简单场景下仍有其用武之地:
- 快速原型开发
- 简单的测试用例
- 不需要返回结果的独立任务
注意:直接调用run()方法并不会启动新线程,它只是在当前线程中同步执行。必须调用start()方法才能创建新线程。
3. 实现Runnable接口方式
3.1 基本实现方式
Runnable是更推荐的线程创建方式,它是一个函数式接口,只有一个run()方法:
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable执行: " + Thread.currentThread().getName()); } } // 使用方式 Thread thread = new Thread(new MyRunnable()); thread.start();Java 8之后,我们可以用Lambda表达式进一步简化:
new Thread(() -> { System.out.println("Lambda Runnable执行"); }).start();3.2 优势分析
Runnable方式相比继承Thread有几个显著优势:
- 避免单继承限制,可以继承其他类
- 任务与线程分离,提高代码复用性
- 更适合线程池的使用
- 降低资源消耗(线程可复用)
在实际项目中,我建议将Runnable实现类设计为无状态的,这样更利于线程安全。如果需要有状态,务必做好同步控制。
3.3 线程池中的应用
Runnable与线程池配合使用是生产环境中的常见做法:
ExecutorService executor = Executors.newFixedThreadPool(5); executor.execute(() -> { // 任务逻辑 });这种方式可以避免频繁创建销毁线程的开销,是更高效的选择。
4. 实现Callable接口方式
4.1 基本实现方式
Callable是Java 5引入的接口,与Runnable类似但可以返回结果和抛出异常:
class MyCallable implements Callable<String> { @Override public String call() throws Exception { Thread.sleep(1000); return "任务完成: " + Thread.currentThread().getName(); } } // 使用方式 ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(new MyCallable()); String result = future.get(); // 阻塞获取结果4.2 Future与FutureTask
Callable通常与Future配合使用。Future接口提供了以下关键方法:
- get():获取结果,阻塞直到计算完成
- get(long timeout, TimeUnit unit):带超时的获取
- cancel(boolean mayInterruptIfRunning):尝试取消任务
- isDone():检查任务是否完成
FutureTask是Future的一个实现类,可以直接作为Runnable使用:
FutureTask<String> futureTask = new FutureTask<>(new MyCallable()); new Thread(futureTask).start(); String result = futureTask.get();4.3 异常处理优势
Callable相比Runnable的一个重大改进是完善的异常处理机制。call()方法可以抛出受检异常,这些异常可以通过Future.get()方法捕获:
try { future.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); // 获取实际异常 // 处理异常 }5. 三种方式的对比与选择
5.1 性能对比
从性能角度看,三种方式本身没有本质区别,因为最终都是通过Thread来执行。真正的性能差异来自于使用方式:
- 直接创建Thread对象开销较大(不推荐)
- Runnable+线程池是平衡的选择
- Callable在需要结果时使用
5.2 内存与资源消耗
频繁创建销毁线程代价高昂。实测表明,使用线程池(配合Runnable或Callable)比直接创建Thread对象能减少90%以上的线程创建开销。
5.3 生产环境建议
根据多年项目经验,我总结出以下线程使用原则:
- 永远不要直接继承Thread(除非有特殊需求)
- 简单无返回任务使用Runnable+线程池
- 需要返回结果或异常处理使用Callable
- 线程池大小要根据任务类型合理设置(IO密集型可设大些,CPU密集型不宜过大)
- 使用ThreadFactory给线程命名,便于问题排查
6. 常见问题与解决方案
6.1 线程安全问题
ArrayList、HashMap等集合类是非线程安全的。解决方案:
- 使用Collections.synchronizedList()等包装方法
- 使用并发容器如ConcurrentHashMap
- 通过同步代码块控制访问
6.2 线程池死锁
线程池中任务相互等待会导致死锁。例如:
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future1 = executor.submit(() -> { Future<String> future2 = executor.submit(() -> "内部任务"); return future2.get(); // 死锁! });解决方案是使用不同线程池或增大线程池大小。
6.3 资源泄漏
线程池使用不当会导致资源泄漏。务必记得关闭:
ExecutorService executor = ...; try { // 使用线程池 } finally { executor.shutdown(); }6.4 CPU占用过高排查
Java应用CPU过高常由线程问题引起。排查步骤:
- top -Hp [pid] 找出高CPU线程
- jstack [pid] > thread.txt 获取线程栈
- 将线程ID转为16进制,在thread.txt中查找对应栈
常见原因包括死循环、锁竞争、不当的同步等。
7. 现代Java中的线程使用
7.1 CompletableFuture
Java 8引入的CompletableFuture提供了更强大的异步编程能力:
CompletableFuture.supplyAsync(() -> "Hello") .thenApply(s -> s + " World") .thenAccept(System.out::println);7.2 并行流
对于计算密集型任务,可以考虑使用并行流:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4); int sum = numbers.parallelStream().mapToInt(i -> i).sum();7.3 虚拟线程(Java 19+)
Java 19引入的虚拟线程(协程)可以极大提升并发性能:
Thread.startVirtualThread(() -> { System.out.println("虚拟线程运行中"); });虚拟线程非常适合IO密集型应用,可以创建数百万个而不会导致系统资源耗尽。