一、先搞懂核心基础
Java 中,线程的本质是Thread类的实例。所有创建线程的方式,最终都是通过Thread类来启动线程,区别在于任务逻辑的定义方式不同。
二、方式 1:继承 Thread 类
1. 核心原理
Thread类本身实现了Runnable接口,代表一个线程实例。
- 继承
Thread类,重写run()方法,把线程要执行的任务写在run()里 - 调用
start()方法启动线程(注意:不能直接调用 run (),否则只是普通方法调用,不会启动新线程)
2. 代码实现
java
// 1. 自定义类继承Thread
class MyThread extends Thread {
// 2. 重写run()方法,定义线程任务
@Override
public void run() {
// 线程执行的业务逻辑
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 执行:" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 3. 创建线程对象
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 4. 调用start()启动线程(JVM会自动调用run())
thread1.setName("线程A");
thread2.setName("线程B");
thread1.start();
thread2.start();
}
}
3. 关键细节
start()的作用:通知 JVM 将该线程放入就绪队列,等待 CPU 调度 ,调度后自动执行run()run()的本质:普通方法,直接调用只会在当前线程(主线程)执行,不会开启新线程Thread.currentThread().getName():获取当前执行代码的线程名称
4. 优缺点 & 适用场景
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 代码简单,直接继承即可 | Java 是单继承,继承 Thread 后无法再继承其他类,扩展性差 | 简单的、无继承需求的线程任务 |
三、方式 2:实现 Runnable 接口
1. 核心原理
Runnable是一个函数式接口,只有一个run()抽象方法,仅代表 "可执行的任务",不代表线程本身。
- 实现
Runnable接口,重写run()方法定义任务 - 创建
Runnable实现类对象,作为参数传入Thread类的构造器,创建线程实例 - 调用
start()启动线程
2. 代码实现
java
// 1. 实现Runnable接口
class MyRunnable implements Runnable {
// 2. 重写run()方法,定义任务
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 执行:" + i);
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
// 3. 创建任务对象
MyRunnable task = new MyRunnable();
// 4. 将任务传入Thread构造器,创建线程
Thread thread1 = new Thread(task, "线程A");
Thread thread2 = new Thread(task, "线程B");
// 5. 启动线程
thread1.start();
thread2.start();
// Lambda表达式简化(函数式接口特性)
Thread lambdaThread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " Lambda线程执行");
}, "Lambda线程");
lambdaThread.start();
}
}
3. 关键细节
- 任务与线程分离 :同一个
Runnable任务可以被多个线程共享,适合多线程处理同一资源的场景(如售票系统) - 避免了单继承限制,实现类可以继承其他类,扩展性更强
- 是 Java 中最常用、最推荐的基础创建方式
4. 优缺点 & 适用场景
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 避免单继承限制,任务与线程解耦,可复用任务 | 无返回值,无法抛出 checked 异常 | 绝大多数无返回值的线程任务,是日常开发的首选 |
四、方式 3:实现 Callable 接口
1. 核心原理
Callable是 JDK 1.5 引入的接口,和Runnable的核心区别:
Callable的call()方法有返回值,可以抛出 checked 异常Runnable的run()方法无返回值,不能抛出 checked 异常Callable需要配合FutureTask(实现了RunnableFuture接口,间接实现Runnable)使用,才能作为Thread的任务
2. 代码实现
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 1. 实现Callable接口,泛型指定返回值类型
class MyCallable implements Callable<Integer> {
// 2. 重写call()方法,定义任务,有返回值
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
System.out.println(Thread.currentThread().getName() + " 累加:" + i);
}
return sum; // 返回计算结果
}
}
public class CallableDemo {
public static void main(String[] args) throws Exception {
// 3. 创建Callable任务
MyCallable callableTask = new MyCallable();
// 4. 包装成FutureTask(FutureTask实现了Runnable,可传入Thread)
FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
// 5. 创建线程,启动
Thread thread = new Thread(futureTask, "累加线程");
thread.start();
// 6. 获取线程执行结果(get()会阻塞当前线程,直到任务执行完成)
Integer result = futureTask.get();
System.out.println("线程执行结果:" + result);
}
}
3. 关键细节
FutureTask的作用:桥梁 ,将Callable适配为Runnable,同时提供了获取结果、取消任务、判断状态的方法get()方法:阻塞方法,会等待线程执行完成后返回结果,若任务未完成,主线程会一直阻塞cancel(boolean mayInterruptIfRunning):取消任务,true表示中断正在执行的任务isDone():判断任务是否执行完成
4. 优缺点 & 适用场景
表格
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 有返回值,可抛出异常,适合需要结果的异步任务 | 代码相对复杂,需要配合 FutureTask | 异步计算、需要获取线程执行结果的场景(如大数据计算、接口 |
额外拓展 --- 为什么 Callable 比 Runnable 多了FutureTask包装
一、接口兼容性("翻译" 作用)
我们看一下 Java 的类继承结构:
Thread类 :只能接收Runnable类型的任务。- 构造函数:
Thread(Runnable target)
- 构造函数:
Callable接口 :是一个独立的接口,并没有继承Runnable。- 它的方法是
V call() throws Exception,有返回值和异常。
- 它的方法是
矛盾点 :你不能直接把一个 Callable 对象丢给 Thread,因为 Thread 构造器只认 Runnable。
解决方案 :FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 同时继承了 Runnable 和 Future。
- 它实现了
Runnable:可以传给 Thread,骗过了 Thread 的检查。 - 它持有了
Callable对象:内部保存了你的具体逻辑。
二、 内部原理:它是怎么工作的?
FutureTask 的内部逻辑非常巧妙,它完成了两件事:
1. 承接任务(适配)
java
public class FutureTask<V> implements RunnableFuture<V> {
private Callable<V> callable; // 内部持有Callable任务
// 构造器
public FutureTask(Callable<V> callable) {
this.callable = callable; // 把传进来的Callable存起来
}
}
2. 执行逻辑(桥接)
当 Thread 启动后,会调用 FutureTask 的 run() 方法。FutureTask 的 run() 方法内部会主动调用 你传入的 Callable.call()。
java
// FutureTask.run() 的伪代码逻辑
public void run() {
// 调用实际的Callable任务
V result = callable.call();
// 把结果存起来
set(result);
}
三、 它的核心价值:不仅是 "桥梁",更是 "管家"
如果只是为了把 Callable 变成 Runnable,那只需要一个简单的包装类就行。但 FutureTask 之所以叫 Future(未来),是因为它还提供了异步获取结果的能力。
它额外提供了三大核心功能(这也是为什么不能直接用 Thread 执行 Callable 的原因):
1. 结果存储 (get)
Thread 执行完 run() 方法就结束了,它没有地方存返回值。FutureTask 内部有一个变量 private Object outcome;,专门用来存储 call() 的返回结果。
2. 阻塞等待 (get ())
这是最关键的功能。
- 如果你直接
new Thread(new MyCallable()).start();,线程执行完就结束了,你永远拿不到计算结果。 - 有了
FutureTask,你可以调用futureTask.get()。- 原理 :如果任务还没执行完,
get()方法会阻塞主线程,直到任务执行完毕并把结果拿回来。 - 作用:保证了 "同步等待结果" 的逻辑。
- 原理 :如果任务还没执行完,
3. 状态管理
FutureTask 内部维护了任务的状态(NEW -> COMPLETING -> NORMAL 等)。
- 它能知道任务是正常结束 、抛出异常 还是被取消了。
四、 总结
为什么 Callable 比 Runnable 多了这个包装?
- 为了合规 :
Thread只认Runnable,必须通过FutureTask中转。 - 为了功能 :单纯的
Runnable运行完就消失了,什么都留不下。 - 为了异步 :
FutureTask赋予了任务 "存结果、等结果、查状态" 的能力,实现了真正的异步计算。
五、方式 4:线程池(生产环境核心方案)
前面 3 种方式都是手动创建销毁线程 ,线程的创建和销毁会消耗大量系统资源,高并发场景下性能极差。线程池的核心思想:提前创建好一批线程,放入池中复用,任务提交后直接分配线程执行,执行完线程不销毁,等待下一个任务,避免频繁创建销毁,提升性能。
分为两类:Executors工厂类、ThreadPoolExecutor自定义线程池
4.1 Executors 工厂类(快速创建线程池,不推荐生产环境使用)
Executors是 JDK 提供的线程池工具类,封装了 4 种常用线程池,一行代码即可创建:
1. 常用 4 种线程池
| 方法名 | 核心特点 | 适用场景 |
|---|---|---|
Executors.newFixedThreadPool(int nThreads) |
固定大小的线程池,核心线程数 = 最大线程数,无界队列 | 负载稳定的固定并发任务 |
Executors.newCachedThreadPool() |
可缓存线程池,核心线程数为 0,最大线程数为 Integer.MAX_VALUE,空闲线程 60 秒后销毁 | 短期大量异步任务、负载波动大的场景 |
Executors.newSingleThreadExecutor() |
单线程线程池,保证任务按顺序执行 | 需要串行执行任务的场景 |
Executors.newScheduledThreadPool(int corePoolSize) |
定时任务线程池,支持定时、周期性任务 | 定时任务、周期任务 |
2. 代码示例(newFixedThreadPool)
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsDemo {
public static void main(String[] args) {
// 1. 创建固定大小为3的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 2. 提交任务(可以是Runnable或Callable)
for (int i = 0; i < 10; i++) {
int taskNum = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskNum);
});
}
// 3. 关闭线程池(不再接受新任务,等待现有任务执行完)
threadPool.shutdown();
}
}
3. 为什么不推荐生产环境使用?
Executors创建的线程池默认使用无界队列LinkedBlockingQueue,当任务量暴增时,队列会无限堆积,最终导致 OOM(内存溢出),违反阿里 Java 开发手册规范。
4.2 ThreadPoolExecutor 自定义线程池(生产环境标准方案)
ThreadPoolExecutor是线程池的底层实现类 ,所有Executors的线程池都是基于它封装的。自定义线程池可以精准控制线程池的所有参数,避免 OOM 风险。
1. 核心构造参数(7 大参数)
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数:线程池长期保留的线程数,即使空闲也不会销毁
int maximumPoolSize, // 最大线程数:线程池能创建的最大线程数
long keepAliveTime, // 空闲线程存活时间:非核心线程空闲超过该时间会被销毁
TimeUnit unit, // 时间单位:keepAliveTime的单位(秒、毫秒等)
BlockingQueue<Runnable> workQueue, // 任务队列:存放等待执行的任务的阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的工厂,可自定义线程名、优先级等
RejectedExecutionHandler handler // 拒绝策略:当队列满、线程数达到最大值时,对新任务的处理策略
) {}
2. 关键参数详解
- 核心线程数 corePoolSize:根据业务压测结果设置,一般设置为 CPU 核心数 * 2(IO 密集型)或 CPU 核心数(CPU 密集型)
- 任务队列 workQueue :
ArrayBlockingQueue:有界队列,必须指定大小,生产环境推荐,避免 OOMLinkedBlockingQueue:无界队列,Executors 默认使用,不推荐SynchronousQueue:不存储任务,直接提交给线程,适合高并发、任务处理快的场景
- 拒绝策略 handler (4 种内置策略):
AbortPolicy(默认):直接抛出RejectedExecutionException,阻止系统运行CallerRunsPolicy:由提交任务的线程(主线程)执行该任务,不会丢弃DiscardPolicy:直接丢弃新任务,不抛异常DiscardOldestPolicy:丢弃队列中最老的任务,执行新任务生产环境推荐自定义拒绝策略,记录日志、告警、降级处理
3. 代码实现(生产环境标准自定义线程池)
java
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class CustomThreadPoolDemo {
public static void main(String[] args) {
// 1. 自定义线程工厂(给线程起有意义的名字,方便排查问题)
ThreadFactory namedThreadFactory = new ThreadFactory() {
private final AtomicInteger threadNum = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "custom-pool-thread-" + threadNum.getAndIncrement());
thread.setPriority(Thread.NORM_PRIORITY); // 设置线程优先级
return thread;
}
};
// 2. 自定义拒绝策略(记录日志+告警)
RejectedExecutionHandler customHandler = (r, executor) -> {
System.err.println("线程池已满,拒绝任务:" + r.toString() + ",当前队列大小:" + executor.getQueue().size());
// 实际生产中可接入告警系统、消息队列降级
};
// 3. 创建自定义线程池(核心参数根据业务调整)
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5, // 核心线程数5
10, // 最大线程数10
60L, // 空闲线程存活60秒
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列,大小100,避免OOM
namedThreadFactory, // 自定义线程工厂
customHandler // 自定义拒绝策略
);
// 4. 提交任务
for (int i = 0; i < 20; i++) {
int taskNum = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskNum);
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 5. 关闭线程池
threadPool.shutdown();
}
}
4. 线程池工作流程(核心原理)
- 提交任务时,若核心线程数未满,创建核心线程执行任务
- 若核心线程数已满,任务进入阻塞队列等待
- 若队列已满,创建非核心线程执行任务(直到达到最大线程数)
- 若线程数达到最大值,触发拒绝策略处理新任务
- 非核心线程空闲超过
keepAliveTime后,被销毁,最终线程池保留核心线程数
5. 优缺点 & 适用场景
表格
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 线程复用,避免频繁创建销毁,提升性能;可精准控制并发数,防止 OOM | 代码相对复杂,需要合理设置参数 | 生产环境所有高并发场景,是 Java 多线程的标准解决方案 |
避坑指南
1. start () 和 run () 的区别?
start():启动线程,JVM 将线程放入就绪队列,等待 CPU 调度,真正开启新线程run():普通方法,直接调用只会在当前线程执行,不会开启新线程
2. Runnable 和 Callable 的区别?
Runnable:run()无返回值,不能抛 checked 异常Callable:call()有返回值,可抛 checked 异常,需要配合FutureTask使用
3. 线程池的 7 大参数是什么?工作流程?
见上文 4.2 部分
4. 为什么不推荐使用 Executors 创建线程池?
newFixedThreadPool/newSingleThreadExecutor:使用无界队列LinkedBlockingQueue,任务堆积导致 OOMnewCachedThreadPool/newScheduledThreadPool:最大线程数为Integer.MAX_VALUE,可创建无限多线程,导致 OOM
5. 线程池的拒绝策略有哪些?
见上文 4.2 部分,4 种内置策略 + 自定义策略
八、补充:Lambda 表达式简化创建
Java 8 之后,Runnable和Callable都是函数式接口,可以用 Lambda 表达式大幅简化代码:
java
// Runnable简化
new Thread(() -> System.out.println("Lambda线程执行")).start();
// Callable简化
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
return 100; // 直接写call()逻辑
});
new Thread(futureTask).start();