多线程基础
进程和线程
进程是程序执行的实体,每一个进程都是一个应用程序,都有自己的内存空间,CPU一个核心同时只能处理一件事情,当出现多个进程需要同时运行时,CPU一般通过时间片轮转调度算法,来实现多个进程的同时运行。
线程像进程里的执行路线
- 同一个进程里可以有多个线程
- 这些线程共享这个进程的内存和大部分资源
- 用来并发执行任务
| 维度 | 进程 (Process) | 线程 (Thread) |
|---|---|---|
| 本质 | 资源分配的基本单位 | CPU 调度(执行)的基本单位 |
| 资源拥有 | 拥有独立的内存地址空间(Code, Data, Heap, Stack 等) | 共享所属进程的资源,仅拥有少量的私有资源(如 PC, 栈, 寄存器) |
| 创建/切换开销 | 大。需要分配内存空间、初始化数据段等 | 小。直接共享进程资源,上下文切换比进程快得多 |
| 通信方式 | 复杂。需通过 IPC(管道、信号量、Socket、共享内存等) | 简单。通过读写同一进程内的全局变量或堆内存即可通信 |
| 健壮性 | 高。一个进程崩溃通常不会影响其他进程 | 低。一个线程崩溃可能导致整个进程内的所有线程一起挂掉 |
线程的创建和启动
通过创建Thread对象来创建一个新的线程,Thread构造方法中需要传入一个Runnable接口的实现(其实就是编写要在另一个线程执行的逻辑)同时Runnable只有一个未实现方法,因此可以直接使用lambda表达式:
1 | @FunctionalInterface |
创建好后,通过调用start()方法来运行此线程:
1 | public static void main(String[] args) { |
实际上,线程和进程差不多,也会等待获取CPU资源,一旦获取到,就开始按顺序执行我们给定的程序,当需要等待外部IO操作(比如Scanner获取输入的文本),就会暂时处于休眠状态,等待通知,或是调用sleep()方法来让当前线程休眠一段时间

线程的状态
线程的几种基本状态
1. 新建状态
线程对象已经创建出来了,但还没有启动。
比如:
1 | Thread t = new Thread(() -> { |
这时候线程 t 已经有了,但还没调用 start(),所以它处于新建状态。
2. 就绪状态
调用了 start() 之后,线程就进入就绪状态。
特点是:
- 已经具备运行条件
- 但是还没有真正占用 CPU
- 需要等待 CPU 调度
比如:
1 | t.start(); |
调用后,线程不会立刻执行,而是先进入就绪状态。
3. 运行状态
线程获得 CPU 时间片,开始真正执行代码时,就是运行状态。
也就是说:
- 就绪状态是“等着运行”
- 运行状态是“正在运行”
比如线程开始执行 run() 方法里的内容时,它就在运行。
4. 阻塞状态
线程因为某些原因暂时不能继续运行,就会进入阻塞状态。
常见原因有:
- 等待 I/O
- 等待锁
- 调用
sleep() - 调用
wait() - 等待其他线程执行完
阻塞时线程不会占用 CPU。
等阻塞原因消失后,线程会重新回到就绪状态,等待再次调度。
5. 终止状态
线程执行完毕,或者因为异常而结束,就是终止状态。
比如 run() 方法执行完了,线程的生命周期也就结束了。
线程一旦终止,就不能再次启动。
状态变化过程
线程状态变化通常是这样:
新建 → 就绪 → 运行 → 阻塞 → 就绪 → 运行 → 终止
注意:
- 就绪和运行之间可能来回切换
- 运行中如果遇到阻塞,会进入阻塞
- 阻塞解除后不会直接运行,而是先回到就绪
Java 里更细的 6 种状态
Java 线程状态,Thread.State 里分得更细,一共有 6 种:
1. NEW
新建状态
2. RUNNABLE
可运行状态
注意:Java 里的 RUNNABLE 把就绪和运行两种情况合并到一起了。
也就是说:
- 可能正在等 CPU
- 也可能正在执行
在 Java 里统称 RUNNABLE
3. BLOCKED
阻塞状态
通常指等待获取对象锁。
4. WAITING
无限期等待状态
比如:
Object.wait()Thread.join()LockSupport.park()
需要别人唤醒,才能继续。
5. TIMED_WAITING
限时等待状态
比如:
Thread.sleep(1000)wait(1000)join(1000)
特点是:会等待一段时间,时间到了自动恢复。
6. TERMINATED
终止状态
线程执行结束。
线程的休眠和中断
线程休眠
在线程里,休眠通常用:
1 | Thread.sleep(1000); |
意思是:
让当前线程暂停 1000 毫秒。
休眠的特点
1. 是当前线程休眠
谁执行 sleep(),谁就休眠。
2. 进入限时等待状态
在 Java 里,调用 sleep() 后线程进入 TIMED_WAITING 状态。
3. 不会释放锁
如果线程在同步代码块里调用 sleep(),它休眠时仍然持有锁,别的线程拿不到这个锁。
线程中断
线程中断用的是:
1 | t.interrupt(); |
它的意思不是“立刻强制杀死线程”,而是:
给线程发一个中断信号。
也就是说,中断本质上是一种通知机制。
中断的特点
1. 不会直接把线程干掉
Java 里的中断不是强制停止线程。
2. 是协作式的
线程自己要去检查有没有被中断,然后决定是否结束。
休眠和中断的关系
一个线程如果正在 sleep(),这时别的线程调用它的 interrupt(),会发生什么?
答案是:
- 这个休眠线程会被提前唤醒
- 同时抛出
InterruptedException
isInterrupted 和 interrupted
isInterrupted()
1 | t.isInterrupted() |
- 判断某个线程是否被中断
- 不会清除中断标志
Thread.interrupted()
1 | Thread.interrupted() |
- 判断当前线程是否被中断
- 会清除中断标志
线程的优先级
实际上,Java程序中的每个线程并不是平均分配CPU时间的,为了使得线程资源分配更加合理,Java采用的是抢占式调度方式,优先级越高的线程,优先使用CPU资源。我们希望CPU花费更多的时间去处理更重要的任务,而不太重要的任务,则可以先让出一部分资源。线程的优先级一般分为以下三种:
- MIN_PRIORITY 最低优先级
- MAX_PRIORITY 最高优先级
- NOM_PRIORITY 常规优先级
java复制代码
1 | public static void main(String[] args) { |
优先级越高的线程,获得CPU资源的概率会越大,并不是说一定优先级越高的线程越先执行!
线程的礼让和加入
线程礼让 yield()
线程礼让指的是:
当前线程主动让出 CPU,给别的线程一个先执行的机会。
写法:
1 | Thread.yield(); |
作用
表示当前线程说:
“我先不抢了,别人可以先来。”
特点
- 只是让一下
- 不一定真的成功
- 礼让后线程会回到就绪状态
- CPU 可能还是继续调度它自己
线程加入 join()
线程加入指的是:
一个线程等待另一个线程执行完毕。
写法:
1 | t.join(); |
作用
比如主线程里调用了 t.join(),意思就是:
主线程要先等线程 t 执行完,自己再继续往下走。
例子
1 | Thread t = new Thread(() -> { |
意思就是:
- 启动子线程
- 主线程等待
- 子线程执行完
- 主线程再继续
wait和notify
wait() 是什么
wait() 的作用是:
让当前线程进入等待状态,并且释放它持有的对象锁。
比如:
1 | synchronized (lock) { |
执行后会发生:
- 当前线程进入等待状态
- 释放
lock这把锁 - 直到别的线程来唤醒它
重点
wait() 和 sleep() 很容易混:
sleep():休眠时不释放锁wait():等待时会释放锁
notify() 是什么
notify() 的作用是:
唤醒在这个对象上等待的一个线程。
比如:
1 | synchronized (lock) { |
意思是:
- 在
lock对象上等待的线程里 - 随机唤醒一个
但要注意:
被唤醒的线程不是立刻执行,它要先重新抢到锁,才能继续往下运行。
notifyAll() 是什么
除了 notify(),还有一个:
1 | lock.notifyAll(); |
作用是:
唤醒所有在这个对象上等待的线程。
然后这些线程再去竞争锁,谁抢到谁先执行。
示例
1 | class Demo { |
大致过程:
t1进入同步代码块- 调用
wait(),进入等待,同时释放锁 t2拿到锁,调用notify()t1被唤醒t1重新拿到锁后继续执行
多线程情况下Java的内存管理

ThreadLocal
ThreadLocal 可以简单理解成:
给每个线程都准备一份“自己专属的变量副本”。
也就是说,同一个 ThreadLocal 对象,不同线程去访问时,拿到的值是各自独立的,互不影响。
常用方法
set()
给当前线程设置值
1 | ThreadLocal<String> tl = new ThreadLocal<>(); |
get()
获取当前线程自己的值
1 | String s = tl.get(); |
remove()
删除当前线程保存的值
1 | tl.remove(); |
这个方法很重要,尤其在线程池里常常要手动清理。
示例
1 | public class Demo { |
1 | Thread-0:线程A的数据 |
ThreadLocal 和普通共享变量的区别
普通变量
- 多个线程访问的是同一份数据
- 可能有线程安全问题
- 往往需要加锁
ThreadLocal
- 每个线程有自己独立副本
- 不共享
- 一般不需要加锁
定时器
定时器的作用就是:
让程序按照时间规则自动执行任务。
基本用法
TimerTask
表示你要执行的任务。
1 | TimerTask task = new TimerTask() { |
Timer
负责安排任务什么时候执行。
1 | Timer timer = new Timer(); |
意思是:
3 秒后执行一次这个任务。
常见调度方式
延迟执行一次
1 | timer.schedule(task, 3000); |
3 秒后执行一次。
延迟后周期执行
1 | timer.schedule(task, 3000, 2000); |
意思是:
- 先延迟 3 秒
- 然后每隔 2 秒执行一次
守护线程
守护线程(Daemon Thread)是:
在后台运行、为用户线程提供服务的线程。
比如:
- 垃圾回收线程(GC)就是典型守护线程
特点
守护线程最关键的特点是:
当所有用户线程都结束后,守护线程会自动结束。
也就是说,守护线程不会阻止 JVM 退出。
用户线程和守护线程区别
用户线程
是程序真正干活的线程。
比如:
- main 线程
- 你自己创建的业务线程
只要还有用户线程没结束,JVM 就不会退出。
守护线程
主要做辅助工作。
当所有用户线程都结束时,即使守护线程还在跑,JVM 也会直接结束它。
语法
1 | Thread t = new Thread(() -> { |
线程构建器(Java21)
Java 21 在 Thread 里提供了 Thread.Builder API,用来创建:
- 平台线程
Thread.ofPlatform() - 虚拟线程
Thread.ofVirtual()
以前常见写法是:
1 | Thread t = new Thread(task); |
现在可以改成链式写法:
1 | Thread.ofPlatform() |
示例
创建并启动平台线程
1 | Thread.ofPlatform() |
意思就是:
先配置线程属性,再直接启动这个平台线程。相关配置能力由 Thread.Builder.OfPlatform 提供。
例 2:创建但不启动
1 | Thread t = Thread.ofPlatform() |
这里 unstarted() 只是创建线程,不会立即运行。
虚拟线程(Java21)

虚拟线程是Java中的一种轻量级线程,由Java虚拟机(JVM)管理,与我们之前介绍的传统的操作系统级线程(Platform Threads)相比,它免去了平台线程的CPU的上下文切换,而是由程序在线程内自行控制,消耗的资源更少,启动和切换速度更快,进而可以在同一物理线程上并发执行大量虚拟线程。
- 平台线程: 就是我们上面讲解的线程,由操作系统进行调度。
- 虚拟线程: 本节介绍的由JVM在线程内部调度的线程。
创建语法
1 | Thread.startVirtualThread(() -> { |
它默认就是守护线程且不能修改
1 | Thread.ofVirtual() |