在 Java 创建多线程,最常见有三种方式:继承 Thread 类、实现 Runnable 接口、用 ExecutorService 线程池提交任务。本文一次讲清写法、取舍与共享状态的坑。很多刚接触并发的同学以为“多线程就是 new 一个 Thread 然后 start”,结果在高并发接口里疯狂建线程,把系统资源耗光。其实 Java 从语言层面到 JDK 工具类,已经把多种创建方式都准备好了,区别主要在耦合度、资源开销和可维护性上。我们先建立“线程是执行任务的载体”这个认知,再分别看三种写法,最后讨论共享变量带来的线程安全问题与线程池的选型。把基础打牢,后面学锁、学并发容器才不会发懵。

一、继承 Thread 类的最直白写法
线程的本质是“一段可并发执行的任务”,而 Thread 类正是 Java 对操作系统线程的封装。最直接的方式是新建一个类继承 Thread,重写 run 方法写入要执行的代码,再调用 start 启动。在 Java 速查手册 里可以查到 Thread 的核心方法,比如 start、sleep、join 的语义。下面的例子定义了一个打印数字的线程并启动它:
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(getName() + ":" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
}
这种写法胜在直观,但缺点也明显:Java 是单继承,继承了 Thread 就不能再继承别的类,任务与线程强耦合,复用性差,工程里较少这样写。
要强调一点:启动线程必须调用 start 而不是 run。直接调用 run 只是在当前线程里普通执行方法,并没有新建线程,也就失去了并发意义。start 才会真正向系统申请线程资源并执行 run,这一点在面试和排错里都经常被问到。
二、实现 Runnable 接口解耦任务
更推荐的写法是让任务类实现 Runnable 接口,把“做什么”和“谁来做”分开。Runnable 只有一个 run 方法,你只需把业务逻辑放进去,再把实例传给 Thread 构造器启动。这样任务类还能去继承其他类,也方便同一个 Runnable 被多个线程共享执行。下面用 Runnable 重写上面的例子:
class MyTask implements Runnable {
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
Runnable task = new MyTask();
new Thread(task).start();
new Thread(task).start();
}
}
用 Lambda 还能更简洁:new Thread(() -> System.out.println("hi")).start()。Runnable 是后续线程池的基石,理解它对学 ExecutorService 很有帮助。
除 Runnable 外,JDK 还有 Callable 接口,它的 call 方法能返回结果并抛出异常,常配合 Future 获取异步返回值。Runnable 适合不关心结果的纯任务,Callable 适合需要回传数据的场景,两者都可作为线程池的提交单元,是并发工具箱里的左右手。
三、用 ExecutorService 线程池管理
当任务很多时,频繁 new Thread 会带来巨大的创建与销毁开销,还可能耗尽系统资源。JDK 提供的线程池 ExecutorService 能复用固定数量的线程,统一调度任务。通过 Executors.newFixedThreadPool 可快速建池,用 submit 或 execute 提交 Runnable。示例如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
pool.execute(() -> System.out.println(Thread.currentThread().getName()));
}
pool.shutdown();
}
}
线程池把“创建几个线程、任务怎么排队”交给了框架,既可控又高效。生产环境建议用 ThreadPoolExecutor 显式指定核心参数,而不是无脑用工具类,避免队列无限增长。
Executors 还提供 newCachedThreadPool、newSingleThreadExecutor 等快捷工厂,但阿里规范更推荐直接用 ThreadPoolExecutor 构造器,显式声明核心线程数、队列容量和拒绝策略,防止资源被无限任务拖垮。任务结束后记得调用 shutdown 释放线程,避免进程迟迟不退出。
四、共享变量的线程安全问题
多线程一旦共享同一个变量,就会遇到可见性与原子性问题。比如多个线程同时对一个计数器做 i++,由于读取、计算、写回不是原子操作,最终结果往往小于预期。解决手段包括 synchronized 加锁、使用 AtomicInteger、或用并发容器。下面演示一个不安全的计数器:
class Counter {
int count = 0;
void add() { for (int i = 0; i < 1000; i++) count++; }
}
// 两个线程各调用一次 add,count 大概率小于 2000
把 count 改成 AtomicInteger 并在循环里调用 incrementAndGet,就能得到稳定的 2000。理解这一点,是写出正确并发程序的前提,也是面试高频考点。
synchronized 能保证同一时刻只有一个线程进入临界区,既解决原子性也保证可见性;volatile 只保证可见性不保证原子性,适合状态标志位。选择时先看操作是不是复合动作,复合动作请用锁或原子类,单读单写的状态才考虑 volatile,否则依旧会出现竞态。
五、常见坑与选型建议
实际项目里最容易踩两类坑:一是无节制创建线程,导致上下文切换开销爆炸,应当优先用线程池;二是忽视共享状态,以为“看着像串行”就安全,结果出现偶发数据错乱。面对共享变量,要么避免共享,要么用锁或原子类保护。在 阿里巴巴 Java 开发手册 中,对线程池参数和并发规约有明确约定,值得对照自查。选型上:简单演示用继承 Thread 即可,常规任务用 Runnable,生产高并发务必上线程池,并给线程起有意义名字方便排查。
推荐用自定义 ThreadFactory 给线程命名,例如 order-pool-1,出问题时一眼能从堆栈定位归属。同时给线程池设置合理的拒绝策略,比如丢弃并告警,而不是默认可抛异常导致主流程中断。把这些细节固定成模板,团队写并发代码时会省下大量排查时间。很多编程狮读者在学完这三种方式后,会明显感觉并发代码写得更有底气。
下面给出一个显式构造的线程池示例,包含自定义线程命名与拒绝策略,是生产环境的推荐写法:
import java.util.concurrent.*;
public class Demo {
public static void main(String[] args) {
ThreadFactory tf = r -> {
Thread t = new Thread(r);
t.setName("order-pool-" + t.getId());
return t;
};
ExecutorService pool = new ThreadPoolExecutor(
4, 8, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), tf,
new ThreadPoolExecutor.CallerRunsPolicy());
pool.execute(() -> System.out.println(Thread.currentThread().getName()));
pool.shutdown();
}
}

落到实操,配置线程池时务必显式声明核心参数与拒绝策略,运行上面的示例验证任务是否按预期并发;如果某次提交失败或结果小于预期,先检查共享变量是否未加锁,再用 synchronized 或 Atomic 类修复竞态。把三种创建方式在性能、适用场景与线程安全边界上做一轮对比,你会发现继承 Thread 适合演示、Runnable 适合解耦、线程池适合高并发,选型也就不再靠猜。示例代码按官方文档整理,可直接复用。
总结
Java 创建多线程的三种方式各有定位:继承 Thread 直观但耦合高,实现 Runnable 解耦且是线程池基础,ExecutorService 线程池解决资源与调度难题。写并发代码时,请始终把共享状态当作风险点,优先无锁或原子类,必要时加锁。把线程池参数配好、给线程命名,是工程里从“能跑”到“跑得稳”的关键一步。
当你能根据任务性质在三种方式间从容切换,并发编程的第一道门就真正跨过去了。接下来再去学锁、并发容器与并行流,都会水到渠成,因为创建与管理线程的基础已经牢牢夯实。
延伸学习
- Java 入门课程 系统学 Java
- Java 多线程创建笔记 看三种创建方式详解
- Java 教程 复习基础
常见问题
Q:继承 Thread 和实现 Runnable 该选哪个?
A:优先实现 Runnable。Java 单继承,继承 Thread 后无法再继承其他类,且任务与线程强耦合。Runnable 把任务与执行分离,复用性更好,也是线程池的入参要求。
Q:为什么推荐用线程池而不是直接 new Thread?
A:频繁创建销毁线程开销大,还可能耗尽系统资源。线程池复用固定线程、统一管理队列与拒绝策略,既降低开销又便于控量,是高并发下的标准做法。无界队列在任务暴涨时会堆积内存,生产环境务必配上界队列与拒绝策略,避免 OOM。
Q:多线程操作同一个变量一定会出错吗?
A:只要存在“读改写”非原子操作且缺乏同步,就可能因可见性或竞态导致结果错误。可用 synchronized、volatile 或 Atomic 类保护;若各线程只用局部变量,则通常安全。

免费 AI IDE



