一、继承 Thread 类

最直接的方式是继承 Thread 类,并重写 run() 方法。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行中:" + Thread.currentThread().getName());
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

需要注意,启动线程要调用 start() 方法,而不是直接调用 run() 方法。

thread.start();

start() 会真正创建新线程,然后由 JVM 调用 run()。

如果直接调用:

thread.run();

那只是普通方法调用,不会创建新线程。

优点

继承 Thread 的优点是写法简单,适合学习和演示。

缺点

缺点也很明显:

  • Java 是单继承,继承了 Thread 后就不能再继承其他类
  • 任务逻辑和线程对象绑定,不利于解耦
  • 没有返回值
  • 不方便统一管理大量线程

所以实际开发中一般不推荐直接继承 Thread。

二、实现 Runnable 接口

更常见的方式是实现 Runnable 接口。

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("任务执行中:" + Thread.currentThread().getName());
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyTask());
        thread.start();
    }
}

也可以使用 Lambda 表达式:

Thread thread = new Thread(() -> {
    System.out.println("任务执行中:" + Thread.currentThread().getName());
});

thread.start();

Runnable 的特点是把“任务”和“线程”分开了。

Runnable:描述要执行的任务
Thread:负责创建和启动线程

优点

  • 避免 Java 单继承限制
  • 任务和线程解耦
  • 多个线程可以共享同一个任务对象
  • 写法比继承 Thread 更灵活

缺点

  • 没有返回值
  • 不能直接抛出受检异常
  • 仍然需要自己创建和管理线程

如果任务不需要返回结果,Runnable 是比继承 Thread 更推荐的方式。

三、实现 Callable 接口

如果任务执行后需要返回结果,可以使用 Callable。

Callable 和 Runnable 很像,但它有两个区别:

  • Callable 有返回值
  • Callable 可以抛出异常

示例:

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        Callable<Integer> task = () -> {
            System.out.println("任务执行中:" + Thread.currentThread().getName());
            return 100;
        };

        FutureTask<Integer> futureTask = new FutureTask<>(task);

        Thread thread = new Thread(futureTask);
        thread.start();

        Integer result = futureTask.get();
        System.out.println("执行结果:" + result);
    }
}

这里的 FutureTask 用来包装 Callable,并通过 get() 方法获取执行结果

Integer result = futureTask.get();

需要注意:

get() 方法会阻塞,直到任务执行完成并返回结果。

优点

  • 可以获取任务执行结果
  • 可以抛出异常
  • 适合需要返回值的异步任务

缺点

  • 写法比 Runnable 复杂
  • get() 会阻塞
  • 单独使用时仍然不方便管理大量线程

四、使用线程池

实际开发中,最常用的方式是使用线程池。

如果每来一个任务就创建一个线程,会有很多问题:

  • 线程创建和销毁成本高
  • 线程数量不可控
  • 高并发时可能创建大量线程,导致系统资源耗尽
  • 不方便统一管理线程生命周期

线程池的作用就是复用线程,并控制并发数量。

简单示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        executor.submit(() -> {
            System.out.println("任务执行中:" + Thread.currentThread().getName());
        });

        executor.shutdown();
    }
}

不过在实际项目中,不太建议直接使用 Executors 创建线程池,因为它的一些默认配置可能带来风险。

比如:

Executors.newFixedThreadPool(10);

底层使用的是无界队列,任务堆积过多时可能导致 OOM。

更推荐使用 ThreadPoolExecutor 显式指定参数:

import java.util.concurrent.*;

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                10,
                20,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        executor.submit(() -> {
            System.out.println("任务执行中:" + Thread.currentThread().getName());
        });

        executor.shutdown();
    }
}

线程池的核心参数包括:

corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime:非核心线程空闲存活时间
workQueue:任务队列
threadFactory:线程工厂
handler:拒绝策略

优点

  • 线程复用,减少创建和销毁成本
  • 可以控制并发线程数
  • 支持任务队列
  • 支持统一管理线程生命周期
  • 可以配合 Future 获取任务结果

缺点

  • 参数配置不合理可能导致任务堆积或 OOM
  • 使用不当可能造成线程泄漏
  • 需要手动关闭线程池
  • 需要理解线程池运行机制和拒绝策略

实际项目中,传统并发任务一般优先使用线程池。

五、Java 21 虚拟线程

Java 21 正式引入了虚拟线程,这是 Project Loom 的核心能力。

虚拟线程是一种由 JVM 管理的轻量级线程。它不像传统 Java 线程那样一对一绑定操作系统线程,而是可以让大量虚拟线程运行在少量平台线程之上。

可以简单理解为:

传统线程:Java Thread -> OS Thread
虚拟线程:大量 Virtual Thread -> 少量 Platform Thread -> OS Thread

创建虚拟线程非常简单:

public class VirtualThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = Thread.startVirtualThread(() -> {
            System.out.println("虚拟线程执行中:" + Thread.currentThread());
        });

        thread.join();
    }
}

也可以使用虚拟线程 Executor:

import java.util.concurrent.Executors;

public class VirtualThreadExecutorDemo {
    public static void main(String[] args) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> {
                System.out.println("任务执行中:" + Thread.currentThread());
            });
        }
    }
}

这个 Executor 的特点是:

每个任务创建一个虚拟线程

由于虚拟线程创建成本很低,一般不需要像平台线程那样池化复用。

虚拟线程适合什么场景

虚拟线程非常适合 IO 密集型任务,比如:

  • Web 请求处理
  • 数据库访问
  • Redis 调用
  • HTTP/RPC 调用
  • 文件 IO
  • 等待外部服务返回

这类任务大部分时间都在等待 IO。如果使用传统线程,线程会一直阻塞并占用操作系统线程资源。

而虚拟线程在阻塞时,JVM 可以把它挂起,让底层平台线程去执行其他任务。

虚拟线程不适合什么场景

虚拟线程不适合用来提升 CPU 密集型任务性能,比如:

  • 大量数学计算
  • 加密解密
  • 图片处理
  • 视频编码
  • 压缩解压
  • 大规模排序

因为 CPU 核心数量是固定的,虚拟线程再多也不能让 CPU 算得更快。

优点

  • 创建成本低
  • 可以创建大量并发任务
  • 适合 IO 密集型场景
  • 保持同步代码写法,代码更简单
  • 避免复杂的异步回调和链式编排

缺点

  • 不适合 CPU 密集型任务
  • 不能绕过数据库连接池、HTTP 连接池等资源限制
  • 大量使用 ThreadLocal 可能带来内存压力
  • 需要 Java 21 及以上
  • 某些阻塞场景可能导致 carrier thread pinning

比如数据库连接池只有 100 个连接,即使创建 10 万个虚拟线程,请求数据库时仍然最多只有 100 个连接能并发执行。

所以虚拟线程提升的是线程模型的并发承载能力,不是无限放大下游资源能力。

六、几种方式对比

方式 是否有返回值 是否推荐项目使用 适合场景
继承 Thread 不太推荐 简单演示
实现 Runnable 一般 无返回值任务
实现 Callable 一般 需要返回结果的任务
线程池 可有 推荐 传统并发任务
虚拟线程 可有 视 Java 版本和场景 高并发 IO 密集型任务

七、面试总结

面试时可以这样回答:

Java 创建线程常见方式有:继承 Thread、实现 Runnable、实现 Callable 配合 FutureTask、使用线程池,以及 Java 21 的虚拟线程。继承 Thread 写法简单,但受单继承限制,任务和线程耦合;Runnable 实现了任务和线程分离,但没有返回值;Callable 可以返回结果并抛出异常,但获取结果时可能阻塞;线程池可以复用线程、控制并发,是实际开发中最常用的方式;虚拟线程是 Java 21 引入的轻量级线程,适合高并发 IO 密集型场景,但不适合 CPU 密集型任务,也不能突破数据库连接池等下游资源限制。实际项目中,普通并发任务优先使用线程池,IO 密集型高并发场景可以考虑虚拟线程。

更多推荐