专业的JAVA编程教程与资源

网站首页 > java教程 正文

Java 开发必看!多线程创建 3 种核心方式 + 2 种进阶方案讲透

temp10 2025-10-14 05:09:12 java教程 2 ℃ 0 评论

作为 Java 开发,你是不是也遇到过这种情况?明明代码本地跑起来没问题,一上线就出现奇怪的线程安全问题 —— 要么数据计算错乱,要么接口响应突然变慢,排查半天发现,竟然是最开始创建多线程的方式选错了!

我之前帮同事排查过一个订单处理的 bug,明明单测时订单状态更新都正常,线上高峰期却总有几笔订单状态 “卡住”。最后定位到代码里,他用继承 Thread 类的方式创建线程,还在子类里加了共享变量,结果多线程并发时变量被反复覆盖,导致状态同步出了问题。其实如果最开始选实现 Runnable 接口的方式,这类问题完全可以避免。

Java 开发必看!多线程创建 3 种核心方式 + 2 种进阶方案讲透

所以今天咱们就好好聊聊 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?欢迎在评论区分享你的经历,咱们一起交流避坑~ 另外如果这篇文章对你有帮助,也别忘了收藏转发,下次遇到多线程需求就能随时翻出来看!

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表