线程创建与任务模型
Java 并发开发中要区分“线程”和“任务”。线程是执行资源,任务是要执行的逻辑。好的并发设计应把任务定义和线程管理分开。
# 1. 继承 Thread
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("run");
}
}
new MyThread().start();
这种方式简单,但把任务逻辑和线程对象耦合在一起,不利于复用,也不适合线程池。
# 2. 实现 Runnable
Runnable task = () -> System.out.println("run");
Thread thread = new Thread(task);
thread.start();
Runnable 表示无返回值、不能直接抛受检异常的任务。
@FunctionalInterface
public interface Runnable {
void run();
}
优点:
- 任务和线程分离。
- 可以用 Lambda。
- 可以提交给线程池。
# 3. Callable
Callable 表示有返回值、可以抛异常的任务。
Callable<Integer> task = () -> {
return calculate();
};
接口:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
通常与 Future、ExecutorService 配合:
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(task);
Integer result = future.get();
# 4. Future
Future 表示异步任务的结果占位。
submit task
│
▼
Future
│
├─ get 等待结果
├─ cancel 取消任务
├─ isDone 是否完成
└─ isCancelled 是否取消
示例:
Future<String> future = executor.submit(() -> query());
String result = future.get(1, TimeUnit.SECONDS);
get() 会阻塞当前线程,应设置超时,避免无限等待。
# 5. FutureTask
FutureTask 同时实现了 Runnable 和 Future。
FutureTask<Integer> task = new FutureTask<>(() -> 1 + 1);
new Thread(task).start();
Integer result = task.get();
它可以把 Callable 包装成可执行任务,也可以获取结果。
# 6. Executor
Executor 抽象了任务提交。
Executor executor = command -> new Thread(command).start();
executor.execute(() -> System.out.println("run"));
实际开发中更常用 ExecutorService:
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(task);
executor.shutdown();
任务模型:
Runnable / Callable
│
▼
ExecutorService
│
▼
线程池中的工作线程
│
▼
执行任务
# 7. Runnable 与 Callable 对比
| 对比项 | Runnable | Callable |
|---|---|---|
| 返回值 | 无 | 有 |
| 异常 | 不能直接抛受检异常 | 可以抛异常 |
| 方法 | run() | call() |
| 常见提交方式 | execute、submit | submit |
如果任务只是执行动作,用 Runnable;如果需要结果或异常传递,用 Callable。
# 8. execute 与 submit
| 方法 | 返回值 | 异常表现 |
|---|---|---|
execute(Runnable) | 无 | 未捕获异常交给线程异常处理器 |
submit(...) | Future | 异常封装到 Future,调用 get 时抛出 |
示例:
Future<?> future = executor.submit(() -> {
throw new RuntimeException("error");
});
future.get(); // ExecutionException
如果 submit 后从不 get,异常可能不容易被发现。
# 9. 任务取消
future.cancel(true);
参数 true 表示尝试中断正在执行任务的线程。
任务必须配合处理中断:
while (!Thread.currentThread().isInterrupted()) {
doWork();
}
取消不是强杀,任务内部如果不响应中断,可能仍会继续执行。
# 10. 任务设计建议
- 任务对象尽量无状态,或只持有不可变状态。
- 不要在线程池任务中吞异常。
- 长时间任务要支持取消和超时。
- 不要在任务里无限创建新线程。
- 线程池由外部管理,不要在每个任务里创建和关闭线程池。
- IO 任务要设置超时,避免工作线程永久卡住。
# 专家实践与边界
线程创建方式本身不是重点,重点是任务生命周期如何治理。生产系统通常不应该让业务代码随意 new Thread,而是把任务提交给有边界、有命名、有监控、有拒绝策略的执行器。
业务请求
│
▼
任务对象 Runnable / Callable
│
▼
ExecutorService
│
├─ 线程复用
├─ 队列缓冲
├─ 异常治理
└─ 生命周期关闭
| 写法 | 适合场景 | 风险 |
|---|---|---|
new Thread | 教学、极少量临时任务 | 无法统一治理 |
Runnable | 无返回异步动作 | 异常容易丢失 |
Callable | 有返回值任务 | 必须消费 Future |
ExecutorService | 生产任务执行 | 参数必须有边界 |
CompletableFuture | 异步编排 | 线程池和超时要显式 |
设计任务时要明确:是否可取消、是否可重试、是否幂等、是否有超时、失败后由谁记录和补偿。
# Tips 快问快答
Q:继承 Thread 和实现 Runnable 哪个更推荐?
A:实现 Runnable 更推荐,它让任务和线程解耦。
Q:Runnable 能返回结果吗?
A:不能,需要结果时用 Callable。
Q:Callable 的异常怎么拿到?
A:通过 Future.get(),异常会被包装成 ExecutionException。
Q:Future.get 有什么风险? A:会阻塞当前线程,生产代码应考虑超时。
Q:execute 和 submit 有什么区别?
A:execute 无返回值,submit 返回 Future,异常表现也不同。
Q:submit 后不 get 会怎样? A:任务异常可能被封装在 Future 中而不被观察到。
Q:cancel(true) 能强制杀死线程吗? A:不能,只是尝试中断,任务需要配合响应。
Q:任务和线程为什么要分离? A:任务表达业务逻辑,线程是执行资源,分离后才能复用线程池。