进程和线程

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

线程像进程里的执行路线

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

线程的创建和启动

继承Thread类

1
2
3
4
5
6
7
8
9
10
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}

// 调用
MyThread t1 = new MyThread();
t1.start();

实现 Runnable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}

// 调用
MyRunnable runnable = new MyRunnable();
Thread t2 = new Thread(runnable);
t2.start();


实现 Callable 接口(有返回值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14


public class Test {
public static void main(String[] args) throws Exception {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() {
return 666;
}
});
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}

实际开发一般用实现runnable接口,避免单继承局限。

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

image-20260414143119615

线程的状态

线程的几种基本状态

状态 (State) 核心特征描述 触发进入该状态的常见条件
NEW (新建) 线程对象已创建,尚未启动操作系统线程。 new Thread() 实例化后,未调用 start() 前。
RUNNABLE (可运行) 涵盖操作系统的“就绪”和“运行中”状态,正在等待 CPU 调度或正在执行。 调用线程的 start() 方法;或从阻塞/等待状态被唤醒并获取到 CPU 资源。
BLOCKED (阻塞) 线程被挂起,专门等待获取一个隐式的排他锁(Monitor Lock)。 尝试进入 synchronized 代码块或方法时,发现锁已被其他线程占用。
WAITING (无限期等待) 线程进入休眠,放弃 CPU,不会自动苏醒,必须等待其他线程的特定信号。 调用无超时参数的 Object.wait()、Thread.join() 或 LockSupport.park()。
TIMED_WAITING (限期等待) 带有超时机制的休眠,到达指定时间后系统会自动唤醒。 调用 Thread.sleep(long)、Object.wait(long) 或 Thread.join(long) 等。
TERMINATED (终止) 线程生命周期结束,不可逆,无法重新 start()。 线程的 run() 方法正常执行完毕,或抛出了未捕获的异常。

状态变化过程

线程状态变化通常是这样:

新建 → 就绪 → 运行 → 阻塞 → 就绪 → 运行 → 终止

注意:

  • 就绪和运行之间可能来回切换
  • 运行中如果遇到阻塞,会进入阻塞
  • 阻塞解除后不会直接运行,而是先回到就绪

线程的休眠和中断

线程休眠

在线程里,休眠通常用:

1
Thread.sleep(1000);

意思是:

让当前线程暂停 1000 毫秒。

休眠的特点:

1. 是当前线程休眠
谁执行 sleep(),谁就休眠。

2. 进入限时等待状态
在 Java 里,调用 sleep() 后线程进入 TIMED_WAITING 状态,会主动让出 CPU 时间片。

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("我真的是虚拟线程"));