【Java 并发编程】线程创建 6 种方式:Thread/Runnable/Callable 核心类解析+线程池使用说明
引言:线程与Java并发的核心
在Java中,线程是实现并发编程的基础单元,它允许程序在同一时间执行多个任务(如后台处理、异步通信等)。Java提供了多种创建线程的方式,每种方式都有其设计初衷、适用场景和优缺点。本文将以总分总结构,详细拆解Java中创建线程的6种核心方式,包括原理剖析、代码实战、注意事项,并通过流程图辅助理解,帮助你彻底掌握线程创建的底层逻辑与实践技巧。
文章目录
- 注意:输出顺序是由每个线程自己抢的,不是固定的。
一、继承Thread类(最基础的线程创建方式)
Thread是Java中封装线程操作的核心类,它本身实现了Runnable接口。通过继承Thread类并重写run()方法,可以定义线程的执行逻辑,这是最基础的线程创建方式。
1. 原理剖析
Thread类的核心作用:封装了线程的生命周期(新建、就绪、运行、阻塞、终止)和底层操作系统调用(如启动线程、中断线程)。- 线程执行逻辑的载体:
run()方法是线程的“任务入口”,当线程启动后,JVM会自动调用该方法执行任务;若未重写run(),则会执行父类Thread的默认实现(无实际逻辑)。
2. 实现步骤
- 定义自定义类,继承
Thread类; - 重写
Thread类的run()方法,在方法体内编写线程要执行的任务逻辑; - 创建自定义类的实例(即线程对象);
- 调用线程对象的
start()方法,启动线程(注意:不可直接调用run()方法)。
3. 完整代码示例
/** * 方式1:继承Thread类创建线程 */publicclassThreadExtendDemoextendsThread{// 1. 重写run()方法,定义线程任务@Overridepublicvoidrun(){// 线程要执行的逻辑:这里模拟循环打印for(int i =0; i <5; i++){// Thread.currentThread().getName():获取当前线程名称System.out.println("["+Thread.currentThread().getName()+"] 执行任务,i="+ i);try{// 模拟任务耗时:让线程休眠100ms,释放CPU资源Thread.sleep(100);}catch(InterruptedException e){ e.printStackTrace();}}}publicstaticvoidmain(String[] args){// 2. 创建线程实例(自定义Thread子类对象)ThreadExtendDemo thread1 =newThreadExtendDemo();ThreadExtendDemo thread2 =newThreadExtendDemo();// 可选:设置线程名称(便于调试) thread1.setName("线程A"); thread2.setName("线程B");// 3. 调用start()方法启动线程(JVM会自动调用run()) thread1.start(); thread2.start();// 主线程逻辑:与子线程并发执行System.out.println("["+Thread.currentThread().getName()+"] 主线程执行完毕");}}执行结果(部分):
[main] 主线程执行完毕 [线程A] 执行任务,i=0 [线程B] 执行任务,i=0 [线程A] 执行任务,i=1 [线程B] 执行任务,i=1 注意:输出顺序是由每个线程自己抢的,不是固定的。
4. 关键注意点:start() vs run()
很多初学者会直接调用run()方法,但这是错误的!二者的核心区别在于是否创建新线程:
graph TD A[调用 thread.start() ] --> B[JVM向操作系统申请新线程资源] B --> C[新线程启动后,自动调用 run() 方法] C --> D[任务在新线程中执行] E[直接调用 thread.run() ] --> F[无新线程创建,run() 作为普通方法执行]graph TD A[调用 thread.start() ] --> B[JVM向操作系统申请新线程资源] B --> C[新线程启动后,自动调用 run() 方法] C --> D[任务在新线程中执行] E[直接调用 thread.run() ] --> F[无新线程创建,run() 作为普通方法执行] 示例验证:若将上述代码中的thread1.start()改为thread1.run(),执行结果会变成“主线程先执行完run()逻辑,再执行主线程打印”,完全失去并发效果。
5. 优缺点分析
| 优点 | 缺点 |
|---|---|
| 实现简单,直接继承Thread即可 | 受Java单继承限制:若类已继承其他类(如Object外的类),则无法再继承Thread |
可直接通过this获取当前线程对象 | 任务与线程耦合:线程对象与任务逻辑绑定,无法复用线程执行不同任务 |
二、实现Runnable接口(解耦首选方式)
为解决Thread类的单继承限制,Java提供了Runnable接口——它仅定义了一个run()方法,代表线程要执行的任务。通过实现Runnable接口,可以将“线程对象”与“任务逻辑”解耦,是实际开发中更常用的方式。
1. 原理剖析
Runnable是一个函数式接口(Java 8+),定义如下:
@FunctionalInterfacepublicinterfaceRunnable{voidrun();// 仅包含任务逻辑,无返回值、不抛checked异常}- 核心逻辑:
Thread类有一个构造器Thread(Runnable target),可接收Runnable实例(任务)。当线程启动后,JVM会调用target.run(),从而实现“线程对象”与“任务”的分离。
2. 实现步骤
- 定义自定义类,实现
Runnable接口; - 重写
Runnable的run()方法,编写任务逻辑; - 创建
Runnable实例(任务对象); - 创建
Thread实例,将Runnable实例传入Thread构造器; - 调用
Thread实例的start()方法启动线程。
3. 完整代码示例
/** * 方式2:实现Runnable接口创建线程 */publicclassRunnableImplDemoimplementsRunnable{// 1. 重写run()方法,定义任务逻辑@Overridepublicvoidrun(){for(int i =0; i <5; i++){System.out.println("["+Thread.currentThread().getName()+"] 执行任务,i="+ i);try{Thread.sleep(100);}catch(InterruptedException e){ e.printStackTrace();}}}publicstaticvoidmain(String[] args){// 2. 创建任务对象(Runnable实例)RunnableImplDemo task =newRunnableImplDemo();// 3. 创建线程对象,将任务传入Thread构造器(关键:线程与任务分离)Thread thread1 =newThread(task,"线程C");// 第二个参数直接设置线程名Thread thread2 =newThread(task,"线程D");// 4. 启动线程 thread1.start(); thread2.start();System.out.println("["+Thread.currentThread().getName()+"] 主线程执行完毕");}}执行结果(部分):
[main] 主线程执行完毕 [线程C] 执行任务,i=0 [线程D] 执行任务,i=0 [线程C] 执行任务,i=1 [线程D] 执行任务,i=1 4. 核心优势:任务复用与解耦
与“继承Thread”相比,Runnable的核心优势是任务可复用——同一个Runnable实例(任务)可以被多个Thread实例(线程)共享执行。
例如,若要实现“两个线程共同累加一个计数器”,用Runnable可轻松实现(任务共享计数器):
publicclassSharedTaskDemoimplementsRunnable{privateint count =0;// 共享计数器(线程安全需额外处理,此处仅演示复用)@Overridepublicvoidrun(){for(int i =0; i <3; i++){ count++;System.out.println("["+Thread.currentThread().getName()+"] count="+ count);}}publicstaticvoidmain(String[] args){SharedTaskDemo sharedTask =newSharedTaskDemo();// 两个线程共享同一个任务对象,操作同一个countnewThread(sharedTask,"线程E").start();newThread(sharedTask,"线程F").start();}}执行结果(可能):
[线程E] count=1 [线程F] count=2 [线程E] count=3 [线程F] count=4 [线程E] count=5 [线程F] count=6 5. 与Thread类的对比
| 对比维度 | 继承Thread类 | 实现Runnable接口 |
|---|---|---|
| 继承限制 | 受单继承限制,无法再继承其他类 | 无继承限制,可同时实现其他接口 |
| 耦合度 | 线程与任务耦合(线程对象即任务) | 线程与任务解耦(任务独立,可复用) |
| 代码扩展性 | 差(任务逻辑无法单独抽离) | 好(任务可作为参数传递,便于模块化) |
| Java 8支持 | 可通过匿名内部类简化,但不如Runnable灵活 | 支持Lambda表达式(因Runnable是函数式接口) |
6. 优缺点分析
| 优点 | 缺点 |
|---|---|
| 无单继承限制,灵活性更高 | 无法直接获取线程对象:需通过Thread.currentThread()获取,而非this |
| 任务与线程解耦,支持任务复用 | 无返回值:run()方法无返回值,无法获取线程执行结果 |
| 支持Lambda表达式(Java 8+),代码更简洁 | 不抛checked异常:run()方法声明无异常抛出,需在方法内部捕获 |
三、实现Callable接口(带返回值的线程)
无论是Thread还是Runnable,都存在一个明显缺陷:无法获取线程执行的返回结果。为解决这个问题,Java 5引入了Callable接口——它与Runnable类似,但支持返回值和抛出checked异常。
不过,Callable不能直接传入Thread(因Thread仅接收Runnable),需借助FutureTask作为“桥梁”(FutureTask实现了Runnable接口)。
1. 原理剖析
(1)Callable接口定义
Callable是一个泛型接口,泛型参数代表返回值类型,核心方法为call():
@FunctionalInterfacepublicinterfaceCallable<V>{Vcall()throwsException;// 有返回值、可抛checked异常}(2)FutureTask的桥梁作用
FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable和Future:
publicclassFutureTask<V>implementsRunnableFuture<V>{// 构造器:接收Callable实例publicFutureTask(Callable<V> callable){...}// 实现Runnable的run()方法:内部会调用Callable的call(),并存储结果@Overridepublicvoidrun(){...}}publicinterfaceRunnableFuture<V>extendsRunnable,Future<V>{...}因此,FutureTask的核心作用是:
- 作为
Runnable,可传入Thread启动线程; - 作为
Future,可通过get()方法获取Callable的返回值、判断任务是否完成、取消任务。
(3)核心关系流程图
graph LR A[实现Callable接口<br>重写call()(带返回值)] -->|创建实例| B[Callable<V> 任务对象] B -->|传入构造器| C[FutureTask<V> 实例<br>(实现RunnableFuture)] C -->|作为Runnable传入| D[Thread 线程对象] D -->|调用start()| E[执行call()并存储结果] C -->|调用get()| F[获取返回值/抛出异常] C -->|调用isDone()| G[判断任务是否完成] 2. 实现步骤
- 定义自定义类,实现
Callable<V>接口(V为返回值类型); - 重写
call()方法,编写任务逻辑并返回结果(可抛异常); - 创建
Callable<V>实例(任务对象); - 创建
FutureTask<V>实例,将Callable实例传入; - 创建
Thread实例,将FutureTask实例传入; - 调用
Thread的start()方法启动线程; - 调用
FutureTask的get()方法获取call()的返回值(会阻塞当前线程,直到结果返回)。
3. 完整代码示例
importjava.util.concurrent.Callable;importjava.util.concurrent.ExecutionException;importjava.util.concurrent.FutureTask;/** * 方式3:实现Callable接口(带返回值)+ FutureTask */publicclassCallableImplDemoimplementsCallable<Integer>{privateint start;privateint end;// 构造器:传入计算范围(示例:计算start到end的累加和)publicCallableImplDemo(int start,int end){this.start = start;this.end = end;}// 1. 重写call()方法,带返回值、可抛异常@OverridepublicIntegercall()throwsException{int sum =0;for(int i = start; i <= end; i++){ sum += i;System.out.println("["+Thread.currentThread().getName()+"] 正在计算:i="+ i +",当前和="+ sum);Thread.sleep(50);// 模拟耗时}return sum;// 返回计算结果}publicstaticvoidmain(String[] args){// 2. 创建Callable任务对象(计算1-10的和)Callable<Integer> callableTask =newCallableImplDemo(1,10);// 3. 创建FutureTask实例(桥梁:Callable → Runnable)FutureTask<Integer> futureTask =newFutureTask<>(callableTask);// 4. 创建Thread并传入FutureTask,启动线程Thread thread =newThread(futureTask,"计算线程"); thread.start();// 5. 主线程逻辑:获取结果(会阻塞,直到子线程完成)try{// get():阻塞当前线程,直到call()执行完毕并返回结果Integer result = futureTask.get();System.out.println("\n["+Thread.currentThread().getName()+"] 子线程计算结果:1-10的和="+ result);}catch(InterruptedException e){// 线程被中断时抛出 e.printStackTrace();}catch(ExecutionException e){// call()方法抛出异常时,会被封装为ExecutionException抛出System.out.println("子线程执行异常:"+ e.getCause().getMessage());}}}执行结果(部分):
[计算线程] 正在计算:i=1,当前和=1 [计算线程] 正在计算:i=2,当前和=3 ... [计算线程] 正在计算:i=10,当前和=55 [main] 子线程计算结果:1-10的和=55 4. 关键注意点
get()方法的阻塞性:futureTask.get()会阻塞调用线程(如主线程),直到子线程的call()方法执行完毕。若需避免阻塞,可先通过futureTask.isDone()判断任务是否完成,再决定是否调用get()。- 异常处理:
call()方法抛出的异常会被封装为ExecutionException,需通过e.getCause()获取原始异常。 - 任务取消:可通过
futureTask.cancel(boolean mayInterruptIfRunning)取消任务:mayInterruptIfRunning=true:若任务已在执行,会中断线程;mayInterruptIfRunning=false:仅取消未开始的任务。
5. 优缺点分析
| 优点 | 缺点 |
|---|---|
| 支持返回值:可获取线程执行的结果 | get()方法会阻塞:若子线程执行时间长,会阻塞调用线程 |
| 支持抛异常:可将任务中的异常抛出到调用线程处理 | 代码复杂度高:需额外创建FutureTask实例,步骤比Runnable多 |
可取消任务:通过cancel()方法终止未完成的任务 | 无法直接复用任务:若多个线程需执行同一任务,需创建多个Callable实例 |
四、使用线程池(高并发场景必备)
无论是继承Thread、实现Runnable还是Callable,每次创建线程都会涉及“操作系统内核态与用户态的切换”,且线程执行完毕后会被销毁——频繁创建/销毁线程会带来巨大的性能开销。
为解决这个问题,Java提供了线程池(ExecutorService):线程池会预先创建一批线程,线程执行完任务后不会销毁,而是回到线程池等待下一个任务,从而实现线程的复用,降低性能开销。
1. 原理剖析
(1)线程池的核心组件
- 线程池管理器(ExecutorService):负责线程池的创建、管理和销毁,提供提交任务的接口(如
submit()、execute())。 - 工作线程(Worker):线程池中预先创建的线程,负责执行任务,执行完后回到线程池等待新任务。
- 任务队列(BlockingQueue):当核心线程都在忙时,新提交的任务会被放入任务队列暂存。
- 拒绝策略(RejectedExecutionHandler):当任务队列满且线程池达到最大线程数时,对新任务的处理策略(如抛出异常、丢弃任务等)。
(2)线程池工作流程


(3)核心参数解析
创建线程池的核心类是ThreadPoolExecutor,其构造器包含7个核心参数,决定了线程池的行为:
| 参数名称 | 类型 | 作用 | 示例 |
|---|---|---|---|
| corePoolSize | int | 核心线程数:线程池长期维持的线程数量(即使空闲也不销毁) | 核心线程数=CPU核心数+1 |
| maximumPoolSize | int | 最大线程数:线程池允许创建的最大线程数(核心线程数+非核心线程数) | 最大线程数=CPU核心数*2 |
| keepAliveTime | long | 非核心线程空闲时间:超过此时间,非核心线程会被销毁 | 60L(单位:秒) |
| unit | TimeUnit | keepAliveTime的时间单位 | TimeUnit.SECONDS |
| workQueue | BlockingQueue | 任务队列:暂存待执行任务的队列 | LinkedBlockingQueue(无界队列)、ArrayBlockingQueue(有界队列) |
| threadFactory | ThreadFactory | 线程工厂:用于创建线程(可自定义线程名称、优先级等) | Executors.defaultThreadFactory() |
| handler | RejectedExecutionHandler | 拒绝策略:任务队列满且线程数达最大时的处理策略 | AbortPolicy(默认:抛出异常) |
2. 线程池的创建方式
Java提供了两种创建线程池的方式:
- 通过
Executors工具类:快速创建预设参数的线程池(适合简单场景,不推荐高并发场景); - 直接创建
ThreadPoolExecutor实例:自定义核心参数(推荐,可避免Executors的潜在风险)。
(1)方式1:通过Executors工具类创建
Executors提供了4种常用的线程池工厂方法:
| 线程池类型 | 工厂方法 | 核心参数特点 | 适用场景 |
|---|---|---|---|
| FixedThreadPool | Executors.newFixedThreadPool(n) | 核心线程数=最大线程数=n,队列无界 | 任务数量固定、需长期执行的场景(如服务端处理请求) |
| CachedThreadPool | Executors.newCachedThreadPool() | 核心线程数=0,最大线程数=Integer.MAX_VALUE,队列同步移交 | 任务数量多、执行时间短的场景(如临时任务处理) |
| SingleThreadExecutor | Executors.newSingleThreadExecutor() | 核心线程数=1,最大线程数=1,队列无界 | 需串行执行任务的场景(如日志写入、单线程处理请求) |
| ScheduledThreadPool | Executors.newScheduledThreadPool(n) | 核心线程数=n,最大线程数=Integer.MAX_VALUE | 定时/周期性执行任务的场景(如定时备份、心跳检测) |
代码示例:FixedThreadPool
importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;/** * 方式4:通过Executors创建FixedThreadPool */publicclassExecutorsFixedDemo{publicstaticvoidmain(String[] args){// 1. 创建FixedThreadPool(核心线程数=2,最大线程数=2)ExecutorService executorService =Executors.newFixedThreadPool(2);// 2. 提交3个任务(核心线程数=2,第3个任务会进入队列等待)for(int i =1; i <=3; i++){int taskId = i;// 匿名内部类引用外部变量需final或有效final// 提交Runnable任务(无返回值) executorService.execute(()->{System.out.println("["+Thread.currentThread().getName()+"] 执行任务"+ taskId);try{Thread.sleep(500);}catch(InterruptedException e){ e.printStackTrace();}});}// 3. 提交Callable任务(有返回值,需用submit()方法) executorService.submit(()->{Thread.sleep(300);return"Callable任务执行完毕";}).thenAccept(result ->System.out.println("Callable任务结果:"+ result));// Java 8+ CompletableFuture特性// 4. 关闭线程池(重要:不关闭会导致JVM无法退出)// shutdown():等待所有已提交任务执行完毕后关闭线程池 executorService.shutdown();// shutdownNow():立即关闭线程池,终止未执行的任务(慎用)// executorService.shutdownNow();}}执行结果(部分):
[pool-1-thread-1] 执行任务1 [pool-1-thread-2] 执行任务2 [pool-1-thread-1] 执行任务3 Callable任务结果:Callable任务执行完毕 (2)方式2:直接创建ThreadPoolExecutor(推荐)
Executors创建的线程池存在潜在风险(如FixedThreadPool的无界队列可能导致OOM),因此阿里巴巴《Java开发手册》推荐直接使用ThreadPoolExecutor自定义线程池,明确核心参数,避免资源耗尽。
代码示例:自定义线程池
importjava.util.concurrent.ArrayBlockingQueue;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;/** * 方式4:直接创建ThreadPoolExecutor(推荐) */publicclassThreadPoolExecutorDemo{publicstaticvoidmain(String[] args){// 1. 定义核心参数int corePoolSize =2;// 核心线程数=2int maximumPoolSize =4;// 最大线程数=4long keepAliveTime =60L;// 非核心线程空闲60秒后销毁TimeUnit unit =TimeUnit.SECONDS;// 时间单位:秒// 任务队列:有界队列,容量=3(超过3个任务会创建非核心线程)ArrayBlockingQueue<Runnable> workQueue =newArrayBlockingQueue<>(3);// 拒绝策略:当队列满且线程数达最大时,抛出异常(默认策略)ThreadPoolExecutor.AbortPolicy abortPolicy =newThreadPoolExecutor.AbortPolicy();// 2. 创建自定义线程池ExecutorService customExecutor =newThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(),// 默认线程工厂 abortPolicy );// 3. 提交5个任务(核心2 + 队列3 = 5,无需创建非核心线程)for(int i =1; i <=5; i++){int taskId = i; customExecutor.execute(()->{System.out.println("["+Thread.currentThread().getName()+"] 执行任务"+ taskId);try{Thread.sleep(500);}catch(InterruptedException e){ e.printStackTrace();}});}// 4. 提交第6个任务(队列满,创建第3个线程) customExecutor.execute(()->{System.out.println("["+Thread.currentThread().getName()+"] 执行任务6(非核心线程)");});// 5. 关闭线程池 customExecutor.shutdown();}}3. 关键注意点
- 线程池的关闭:必须调用
shutdown()或shutdownNow()关闭线程池,否则线程池的核心线程会一直存活,导致JVM无法退出。shutdown():温和关闭,等待所有已提交任务执行完毕后关闭;shutdownNow():强制关闭,立即中断所有正在执行的任务,并返回未执行的任务列表。
- 拒绝策略的选择:
AbortPolicy(默认):抛出RejectedExecutionException,适合需要明确感知任务拒绝的场景;DiscardPolicy:直接丢弃任务,不抛出异常,适合非核心任务;DiscardOldestPolicy:丢弃队列中最旧的任务,然后提交新任务,适合任务有先后顺序的场景;CallerRunsPolicy:由提交任务的线程(如主线程)自己执行任务,适合需要避免任务丢失的场景。
- 线程池参数的调优:核心参数需根据业务场景调整,例如:
- CPU密集型任务(如计算):核心线程数=CPU核心数+1(减少线程切换开销);
- IO密集型任务(如网络请求、数据库操作):核心线程数=CPU核心数*2(利用IO等待时间复用线程)。
4. 优缺点分析
| 优点 | 缺点 |
|---|---|
| 线程复用:避免频繁创建/销毁线程,降低性能开销 | 配置复杂:需根据业务场景合理设置核心参数(如队列大小、最大线程数) |
| 控制并发:通过核心参数限制最大并发数,避免系统资源耗尽 | 任务堆积风险:若任务执行速度慢,队列可能堆积导致OOM(需用有界队列) |
| 管理便捷:提供统一的任务提交和线程管理接口 | 线程泄漏风险:若忘记关闭线程池,核心线程会一直存活,浪费资源 |
支持异步:可结合Callable获取任务结果,支持定时任务 | 调试难度高:多线程并发问题(如死锁、线程安全)排查复杂 |
五、CompletableFuture(Java 8+异步神器)
Callable+FutureTask虽然支持返回值,但存在一个痛点:获取结果时需要主动调用get()方法,会阻塞线程。为解决这个问题,Java 8引入了CompletableFuture——它基于FutureTask扩展,支持异步回调,无需阻塞即可处理线程执行结果,极大简化了异步编程。
1. 原理剖析
CompletableFuture实现了CompletionStage和Future接口:
CompletionStage:定义了异步任务的“阶段”,支持链式调用(如thenAccept()、thenApply()),一个阶段完成后自动触发下一个阶段;Future:继承了Future的核心能力(如get()、cancel())。
CompletableFuture默认使用ForkJoinPool.commonPool()(一个共享的线程池)执行任务,也可自定义线程池。
2. 核心方法分类
CompletableFuture的方法众多,按功能可分为“创建异步任务”和“处理任务结果”两类:
| 方法类型 | 核心方法 | 作用 |
|---|---|---|
| 创建异步任务(无返回值) | runAsync(Runnable runnable) | 使用默认线程池执行任务 |
runAsync(Runnable runnable, Executor executor) | 使用自定义线程池执行任务 | |
| 创建异步任务(有返回值) | supplyAsync(Supplier<U> supplier) | 使用默认线程池执行任务,返回结果 |
supplyAsync(Supplier<U> supplier, Executor executor) | 使用自定义线程池执行任务,返回结果 | |
| 处理任务结果(回调) | thenAccept(Consumer<? super U> action) | 任务完成后,消费结果(无返回值) |
thenApply(Function<? super U, ? extends V> fn) | 任务完成后,转换结果(有返回值,可链式调用) | |
exceptionally(Function<Throwable, ? extends U> fn) | 任务异常时,处理异常并返回默认值 |
3. 完整代码示例
(1)示例1:无返回值的异步任务
importjava.util.concurrent.CompletableFuture;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;/** * 方式5:CompletableFuture(无返回值) */publicclassCompletableFutureRunAsyncDemo{publicstaticvoidmain(String[] args){// 1. 创建自定义线程池(推荐,避免使用默认的ForkJoinPool)ExecutorService executor =Executors.newFixedThreadPool(2);// 2. 使用runAsync创建无返回值的异步任务CompletableFuture<Void> future =CompletableFuture.runAsync(()->{System.out.println("["+Thread.currentThread().getName()+"] 执行异步任务(无返回值)");try{Thread.sleep(1000);}catch(InterruptedException e){ e.printStackTrace();}}, executor);// 传入自定义线程池// 3. 任务完成后的回调(无需阻塞,自动执行) future.thenRun(()->{System.out.println("["+Thread.currentThread().getName()+"] 异步任务执行完毕(回调)");});// 4. 主线程继续执行其他逻辑(无阻塞)System.out.println("["+Thread.currentThread().getName()+"] 主线程执行其他任务");// 5. 关闭线程池(注意:需等待异步任务完成,否则可能中断任务) executor.shutdown();}}执行结果:
[main] 主线程执行其他任务 [pool-1-thread-1] 执行异步任务(无返回值) [pool-1-thread-1] 异步任务执行完毕(回调) (2)示例2:有返回值的异步任务+链式回调
importjava.util.concurrent.CompletableFuture;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;/** * 方式5:CompletableFuture(有返回值+链式回调) */publicclassCompletableFutureSupplyAsyncDemo{publicstaticvoidmain(String[] args){ExecutorService executor =Executors.newSingleThreadExecutor();// 1. supplyAsync:创建有返回值的异步任务(返回String)CompletableFuture<String> future =CompletableFuture.supplyAsync(()->{System.out.println("["+Thread.currentThread().getName()+"] 执行异步任务(有返回值)");try{Thread.sleep(1000);}catch(InterruptedException e){thrownewRuntimeException("任务被中断", e);}return"Hello, CompletableFuture!";// 返回结果}, executor);// 2. thenApply:转换结果(String → Integer,返回新的CompletableFuture)CompletableFuture<Integer> lengthFuture = future.thenApply(result ->{System.out.println("["+Thread.currentThread().getName()+"] 转换结果:原结果="+ result);return result.length();// 将字符串转换为长度(Integer)});// 3. thenAccept:消费转换后的结果(无返回值) lengthFuture.thenAccept(length ->{System.out.println("["+Thread.currentThread().getName()+"] 最终结果:字符串长度="+ length);});// 4. exceptionally:处理异常(若任务抛出异常,返回默认值) future.exceptionally(ex ->{System.out.println("任务执行异常:"+ ex.getMessage());return"默认值";// 异常时的返回值}); executor.shutdown();}}执行结果:
[pool-1-thread-1] 执行异步任务(有返回值) [pool-1-thread-1] 转换结果:原结果=Hello, CompletableFuture! [pool-1-thread-1] 最终结果:字符串长度=23 4. 核心优势:非阻塞与链式编程
与传统的FutureTask相比,CompletableFuture的核心优势在于:
- 非阻塞回调:无需调用
get()阻塞线程,任务完成后自动触发回调方法; - 链式调用:支持多个回调阶段的链式组合(如
supplyAsync() → thenApply() → thenAccept()),代码更简洁; - 多任务组合:支持多个异步任务的协同(如
allOf():等待所有任务完成;anyOf():等待任意一个任务完成); - 自定义线程池:可避免默认
ForkJoinPool的资源竞争问题。
5. 优缺点分析
| 优点 | 缺点 |
|---|---|
| 非阻塞回调:无需阻塞线程,提高程序吞吐量 | 学习成本高:方法众多,需理解CompletionStage的阶段模型 |
| 链式编程:简化多步骤异步任务的代码逻辑 | 调试难度高:链式回调的异常堆栈可能不清晰 |
| 支持多任务组合:轻松实现复杂的异步协同逻辑 | 默认线程池风险:若使用默认ForkJoinPool,高并发下可能导致资源竞争 |
支持异常处理:通过exceptionally()统一处理异常 | 内存泄漏风险:若忘记关闭自定义线程池,会导致资源浪费 |
六、总结:如何选择合适的线程创建方式?
Java提供的6种线程创建方式,各有适用场景,选择的核心依据是业务需求(是否需要返回值、并发量、是否异步) 和性能要求。以下是各方式的适用场景汇总:
| 线程创建方式 | 核心特点 | 适用场景 |
|---|---|---|
| 继承Thread类 | 实现简单,单继承限制 | 简单的并发场景,无其他继承需求 |
| 实现Runnable接口 | 无继承限制,任务解耦 | 普通并发场景,需复用任务或多实现接口 |
| 实现Callable+FutureTask | 支持返回值和异常,需阻塞获取结果 | 需获取线程执行结果的场景(如计算任务) |
| 线程池(ExecutorService) | 线程复用,控制并发,高性能 | 高并发场景(如服务端处理请求、批量任务) |
| CompletableFuture | 非阻塞回调,链式编程,异步协同 | Java 8+的异步场景(如微服务调用、IO密集型任务) |
关键建议
- 避免频繁创建独立线程:除非是简单的一次性任务,否则优先使用线程池或
CompletableFuture,避免线程创建/销毁的性能开销; - 高并发场景首选线程池:通过自定义
ThreadPoolExecutor明确核心参数,避免Executors的潜在风险; - 异步回调用CompletableFuture:Java 8及以上版本,若需异步处理结果,优先使用
CompletableFuture,简化代码并提高吞吐量; - 线程安全是前提:无论选择哪种方式,都需注意线程安全(如使用
synchronized、ConcurrentHashMap等),避免数据竞争问题。
通过本文的详细拆解,相信你已掌握Java线程创建的所有方式。在实际开发中,需结合业务场景灵活选择,才能写出高效、稳定的并发代码。