一、线程基础概念
1.1 什么是进程?
进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位 。简单来说,当我们在Java中启动main函数时,实际上就启动了一个JVM进程。
1.2 什么是线程?
线程是进程中的一个执行路径,线程本身不会独立存在。一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
核心区别 :操作系统分配资源时是把资源分配给进程的,但CPU资源比较特殊,它是分配给线程的。因为真正占用CPU运行的是线程,所以线程也被称为CPU分配的基本单位。
1.3 线程的私有与共享资源
| 资源类型 | 归属 | 说明 |
|---|---|---|
| 堆 | 线程共享 | 进程中最大的一块内存,存放new创建的对象实例 |
| 方法区 | 线程共享 | 存放JVM加载的类、常量及静态变量等信息 |
| 程序计数器 | 线程私有 | 记录线程当前要执行的指令地址 |
| 虚拟机栈 | 线程私有 | 存储线程的局部变量、调用栈帧 |
为什么程序计数器是线程私有的?
CPU采用时间片轮转方式让线程轮询占用。当线程的时间片用完后,需要让出CPU,程序计数器就是用来记录线程让出CPU时的执行地址,待下次分配到时间片时,线程可以从私有计数器指定的地址继续执行。
注意 :执行
native方法时,程序计数器记录的是undefined地址;只有执行Java代码时,程序计数器记录的才是下一条指令的地址。
1.4 线程的生命周期
线程从创建到销毁会经历以下状态:
-
新建(New):线程对象被创建
-
就绪(Runnable) :调用
start()后,已获取除CPU外的所有资源 -
运行(Running):获取CPU资源,正在执行
-
阻塞(Blocked):等待某些资源或锁
-
终止(Terminated) :
run()方法执行完毕
二、线程的三种创建方式
方式一:继承Thread类
java
public class ThreadTest {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("I am a child thread");
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程,注意不是调用run()
}
}
使用细节:
-
创建
Thread对象后,线程并未启动,直到调用start()方法 -
start()后线程处于就绪状态,等待CPU资源 -
run()方法执行完毕,线程进入终止状态
优点:
- 在
run()方法内获取当前线程直接使用this即可,无需Thread.currentThread()
缺点:
-
Java不支持多继承,继承了
Thread类就不能再继承其他类 -
任务与代码没有分离,多个线程执行相同任务时需要多份任务代码
方式二:实现Runnable接口
java
public class ThreadTest1 {
public static class RunableTask implements Runnable {
@Override
public void run() {
System.out.println("I am a child thread");
}
}
public static void main(String[] args) {
RunableTask task = new RunableTask();
new Thread(task).start();
new Thread(task).start(); // 多个线程共享同一个task
}
}
优点:
-
解决单继承的限制,
RunnableTask还可以继承其他类 -
任务与代码分离,多个线程可以共享同一个任务代码
-
可以通过给
RunnableTask添加参数进行任务区分
缺点:
- 任务没有返回值
方式三:实现Callable接口 + FutureTask(有返回值)
java
public class ThreadTest2 {
public static class CallerTask implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(5000);
return "hello";
}
}
public static void main(String[] args) {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
// 启动线程
new Thread(futureTask).start();
try {
// 等待任务执行完毕,并返回结果(阻塞)
String result = futureTask.get();
System.out.println(result);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}
特点:
-
实现
Callable接口的call()方法 -
通过
FutureTask包装任务,可获取返回值 -
futureTask.get()会阻塞等待任务执行完毕
三、三种方式对比总结
| 对比维度 | 继承Thread | 实现Runnable | 实现Callable |
|---|---|---|---|
| 是否支持多继承 | ❌ | ✅ | ✅ |
| 任务与代码分离 | ❌ | ✅ | ✅ |
| 是否有返回值 | ❌ | ❌ | ✅ |
| 获取当前线程 | 直接使用this |
Thread.currentThread() |
Thread.currentThread() |
| 适用场景 | 简单任务,不需要返回值 | 需要共享任务代码 | 需要获取执行结果 |
四、最佳实践建议
-
优先使用实现接口的方式 :
Runnable和Callable接口方式更加灵活,避免了单继承的限制 -
需要返回值时选择Callable :如果线程执行后需要返回结果,使用
FutureTask+Callable -
多个线程共享任务代码 :使用
Runnable方式,多个线程可以共用一个任务实例 -
线程启动不要直接调用
run():调用run()只是普通方法调用,不会启动新线程;必须调用start()才能启动新线程
