字数 1914,阅读大约需 10 分钟
概述
在介绍多线程之前,我们先讲清楚程序、进程和线程这三个概念:
- 程序(program):正在运行的一段代码,是静态的;
- 进程(process):程序的一次运行实例,是操作系统分配资源的基本单位。我们启动一个 Java 程序(也就是执行
main 方法)时,操作系统就会分配一个独立的内存空间给它,这个进程内部跑的就是 JVM;
- 线程(thread):进程内部的执行流,是 CPU 调度和分配的基本单位。
进程和线程的关系:一个进程可以包含多个线程。同一个进程里的所有线程共享进程的堆和方法区,但每个线程都有自己的程序计数器、虚拟机栈和本地方法栈——这也是为什么线程切换的开销远小于进程,线程因此又被称为轻量级进程。多线程的好处,一是让程序能同时处理多项事件,二是充分利用空闲CPU资源。
例如最典型的 Web 服务场景:一个线程处理网络请求时,往往会穿插数据库查询等 IO 操作。IO 等待期间 CPU 其实闲着——单线程模型下,这个线程只能挂起干等 IO 返回,CPU很空闲;多线程模型下,CPU 把执行权让给其他就绪线程继续干活,等到 IO 返回了再切回来。这就把"等待"变成了"干活",并发吞吐自然上去了。
创建和启动线程
Java 创建线程一共有4 种方式,按演进顺序演示,但实际开发中最常用的是最后那种:
- 继承
Thread 类:最原始的方式,把任务逻辑写在 run() 里,再 new 出来、start()。缺点是 Java 单继承,一个类继承了 Thread 就不能再继承别的类了。下面的例子能观察到 tA 线程整个生命周期的状态。
...
public static void main(String[] args) {
System.out.println("main开始运行");
TaskThread tA = new TaskThread("任务A");
TaskThread tB = new TaskThread("任务B");
System.out.println("任务A状态:" + tA.getState());
tA.start();
tB.start();
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(300);
System.out.println("任务A状态" + tA.getState());
System.out.println("任务B状态" + tB.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
tA.join();
tB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main所有子线程执行完毕,tA状态:" + tA.getState());
// 输出TERMINATED
System.out.println("main程序退出");
}
...
class TaskThread extends Thread {
public TaskThread(String name) {
super(name);
}
@Override
public void run() {
Thread current = Thread.currentThread();
System.out.println(current.getName() + "开始运行,状态:" + current.getState());
for (int i = 1; i <= 5; i++) {
System.out.println(current.getName() + "进度:" + (i * 20) + "%");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(current.getName() + "执行完毕");
}
}
/*
任务A状态:NEW
任务B开始运行,状态:RUNNABLE
任务A开始运行,状态:RUNNABLE
任务A进度:20%
任务B进度:20%
任务A状态TIMED_WAITING
任务B状态TIMED_WAITING
任务B进度:40%
任务A进度:40%
任务A状态TIMED_WAITING
任务B状态TIMED_WAITING
任务A状态TIMED_WAITING
任务B状态TIMED_WAITING
任务A进度:60%
任务B进度:60%
任务A进度:80%
任务B进度:80%
任务B进度:100%
任务A进度:100%
任务B执行完毕
任务A执行完毕
main所有子线程执行完毕,tA状态:TERMINATED
main程序退出
*/
- 实现
Runnable 接口:把任务逻辑放进 run(),再交给 new Thread(task) 包装一下。因为接口不受"单继承"限制、逻辑和线程分离可以复用。
...
public static void main(String[] args) {
System.out.println("main开始运行");
var taskA = new Task("TaskA");
var taskB = new Task("TaskB");
var tA = new Thread(taskA, "ThreadA");
var tB = new Thread(taskB, "ThreadB");
System.out.println("tA状态:" + tA.getState());
tA.start();
tB.start();
for (int i = 0; i < 4; i++) {
try {
Thread.sleep(300);
System.out.println("threadA状态:" + tA.getState());
System.out.println("threadB状态:" + tB.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
tA.join();
tB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("全部完成,tA:状态:" + tA.getState());
}
...
class Task implements Runnable {
private final String taskName;
public Task(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("开始执行:" + currentThread.getName() + "状态" + currentThread.getState());
for (int i = 1; i <= 5; i++) {
System.out.println(currentThread.getName() + "任务" + taskName + " 进度:" + (i * 20) + "%");
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(currentThread.getName() + "执行完毕");
}
}
/*
main开始运行
tA状态:NEW
开始执行:ThreadB状态RUNNABLE
开始执行:ThreadA状态RUNNABLE
ThreadA任务TaskA 进度:20%
ThreadB任务TaskB 进度:20%
tA状态:TIMED_WAITING
tB状态:TIMED_WAITING
ThreadA任务TaskA 进度:40%
ThreadB任务TaskB 进度:40%
tA状态:TIMED_WAITING
tB状态:TIMED_WAITING
ThreadA任务TaskA 进度:60%
ThreadB任务TaskB 进度:60%
tA状态:TIMED_WAITING
tB状态:TIMED_WAITING
ThreadB任务TaskB 进度:80%
ThreadA任务TaskA 进度:80%
tA状态:RUNNABLE
tB状态:TIMED_WAITING
ThreadB任务TaskB 进度:100%
ThreadA任务TaskA 进度:100%
ThreadB执行完毕
ThreadA执行完毕
全部完成,tA:状态:TERMINATED
*/
- 实现
Callable 和 Future:前两种的 run() 没有返回值、也不能抛受检异常,Callable 正好补上了这两个短板——call() 有返回值且可以抛异常,配合 Future 就能拿到执行结果,也是带返回结果的方式。用的时候要把 Callable 包装成 FutureTask 再交给线程,最后调 futureTask.get() 阻塞取结果。
...
public static void main(String[] args) {
System.out.println("main开始运行");
SumTask task = new SumTask(1, 100);
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread worker = new Thread(futureTask, "计算器线程");
worker.start();
...
class SumTask implements Callable<Integer> {
private final int start;
private final int end;
public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Integer call() throws Exception {
Thread current = Thread.currentThread();
System.out.println(current.getName());
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
Thread.sleep(10);
}
System.out.println(current.getName() + "完成,结果为:" + sum);
return sum;
}
}
- 线程池(推荐):生产环境中主流的线程创建方式。前面三种都是"一个任务 new 一个线程",任务一多线程就会无限膨胀;而线程池通过复用已有线程来控制并发数量,顺带还提供了任务队列、拒绝策略、超时控制这些生产需要的功能。实际项目中直接用 JDK 自带的
Executors 工具类创建即可。
public static void main(String[] args) {
// 创建自定义线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, //corePoolSize:核心线程数
4, // maximumPoolSize:最大线程数
60L, // keepAliveTime:空闲存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(10), // 有界队列,容量10
Executors.defaultThreadFactory(),// 线程工厂(可用默认)
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略(让调用者执行)
);
for (int i = 1; i <= 12; i++) {
int taskId = i;
pool.execute(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + "正在执行任务" + taskId);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(name + "完成" + taskId);
});
}
//关闭线程池
pool.shutdown();
System.out.println("main结束");
}
/*
pool-1-thread-1正在执行任务1
pool-1-thread-2正在执行任务2
pool-1-thread-1完成1
pool-1-thread-2完成2
pool-1-thread-1正在执行任务3
pool-1-thread-2正在执行任务4
pool-1-thread-2完成4
pool-1-thread-1完成3
pool-1-thread-1正在执行任务6
pool-1-thread-2正在执行任务5
pool-1-thread-2完成5
pool-1-thread-2正在执行任务7
pool-1-thread-1完成6
pool-1-thread-1正在执行任务8
pool-1-thread-2完成7
pool-1-thread-1完成8
pool-1-thread-2正在执行任务9
pool-1-thread-1正在执行任务10
pool-1-thread-1完成10
pool-1-thread-1正在执行任务11
pool-1-thread-2完成9
pool-1-thread-2正在执行任务12
pool-1-thread-1完成11
pool-1-thread-2完成12
*/
最后四种方式对比如下:
| 方式 |
有返回值 |
可抛受检异常 |
复用性 |
评价 |
继承 Thread |
❌ |
❌ |
❌ |
受单继承限制,不推荐 |
实现 Runnable |
❌ |
❌ |
✅ 任务可复用 |
简单场景够用 |
实现 Callable+Future |
✅ |
✅ |
✅ |
能拿结果,注意 get() 会阻塞 |
| 线程池 |
✅ |
✅ |
✅ |
生产环境使用 |