网站首页 > java教程 正文
作为 Java 开发,你是不是也遇到过这种情况?明明代码本地跑起来没问题,一上线就出现奇怪的线程安全问题 —— 要么数据计算错乱,要么接口响应突然变慢,排查半天发现,竟然是最开始创建多线程的方式选错了!
我之前帮同事排查过一个订单处理的 bug,明明单测时订单状态更新都正常,线上高峰期却总有几笔订单状态 “卡住”。最后定位到代码里,他用继承 Thread 类的方式创建线程,还在子类里加了共享变量,结果多线程并发时变量被反复覆盖,导致状态同步出了问题。其实如果最开始选实现 Runnable 接口的方式,这类问题完全可以避免。
所以今天咱们就好好聊聊 Java 里创建多线程的那些事儿,从基础到进阶一次讲透,以后不管是做业务开发还是应对面试,都能少踩坑。
为啥多线程创建方式不能随便选?
在 Java 开发里,多线程几乎是绕不开的 —— 从处理异步任务(比如发送短信、生成报表),到应对高并发(比如秒杀接口、订单处理),都得靠多线程提高效率。但很多人刚开始学的时候,觉得 “能创建线程就行”,随便选一种写法就上手,却忽略了不同创建方式背后的设计逻辑和适用场景。
举个例子:如果你的需求是 “简单执行一个无返回值的异步任务”,用继承 Thread 类的方式确实快;但如果需要 “多个线程共享同一个任务逻辑”,比如多线程处理同一份文件的不同片段,那实现 Runnable 接口才是更合理的选择 —— 要是这时候还硬用 Thread 类,不仅要重复写任务逻辑,还容易出现共享资源竞争的问题。
更关键的是,Java 里不同的多线程创建方式,还会影响后续的扩展性:比如继承 Thread 类后,就没法再继承其他类了(Java 单继承限制);而用接口实现的方式,后续还能灵活扩展其他功能。所以选对创建方式,不只是 “能不能跑通” 的问题,更是 “代码好不好维护、能不能应对复杂场景” 的关键。
从基础到进阶:5 种多线程创建方式,附代码示例和适用场景
接下来咱们逐个拆解 Java 里常用的 5 种多线程创建方式,每种都给上代码示例和明确的适用场景,你可以根据自己的需求直接套用。
1. 基础方式 1:继承 Thread 类
这是最直观的创建方式,只要新建一个类继承 Thread,重写 run () 方法,把要执行的任务写在 run () 里,然后调用 start () 方法就能启动线程。
代码示例:
// 1. 继承Thread类
class MyThread extends Thread {
// 2. 重写run()方法,定义线程要执行的任务
@Override
public void run() {
// 这里写具体的任务逻辑,比如打印线程名称和执行内容
System.out.println("当前线程:" + Thread.currentThread().getName() + ",执行任务:处理简单异步逻辑");
}
}
// 3. 调用start()启动线程
public class ThreadDemo {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
thread1.setName("任务线程1");
thread1.start(); // 启动线程,会自动调用run()方法
MyThread thread2 = new MyThread();
thread2.setName("任务线程2");
thread2.start();
}
}
适用场景:适合简单的、无共享逻辑的单任务场景,比如单个异步通知、简单的日志打印。
注意点:因为 Java 是单继承,所以如果你的类已经继承了其他类(比如 Service 类),就不能再用这种方式了;而且多个线程之间无法共享任务逻辑,每个线程都要新建一个子类实例。
2. 基础方式 2:实现 Runnable 接口
这种方式是新建一个类实现 Runnable 接口,同样重写 run () 方法,然后把这个 Runnable 实例传给 Thread 类的构造方法,再调用 start () 启动。
代码示例:
// 1. 实现Runnable接口
class MyRunnable implements Runnable {
// 2. 重写run()方法,定义共享任务逻辑
@Override
public void run() {
// 多个线程会共享这个run()里的逻辑
System.out.println("当前线程:" + Thread.currentThread().getName() + ",执行共享任务:处理文件片段");
}
}
// 3. 把Runnable实例传给Thread,启动线程
public class RunnableDemo {
public static void main(String[] args) {
// 创建一个共享的Runnable实例
MyRunnable sharedRunnable = new MyRunnable();
// 多个线程共用同一个Runnable实例,共享任务逻辑
Thread threadA = new Thread(sharedRunnable, "文件处理线程A");
Thread threadB = new Thread(sharedRunnable, "文件处理线程B");
threadA.start();
threadB.start();
}
}
适用场景:适合多个线程需要共享任务逻辑的场景,比如多线程处理同一份数据、分布式任务拆分;而且因为是实现接口,不影响类的继承关系,扩展性更强。
注意点:如果 Runnable 实例里有共享变量,要注意线程安全(比如加 synchronized 锁),避免数据错乱。
3. 基础方式 3:实现 Callable 接口(带返回值)
前面两种方式的 run () 方法都没有返回值,也没法抛出受检异常 —— 如果你的任务需要 “执行完返回结果”(比如计算一个复杂的数值、获取远程接口的返回值),那实现 Callable 接口就最合适了。
Callable 需要重写 call () 方法,这个方法可以返回泛型结果,还能抛出异常;不过 Callable 不能直接传给 Thread,得借助 FutureTask 类(它实现了 Runnable 接口)来包装,最后再传给 Thread 启动。
代码示例:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 1. 实现Callable接口,指定返回值类型(这里是Integer)
class MyCallable implements Callable<Integer> {
private int taskId;
// 可以通过构造方法传参,比如任务ID
public MyCallable(int taskId) {
this.taskId = taskId;
}
// 2. 重写call()方法,带返回值和异常抛出
@Override
public Integer call() throws Exception {
System.out.println("当前线程:" + Thread.currentThread().getName() + ",执行计算任务" + taskId);
// 模拟任务执行耗时(比如复杂计算)
Thread.sleep(1000);
// 返回计算结果(比如任务ID*10)
return taskId * 10;
}
}
// 3. 用FutureTask包装Callable,启动线程并获取结果
public class CallableDemo {
public static void main(String[] args) throws Exception {
// 创建Callable实例
MyCallable callable1 = new MyCallable(1);
MyCallable callable2 = new MyCallable(2);
// 用FutureTask包装Callable,FutureTask兼具Runnable和Future的功能
FutureTask<Integer> futureTask1 = new FutureTask<>(callable1);
FutureTask<Integer> futureTask2 = new FutureTask<>(callable2);
// 启动线程
new Thread(futureTask1, "计算线程1").start();
new Thread(futureTask2, "计算线程2").start();
// 调用get()方法获取返回值(会阻塞,直到任务执行完成)
Integer result1 = futureTask1.get();
Integer result2 = futureTask2.get();
System.out.println("计算任务1结果:" + result1); // 输出10
System.out.println("计算任务2结果:" + result2); // 输出20
}
}
适用场景:需要获取线程执行结果的场景,比如异步计算、远程接口调用、多任务结果汇总(比如统计多个数据源的数据后合并)。
注意点:FutureTask 的 get () 方法会阻塞当前线程,直到任务完成,所以如果不需要立即获取结果,可以先做其他逻辑,最后再调用 get ();另外如果任务可能耗时很久,建议设置超时时间(get (long timeout, TimeUnit unit)),避免一直阻塞。
4. 进阶方式 1:线程池(ExecutorService)
前面三种基础方式,每次创建线程都要 new Thread (),但线程的创建和销毁是有开销的 —— 如果频繁创建线程(比如秒杀场景每秒几百个请求),会浪费大量资源,甚至导致线程数过多引发 OOM。
这时候就需要线程池了:线程池会预先创建一批线程,任务来了直接用空闲线程执行,任务结束后线程不销毁,放回池里复用。Java 里通过 ExecutorService 框架实现线程池,常用的创建方式是 Executors 工具类,或者自定义 ThreadPoolExecutor。
代码示例(用 Executors 创建固定大小线程池):
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建固定大小的线程池(比如3个核心线程)
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 2. 提交任务(可以提交Runnable或Callable)
for (int i = 1; i <= 5; i++) {
int taskId = i;
// 提交Runnable任务
threadPool.submit(() -> {
System.out.println("线程池线程:" + Thread.currentThread().getName() + ",执行任务" + taskId);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 3. 关闭线程池(先停止接收新任务,等已提交任务执行完再关闭)
threadPool.shutdown();
}
}
适用场景:高并发、频繁创建线程的场景,比如接口请求处理、批量任务执行(比如定时同步数据)、秒杀系统;几乎所有生产环境的多线程场景,都推荐用线程池,而不是手动 new Thread。
注意点:Executors 工具类创建的线程池有默认参数,比如 newCachedThreadPool 可能创建过多线程导致 OOM,所以生产环境建议自定义 ThreadPoolExecutor,根据业务需求设置核心线程数、最大线程数、队列容量等参数;另外线程池一定要记得关闭,避免资源泄漏。
5. 进阶方式 2:Fork/Join 框架(并行计算)
如果你的任务可以拆分成多个 “子任务”,子任务执行完后再合并结果(比如计算 1 到 10000 的总和,可以拆成 10 个线程分别计算 1-1000、1001-2000…… 最后把结果加起来),那 Fork/Join 框架就很适合 —— 它是 Java 7 引入的,专门用于 “分而治之” 的并行计算场景。
Fork/Join 的核心是 ForkJoinTask 类,通常我们会继承它的子类 RecursiveTask(有返回值)或 RecursiveAction(无返回值),重写 compute () 方法实现任务拆分和合并。
代码示例(用 Fork/Join 计算 1 到 n 的总和):
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
// 1. 继承RecursiveTask(有返回值),指定返回值类型为Long
class SumTask extends RecursiveTask<Long> {
// 拆分阈值:当任务规模小于等于这个值时,直接计算,不拆分
private static final int THRESHOLD = 1000;
private long start;
private long end;
public SumTask(long start, long end) {
this.start = start;
this.end = end;
}
// 2. 重写compute()方法,实现任务拆分和合并
@Override
protected Long compute() {
long sum = 0;
// 判断任务是否需要拆分:如果规模小于阈值,直接计算
if (end - start <= THRESHOLD) {
for (long i = start; i <= end; i++) {
sum += i;
}
} else {
// 任务拆分:分成两个子任务
long mid = (start + end) / 2;
SumTask leftTask = new SumTask(start, mid);
SumTask rightTask = new SumTask(mid + 1, end);
// 执行子任务(fork()方法会异步执行子任务)
leftTask.fork();
rightTask.fork();
// 获取子任务结果并合并(join()方法会阻塞直到子任务完成)
long leftResult = leftTask.join();
long rightResult = rightTask.join();
sum = leftResult + rightResult;
}
return sum;
}
}
// 3. 用ForkJoinPool执行任务
public class ForkJoinDemo {
public static void main(String[] args) {
// 创建ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 创建任务:计算1到10000的总和
SumTask sumTask = new SumTask(1, 10000);
// 提交任务并获取结果
Long totalSum = forkJoinPool.invoke(sumTask);
System.out.println("1到10000的总和:" + totalSum); // 输出50005000
// 关闭线程池
forkJoinPool.shutdown();
}
}
适用场景:大规模并行计算场景,比如大数据量的统计分析(比如计算海量日志的指标)、复杂算法的并行实现(比如归并排序)、文件批量处理(比如遍历文件夹下所有文件并统计大小)。
注意点:任务拆分的阈值很关键 —— 阈值太大可能导致并行度不够,阈值太小会增加任务拆分和合并的开销,需要根据业务场景测试调整;另外 Fork/Join 框架会利用 CPU 的多核特性,适合 CPU 密集型任务,不适合 IO 密集型任务(IO 密集型用线程池更合适)。
总结:3 分钟快速选对多线程创建方式
最后咱们用一张表把 5 种方式的核心区别和适用场景汇总一下,以后遇到需求就能直接对号入座:
创建方式 | 核心特点 | 适用场景 | 注意事项 |
继承 Thread 类 | 单继承、无共享逻辑 | 简单异步任务、无返回值 | 不能继承其他类,避免共享变量 |
实现 Runnable 接口 | 多实现、可共享逻辑 | 多线程共享任务、无返回值 | 共享变量需保证线程安全 |
实现 Callable 接口 | 带返回值、可抛异常 | 需获取任务结果(如异步计算) | FutureTask.get () 会阻塞,建议设超时 |
线程池(ExecutorService) | 线程复用、高并发支持 | 频繁创建线程、IO 密集型任务(如接口处理) | 生产环境建议自定义线程池参数 |
Fork/Join 框架 | 分而治之、并行计算 | 大规模 CPU 密集型任务(如大数据统计) | 合理设置任务拆分阈值,避免过度拆分 |
其实 Java 多线程创建方式没有 “最好”,只有 “最合适”—— 比如做一个简单的日志异步打印,用 Thread 或 Runnable 就够了;但如果是秒杀系统的订单处理,必须用线程池;如果是计算海量数据的指标,Fork/Join 才是最优解。
最后也想跟你互动一下:你平时开发中用得最多的是哪种多线程创建方式?有没有遇到过因为选错方式导致的 bug?欢迎在评论区分享你的经历,咱们一起交流避坑~ 另外如果这篇文章对你有帮助,也别忘了收藏转发,下次遇到多线程需求就能随时翻出来看!
- 上一篇: 大数据开发学习最全汇总_大数据开发基础知识
- 下一篇: 大数据开发:关于JVM内存模型JMM详解
猜你喜欢
- 2025-10-14 SpringBoot+Dubbo+Zookeeper+Redis+MQ分布式快速开发平台源码
- 2025-10-14 大数据开发工程师是做什么的?_大数据开发工程师怎么样
- 2025-10-14 如何学好大数据开发?---shell基本语法
- 2025-10-14 译见:从理论到实践,基于Java的开源大数据工具
- 2025-10-14 基于springcloud2.x+jeesuite-libs构建企业级开发平台源码分享
- 2025-10-14 深度揭秘!互联网大厂Java开发不得不知的关键技术与前沿趋势
- 2025-10-14 大数据开发:关于JVM内存模型JMM详解
- 2025-10-14 大数据开发学习最全汇总_大数据开发基础知识
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)