创建线程的几种方式?8 年 Java 开发:从业务场景到代码实战(附避坑指南)

创建线程的几种方式?8 年 Java 开发:从业务场景到代码实战(附避坑指南)

作为一名摸爬滚打 8 年的 Java 开发,从刚入行时 “new Thread () 一把梭”,到后来电商高并发场景下 “线程池参数调优到凌晨”,再到现在微服务异步链路用 “CompletableFuture 链式调用”,创建线程的方式选不对,不仅会踩坑(比如 OOM、线程泄露),还会直接影响系统性能。

今天不聊空洞的理论,只从实际业务场景出发,带你搞懂 Java 中创建线程的 6 种常见方式 —— 每种方式都附 “业务场景 + 核心代码 + 优缺点 + 避坑点”,看完就能直接用到项目里。

一、先想清楚:为什么需要 “创建线程”?

新手容易上来就问 “怎么创建线程”,但八年开发的经验是:先明确业务需求,再选合适的方式。比如这些场景都需要多线程:

  • 场景 1:电商下单后,异步发送短信 / 推送通知(不能让用户等通知发送完才看到下单成功);
  • 场景 2:批量处理 10 万条订单数据(单线程处理要 10 分钟,多线程分拆后 1 分钟搞定);
  • 场景 3:调用第三方接口(比如支付回调),需要超时重试(单独开线程避免阻塞主线程);
  • 场景 4:计算订单总额(需要异步计算商品折扣、优惠券金额,最后汇总结果)。

不同场景对应不同的线程创建方式 —— 比如 “简单异步通知” 用线程池就行,“需要返回结果的计算” 就得用 Callable,“复杂异步链路” 必须上 CompletableFuture。

二、6 种创建线程的方式:从简单到实战

1. 方式 1:继承 Thread 类(新手入门级,生产用得少)

业务场景

最简单的单任务异步操作,比如 “打印用户下单日志”(不需要返回结果,逻辑简单)。

核心代码
// 1. 自定义线程类,继承Thread
class OrderLogThread extends Thread {
    // 传入业务参数(比如订单ID)
    private Long orderId;

    public OrderLogThread(Long orderId) {
        this.orderId = orderId;
    }

    // 2. 重写run()方法:线程要执行的业务逻辑
    @Override
    public void run() {
        // 模拟打印日志(实际业务可能是写日志到ELK、数据库)
        System.out.println("线程" + Thread.currentThread().getName() + ":打印订单日志,订单ID=" + orderId);
    }
}

// 3. 调用方式
public class ThreadDemo {
    public static void main(String[] args) {
        // 新建线程对象
        Thread logThread = new OrderLogThread(1001L);
        // 启动线程(必须调用start(),不是run()!)
        logThread.start();
    }
}
优缺点分析
优点缺点
代码简单,新手易理解不能多继承(Java 单继承限制,扩展性差)
直接操作 Thread 对象业务逻辑与线程耦合(线程类既管业务又管线程)
无法返回执行结果(run () 无返回值)
八年开发解析
  • 新手坑:直接调用logThread.run()不会启动新线程!run()只是普通方法,必须用start()—— 它会告诉 JVM“启动一个新线程,执行 run () 里的逻辑”。
  • 生产不用:这种方式耦合太高,比如后续要给线程加 “超时控制”“结果返回”,根本没法扩展,实际项目中几乎不用。

2. 方式 2:实现 Runnable 接口(解耦入门级,常用)

业务场景

多线程处理相同任务,比如 “批量处理 100 条订单,每个线程处理 10 条”(任务与线程分离,便于复用)。

核心代码
// 1. 实现Runnable接口:封装业务逻辑(任务)
class OrderProcessTask implements Runnable {
    private List<Long> orderIds; // 要处理的订单ID列表

    public OrderProcessTask(List<Long> orderIds) {
        this.orderIds = orderIds;
    }

    // 2. 重写run():定义任务逻辑
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (Long orderId : orderIds) {
            // 模拟处理订单(比如更新订单状态为“已处理”)
            System.out.println("线程" + threadName + ":处理订单,ID=" + orderId);
        }
    }
}

// 3. 调用方式:用Thread包装Runnable任务
public class RunnableDemo {
    public static void main(String[] args) {
        // 准备100条订单ID
        List<Long> allOrderIds = IntStream.rangeClosed(1, 100)
                .mapToObj(Long::valueOf)
                .collect(Collectors.toList());

        // 分10个线程处理,每个线程处理10条
        for (int i = 0; i < 10; i++) {
            // 分片订单ID(第i个线程处理第i*10到(i+1)*10条)
            int start = i * 10;
            int end = Math.min((i + 1) * 10, allOrderIds.size());
            List<Long> subOrderIds = allOrderIds.subList(start, end);

            // 用Thread包装Runnable任务,启动线程
            Thread taskThread = new Thread(new OrderProcessTask(subOrderIds));
            taskThread.start();
        }
    }
}
优缺点分析
优点缺点
解耦(任务与线程分离)无法返回执行结果(run () 无返回值)
支持多实现(可再实现其他接口)不能直接启动,需用 Thread 包装
任务可复用(多个 Thread 可共用一个 Runnable)异常只能在 run () 内部处理,无法向外抛出
八年开发解析
  • 核心改进:Runnable 把 “任务逻辑” 和 “线程控制” 分开了 —— 比如OrderProcessTask只关心 “怎么处理订单”,至于用哪个线程执行、怎么启动,交给 Thread 去管,符合单一职责原则
  • 生产常用:简单异步任务(无返回值)几乎都用这种方式,比如异步发送短信、日志异步写入。

3. 方式 3:实现 Callable 接口(有返回值,进阶)

业务场景

需要异步获取执行结果的场景,比如 “计算订单总额(要累加商品金额、折扣、优惠券)”,每个计算步骤异步执行,最后汇总结果。

核心代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 1. 实现Callable接口:泛型指定返回值类型(这里是Double,订单总额)
class OrderAmountCalculateTask implements Callable<Double> {
    private Long orderId;

    public OrderAmountCalculateTask(Long orderId) {
        this.orderId = orderId;
    }

    // 2. 重写call():有返回值,可抛异常
    @Override
    public Double call() throws Exception {
        String threadName = Thread.currentThread().getName();
        System.out.println("线程" + threadName + ":开始计算订单" + orderId + "的总额");

        // 模拟复杂计算(比如调用商品服务查单价、折扣服务算优惠)
        Thread.sleep(1000); // 模拟耗时操作
        double goodsAmount = 299.9; // 商品金额
        double discount = 50.0; // 折扣金额
        double total = goodsAmount - discount;

        System.out.println("线程" + threadName + ":订单" + orderId + "总额计算完成,结果=" + total);
        return total; // 返回计算结果
    }
}

// 3. 调用方式:用FutureTask包装Callable,获取结果
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // a. 创建Callable任务
        Callable<Double> calculateTask = new OrderAmountCalculateTask(1001L);
        // b. 用FutureTask包装(FutureTask实现了Runnable,可被Thread启动)
        FutureTask<Double> futureTask = new FutureTask<>(calculateTask);
        // c. 启动线程
        new Thread(futureTask).start();

        // d. 获取结果(get()会阻塞,直到任务完成;建议加超时,避免死等)
        // 方式1:直接阻塞获取
        Double orderTotal = futureTask.get();
        // 方式2:超时获取(推荐!避免线程永久阻塞)
        // Double orderTotal = futureTask.get(3, TimeUnit.SECONDS);

        System.out.println("主线程:获取到订单总额=" + orderTotal);
    }
}
优缺点分析
优点缺点
可返回执行结果get () 方法会阻塞主线程(需注意超时)
支持抛出异常(便于外层处理)代码较复杂(需 FutureTask 包装)
无法直接取消任务(需结合 Future 的 cancel ())
八年开发解析
  • 关键区别:Callable 和 Runnable 的核心差异是call()有返回值且可抛异常 —— 这解决了 “异步任务需要结果反馈” 的痛点,比如支付回调后的结果校验、数据计算后的汇总。
  • 避坑点:futureTask.get()会阻塞主线程!如果任务执行时间长(比如 3 秒以上),主线程会一直等,影响性能。生产环境必须用get(long timeout, TimeUnit unit)加超时,超时后抛TimeoutException,避免死等。

4. 方式 4:线程池(ExecutorService)(生产环境首选)

业务场景

高并发场景,比如 “电商大促时,每秒 1000 个下单请求,每个请求后需异步发通知、扣积分”—— 手动创建线程会导致线程数暴增(OOM 风险),线程池可复用线程、控制并发数。

核心代码
import java.util.concurrent.*;

public class ThreadPoolDemo {
    // 1. 手动创建线程池(推荐!避免用Executors,防止OOM)
    private static final ExecutorService threadPool = new ThreadPoolExecutor(
            5,                  // 核心线程数(常驻线程,即使空闲也不销毁)
            10,                 // 最大线程数(核心线程满+队列满后,最多再创建的线程数)
            60,                 // 空闲线程存活时间(非核心线程空闲超过这个时间会销毁)
            TimeUnit.SECONDS,    // 时间单位
            new LinkedBlockingQueue<>(100), // 任务队列(核心线程满后,任务先放队列)
            Executors.defaultThreadFactory(), // 线程工厂(创建线程的方式,可自定义命名)
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略(队列满+最大线程满后,如何处理新任务)
    );

    // 2. 业务任务:异步发送订单通知
    static class OrderNotifyTask implements Runnable {
        private Long orderId;

        public OrderNotifyTask(Long orderId) {
            this.orderId = orderId;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                // 模拟调用短信接口发送通知
                Thread.sleep(500);
                System.out.println("线程" + threadName + ":给订单" + orderId + "发送通知成功");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态,避免线程池忽略中断
                System.out.println("线程" + threadName + ":发送通知被中断,订单ID=" + orderId);
            }
        }
    }

    public static void main(String[] args) {
        // 模拟大促场景:1000个下单请求,每个请求触发异步通知
        for (int i = 1; i <= 1000; i++) {
            Long orderId = Long.valueOf(i);
            // 3. 提交任务到线程池(有两种方式:submit和execute)
            // 方式1:execute提交Runnable(无返回值)
            threadPool.execute(new OrderNotifyTask(orderId));

            // 方式2:submit提交Callable(有返回值,返回Future)
            // Future<Boolean> future = threadPool.submit(() -> {
            //     // 模拟发送通知并返回结果
            //     return true;
            // });
        }

        // 4. 关闭线程池(生产环境一般不主动关,随应用生命周期)
        // threadPool.shutdown(); // 平缓关闭:处理完已提交的任务,不再接受新任务
        // threadPool.shutdownNow(); // 强制关闭:立即中断所有任务,可能导致数据不一致
    }
}
优缺点分析
优点缺点
线程复用(减少创建 / 销毁开销)参数配置复杂(核心线程数、队列、拒绝策略需结合业务调优)
控制并发数(避免线程暴增 OOM)需手动关闭(否则应用退出时可能有任务残留)
支持任务排队、拒绝策略(容错性强)线程池耗尽时,新任务会排队或被拒绝(需合理配置)
八年开发解析
  • 为什么不用 Executors?Executors 提供的newFixedThreadPool()newCachedThreadPool()有坑:

    • newCachedThreadPool():最大线程数是Integer.MAX_VALUE,高并发下会创建无数线程,直接 OOM;
    • newFixedThreadPool():队列是LinkedBlockingQueue(无界),任务堆积会导致队列满,同样 OOM。
      生产环境必须手动创建 ThreadPoolExecutor,明确核心参数(尤其是队列和拒绝策略)。
  • 核心参数怎么设?参考公式:

    • CPU 密集型任务(比如计算):核心线程数 = CPU 核心数 + 1(充分利用 CPU,避免上下文切换);
    • IO 密集型任务(比如发通知、查数据库):核心线程数 = CPU 核心数 * 2(IO 等待时线程可空闲,多开线程提高利用率)。
  • 拒绝策略选哪个?

    • 核心业务(下单):用CallerRunsPolicy(让调用者线程执行任务,避免丢任务);
    • 非核心业务(日志):用DiscardOldestPolicy(丢弃最老的任务,保留新任务)或DiscardPolicy(直接丢弃新任务)。

5. 方式 5:Lambda 表达式(简化代码,Java 8+)

业务场景

简化 Runnable/Callable 的代码,比如 “简单的异步任务,不需要单独定义类”(比如打印日志、简单计算)。

核心代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class LambdaThreadDemo {
    public static void main(String[] args) {
        // 1. 简化Runnable(结合Thread)
        new Thread(() -> {
            System.out.println("Lambda线程1:打印简单日志");
        }).start();

        // 2. 简化Runnable(结合线程池)
        ExecutorService pool = Executors.newFixedThreadPool(3);
        pool.execute(() -> {
            System.out.println("Lambda线程2:处理简单任务");
        });

        // 3. 简化Callable(结合线程池)
        pool.submit(() -> {
            // 模拟计算并返回结果
            return "Lambda线程3:计算结果=100";
        });

        pool.shutdown();
    }
}
八年开发解析
  • 本质:Lambda 是函数式接口的简化写法 ——Runnable(只有一个 run () 方法)、Callable(只有一个 call () 方法)都是函数式接口,所以可以用 Lambda 简写,省去单独定义类的麻烦。
  • 生产建议:简单任务用 Lambda,复杂任务(比如 10 行以上代码)还是单独定义类 —— 避免 Lambda 代码过长,可读性差。

6. 方式 6:CompletableFuture(异步链式调用,Java 8+)

业务场景

复杂异步链路,比如 “下单流程:异步调用库存服务→异步调用支付服务→异步调用通知服务”,需要前一个任务的结果作为后一个任务的参数,且避免回调地狱。

核心代码
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CompletableFutureDemo {
    // 自定义线程池(CompletableFuture默认用ForkJoinPool,高并发下建议自定义)
    private static final ExecutorService customPool = Executors.newFixedThreadPool(5);

    public static void main(String[] args) {
        Long orderId = 1001L;
        // 1. 异步调用库存服务(supplyAsync有返回值,对应Callable)
        CompletableFuture<Boolean> stockFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("线程" + Thread.currentThread().getName() + ":检查订单" + orderId + "库存");
            // 模拟调用库存服务,返回是否有库存
            return true; // 有库存
        }, customPool)
        // 2. 库存检查成功后,异步调用支付服务(thenApply接收前一个结果,返回新结果)
        .thenApplyAsync(hasStock -> {
            if (hasStock) {
                System.out.println("线程" + Thread.currentThread().getName() + ":库存充足,发起支付");
                // 模拟调用支付服务,返回支付是否成功
                return true; // 支付成功
            } else {
                throw new RuntimeException("库存不足,支付失败");
            }
        }, customPool)
        // 3. 支付成功后,异步调用通知服务(thenAccept接收前一个结果,无返回值)
        .thenAcceptAsync(paySuccess -> {
            if (paySuccess) {
                System.out.println("线程" + Thread.currentThread().getName() + ":支付成功,发送订单通知");
                // 模拟调用通知服务
            }
        }, customPool)
        // 4. 异常处理(exceptionally捕获整个链路的异常,避免静默失败)
        .exceptionally(ex -> {
            System.err.println("异步链路异常:" + ex.getMessage());
            return null;
        });

        // 等待链路完成(生产环境一般不用,让异步链路自己跑)
        // stockFuture.join();
    }
}
优缺点分析
优点缺点
支持链式调用(避免回调地狱)学习成本高(API 多,比如 thenApply、thenAccept、whenComplete)
可自定义线程池(性能可控)异常处理复杂(需显式用 exceptionally 或 whenComplete)
支持并行任务(allOf、anyOf)调试难度大(异步链路栈信息不完整)
八年开发解析
  • 为什么用它?CompletableFuture 解决了 Future 的 “阻塞” 和 “回调地狱” 问题 —— 比如之前用 Future 做 3 个异步任务,需要依次 get () 阻塞;现在用CompletableFuture.allOf(future1, future2, future3)就能并行执行,效率更高。
  • 避坑点:CompletableFuture 默认用ForkJoinPool.commonPool(),这个线程池是 JVM 共享的,高并发下会和其他任务(比如 Stream 并行流)抢资源,导致性能下降。生产环境必须自定义线程池,传入supplyAsync/thenApplyAsync的第二个参数。

三、八年开发的避坑指南:这些错别再犯!

  1. 不要直接调用 run () 方法:启动线程必须用start()run()只是普通方法,不会创建新线程;
  2. 避免手动创建大量线程:比如循环new Thread(),高并发下会导致线程数暴增,触发OutOfMemoryError: unable to create new native thread
  3. 线程池不要用 ExecutorsnewCachedThreadPool()newFixedThreadPool()有 OOM 风险,手动创建ThreadPoolExecutor并指定队列和拒绝策略;
  4. Future.get () 必须加超时:不加超时会导致主线程永久阻塞,生产环境用get(3, TimeUnit.SECONDS)
  5. CompletableFuture 要处理异常:默认异常会静默丢失(控制台不打印),必须用exceptionallywhenComplete捕获;
  6. 线程安全要注意:多线程共享变量(比如静态变量)必须加锁(synchronized/Lock)或用线程安全类(ConcurrentHashMapAtomicInteger),避免数据错乱。

四、总结:按业务场景选对方式

最后给一张 “选型对照表”,八年开发亲测好用:

业务场景推荐方式不推荐方式
简单无返回值任务(打印日志)Runnable + 线程池继承 Thread
简单有返回值任务(计算结果)Callable + 线程池FutureTask+Thread
高并发任务(电商大促)线程池(ThreadPoolExecutor)手动 new Thread
复杂异步链路(多服务调用)CompletableFuture + 自定义线程池Future 链式 get ()
简化代码(Java 8+)Lambda + 线程池单独定义 Runnable 类

其实创建线程的核心不是 “会多少种方式”,而是 “能不能根据业务选对方式”—— 比如单线程能搞定的不用多线程,多线程能搞定的不用线程池,线程池能搞定的不用 CompletableFuture。适合业务的,才是最好的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天摸鱼的java工程师

谢谢老板,老板大气。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值