进程和线程

进程是程序执行的实体,每一个进程都是一个应用程序,都有自己的内存空间,CPU一个核心同时只能处理一件事情,当出现多个进程需要同时运行时,CPU一般通过时间片轮转调度算法,来实现多个进程的同时运行。

线程像进程里的执行路线

  • 同一个进程里可以有多个线程
  • 这些线程共享这个进程的内存和大部分资源
  • 用来并发执行任务
维度 进程 (Process) 线程 (Thread)
本质 资源分配的基本单位 CPU 调度(执行)的基本单位
资源拥有 拥有独立的内存地址空间(Code, Data, Heap, Stack 等) 共享所属进程的资源,仅拥有少量的私有资源(如 PC, 栈, 寄存器)
创建/切换开销 。需要分配内存空间、初始化数据段等 。直接共享进程资源,上下文切换比进程快得多
通信方式 复杂。需通过 IPC(管道、信号量、Socket、共享内存等) 简单。通过读写同一进程内的全局变量或堆内存即可通信
健壮性 。一个进程崩溃通常不会影响其他进程 。一个线程崩溃可能导致整个进程内的所有线程一起挂掉

线程的创建和启动

通过创建Thread对象来创建一个新的线程,Thread构造方法中需要传入一个Runnable接口的实现(其实就是编写要在另一个线程执行的逻辑)同时Runnable只有一个未实现方法,因此可以直接使用lambda表达式:

1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

创建好后,通过调用start()方法来运行此线程:

1
2
3
4
5
6
public static void main(String[] args) {
Thread t = new Thread(() -> { //直接编写逻辑
System.out.println("我是另一个线程!");
});
t.start(); //调用此方法来开始执行此线程
}

实际上,线程和进程差不多,也会等待获取CPU资源,一旦获取到,就开始按顺序执行我们给定的程序,当需要等待外部IO操作(比如Scanner获取输入的文本),就会暂时处于休眠状态,等待通知,或是调用sleep()方法来让当前线程休眠一段时间

image-20260414143119615

线程的状态

线程的几种基本状态

1. 新建状态

线程对象已经创建出来了,但还没有启动。

比如:

1
2
3
Thread t = new Thread(() -> {
System.out.println("hello");
});

这时候线程 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
2
3
4
5
6
7
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("线程开始运行!");
});
t.setPriority(Thread.MIN_PRIORITY); //通过使用setPriority方法来设定优先级
t.start();
}

优先级越高的线程,获得CPU资源的概率会越大,并不是说一定优先级越高的线程越先执行!

线程的礼让和加入

线程礼让 yield()

线程礼让指的是:

当前线程主动让出 CPU,给别的线程一个先执行的机会。

写法:

1
Thread.yield();

作用

表示当前线程说:

“我先不抢了,别人可以先来。”

特点

  • 只是让一下
  • 不一定真的成功
  • 礼让后线程会回到就绪状态
  • CPU 可能还是继续调度它自己

线程加入 join()

线程加入指的是:

一个线程等待另一个线程执行完毕。

写法:

1
t.join();

作用

比如主线程里调用了 t.join(),意思就是:

主线程要先等线程 t 执行完,自己再继续往下走。

例子

1
2
3
4
5
6
7
Thread t = new Thread(() -> {
System.out.println("子线程执行");
});

t.start();
t.join();
System.out.println("主线程继续执行");

意思就是:

  1. 启动子线程
  2. 主线程等待
  3. 子线程执行完
  4. 主线程再继续

wait和notify

wait() 是什么

wait() 的作用是:

让当前线程进入等待状态,并且释放它持有的对象锁。

比如:

1
2
3
synchronized (lock) {
lock.wait();
}

执行后会发生:

  • 当前线程进入等待状态
  • 释放 lock 这把锁
  • 直到别的线程来唤醒它

重点

wait()sleep() 很容易混:

  • sleep():休眠时不释放锁
  • wait():等待时会释放锁

notify() 是什么

notify() 的作用是:

唤醒在这个对象上等待的一个线程。

比如:

1
2
3
synchronized (lock) {
lock.notify();
}

意思是:

  • lock 对象上等待的线程里
  • 随机唤醒一个

但要注意:

被唤醒的线程不是立刻执行,它要先重新抢到锁,才能继续往下运行。

notifyAll() 是什么

除了 notify(),还有一个:

1
lock.notifyAll();

作用是:

唤醒所有在这个对象上等待的线程。

然后这些线程再去竞争锁,谁抢到谁先执行。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Demo {
static final Object lock = new Object();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("t1开始等待");
lock.wait();
System.out.println("t1被唤醒了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2发出通知");
lock.notify();
}
});

t1.start();
t2.start();
}
}

大致过程:

  1. t1 进入同步代码块
  2. 调用 wait(),进入等待,同时释放锁
  3. t2 拿到锁,调用 notify()
  4. t1 被唤醒
  5. t1 重新拿到锁后继续执行

多线程情况下Java的内存管理

image-20260414160354093

ThreadLocal

ThreadLocal 可以简单理解成:

给每个线程都准备一份“自己专属的变量副本”。

也就是说,同一个 ThreadLocal 对象,不同线程去访问时,拿到的值是各自独立的,互不影响。

常用方法

set()

给当前线程设置值

1
2
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("hello");

get()

获取当前线程自己的值

1
String s = tl.get();

remove()

删除当前线程保存的值

1
tl.remove();

这个方法很重要,尤其在线程池里常常要手动清理。


示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo {
static ThreadLocal<String> tl = new ThreadLocal<>();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
tl.set("线程A的数据");
System.out.println(Thread.currentThread().getName() + ":" + tl.get());
});

Thread t2 = new Thread(() -> {
tl.set("线程B的数据");
System.out.println(Thread.currentThread().getName() + ":" + tl.get());
});

t1.start();
t2.start();
}

}
1
2
Thread-0:线程A的数据
Thread-1:线程B的数据

ThreadLocal 和普通共享变量的区别

普通变量

  • 多个线程访问的是同一份数据
  • 可能有线程安全问题
  • 往往需要加锁

ThreadLocal

  • 每个线程有自己独立副本
  • 不共享
  • 一般不需要加锁

定时器

定时器的作用就是:

让程序按照时间规则自动执行任务。

基本用法

TimerTask

表示你要执行的任务。

1
2
3
4
5
6
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("定时任务执行了");
}
};

Timer

负责安排任务什么时候执行。

1
2
Timer timer = new Timer();
timer.schedule(task, 3000);

意思是:

3 秒后执行一次这个任务。

常见调度方式

延迟执行一次

1
timer.schedule(task, 3000);

3 秒后执行一次。

延迟后周期执行

1
timer.schedule(task, 3000, 2000);

意思是:

  • 先延迟 3 秒
  • 然后每隔 2 秒执行一次

守护线程

守护线程(Daemon Thread)是:

在后台运行、为用户线程提供服务的线程。

比如:

  • 垃圾回收线程(GC)就是典型守护线程

特点

守护线程最关键的特点是:

当所有用户线程都结束后,守护线程会自动结束。

也就是说,守护线程不会阻止 JVM 退出。

用户线程和守护线程区别

用户线程

是程序真正干活的线程。

比如:

  • main 线程
  • 你自己创建的业务线程

只要还有用户线程没结束,JVM 就不会退出。

守护线程

主要做辅助工作。

当所有用户线程都结束时,即使守护线程还在跑,JVM 也会直接结束它。

语法

1
2
3
4
5
6
7
Thread t = new Thread(() -> {
while (true) {
System.out.println("守护线程运行中");
}
});
t.setDaemon(true);
t.start();

线程构建器(Java21)

Java 21 在 Thread 里提供了 Thread.Builder API,用来创建:

  • 平台线程 Thread.ofPlatform()
  • 虚拟线程 Thread.ofVirtual()

以前常见写法是:

1
2
3
4
Thread t = new Thread(task);
t.setName("t1");
t.setDaemon(true);
t.start();

现在可以改成链式写法:

1
2
3
4
5
Thread.ofPlatform()
.name("t1")
.daemon(true)
.start(task);

示例

创建并启动平台线程

1
2
3
4
5
6
7
Thread.ofPlatform()
.name("线程01")
.daemon(true)
.priority(Thread.MAX_PRIORITY)
.start(() -> {
System.out.println("Hello World");
});

意思就是:
先配置线程属性,再直接启动这个平台线程。相关配置能力由 Thread.Builder.OfPlatform 提供。

例 2:创建但不启动

1
2
3
4
5
6
7
Thread t = Thread.ofPlatform()
.name("t1")
.unstarted(() -> {
System.out.println("run");
});

t.start();

这里 unstarted() 只是创建线程,不会立即运行。

虚拟线程(Java21)

image-20260414172103427

虚拟线程是Java中的一种轻量级线程,由Java虚拟机(JVM)管理,与我们之前介绍的传统的操作系统级线程(Platform Threads)相比,它免去了平台线程的CPU的上下文切换,而是由程序在线程内自行控制,消耗的资源更少,启动和切换速度更快,进而可以在同一物理线程上并发执行大量虚拟线程。

  • 平台线程: 就是我们上面讲解的线程,由操作系统进行调度。
  • 虚拟线程: 本节介绍的由JVM在线程内部调度的线程。

创建语法

1
2
3
Thread.startVirtualThread(() -> {
System.out.println("我是虚拟线程");
});

它默认就是守护线程且不能修改

1
2
3
Thread.ofVirtual()
.name("虚拟线程 01")
.start(() -> System.out.println("我真的是虚拟线程"));