JUC概述

JUC是什么

JUC 是 Java 中处理多线程编程的一个工具包(java.util.concurrent 包),提供了一套强大的工具,让开发者能更轻松、安全地编写高性能的多线程程序。

进程 线程 协程

进程(Process)

进程是操作系统进行资源分配和调度的基本单位。它是一个正在执行的程序的实例,拥有独立的地址空间、代码、数据和系统资源(如文件句柄、网络端口等)。

可以简单理解为资源+线程,操作系统运行的一个程序。

线程(Thread)

线程是进程内的执行单元,是CPU调度和执行的基本单位。一个进程可以包含多个线程,所有线程共享进程的地址空间和资源。

比如一个程序通过一个个线程对CPU进行调度。

协程(Coroutine / Fiber)

协程是用户态的轻量级线程,由程序员在用户空间管理调度(而非操作系统内核)。协程在同一个线程内执行,通过协作式调度(非抢占式)来切换执行流。

总结

  • 先有进程,然后进程可以创建线程,线程是依附在进程里面的,线程里面可以包含多个协程。
  • 进程之间不共享资源,所有的线程共享进程的资源,协程共享线程的资源。

串行 并发 并行

串行(Serial)

串行执行是指任务按顺序一个接一个地执行,只有在前一个任务完全完成后,后一个任务才能开始。

1
时间轴:|--- 任务A ---||--- 任务B ---||--- 任务C ---|>

并发(Concurrent)

并发是指在重叠的时间段内处理多个任务,通过任务间的快速切换来模拟”同时”执行的效果。这可以在单核或多核处理器上实现。

多个线程微观上是串行的,但是由于CPU切换速度很快,宏观上这些线程是“同时”执行的。

1
2

时间轴:|---任务A---||---任务B---||---任务C---||---任务A---||---任务B---||---任务C---|>

并行(Parallel)

并行是指真正的同时执行多个任务,多个核心执行不多个进程/线程。

1
2
3
4
多核CPU时间轴:
核心1:|--- 任务A ---||--- 任务A ---|>
核心2:|--- 任务B ---||--- 任务B ---|>
核心3:|--- 任务C ---||--- 任务C ---|>

上下文切换

CPU上下文

多核CPU在运行任务时,使用程序计数器来存储当前执行指令的位置或者下一条将要执行的指令的位置,而CPU寄存器则是CPU中的内存。它们都是CPU在运行时必须依赖的环境,因此也被称作为CPU上下文。

CPU上下文切换

CPU上下文切换就是把前一个任务的上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文开始执行。

而保存起来的上下文,会存储在系统内核中,并在任务重新执行时再次加载进来。

根据CPU执行的任务的不同,上下文切换分为这三种:进程上下文切换、线程上下文切换,中断上下文切换。

什么时候会发生上下文切换

主动让出CPU(自愿性切换 - Voluntary)

线程主动请求暂停,并告诉调度器“我可以被切换了”。

  • 等待资源(I/O操作):这是最常见的原因。当一个线程需要读取文件、等待网络数据包、或获取用户输入时,它会发起一个系统调用并进入睡眠(Sleep)或阻塞(Blocked)状态。CPU立即被释放,操作系统会选择另一个就绪(Runnable)的线程(可以是同一进程的,也可以是其他进程的)来运行。
  • 主动休眠:线程调用睡眠函数(如 sleep(), nanosleep()),明确告诉操作系统“我需要在未来X段时间内放弃CPU”。
  • 等待同步原语:线程尝试获取一个已经被其他线程持有的(如互斥锁Mutex)、信号量(Semaphore)时,它会被阻塞,从而触发切换。
  • 主动让出:线程显式调用类似 pthread_yield()sched_yield() 的函数,建议调度器立即切换上下文,让其他同等优先级的线程运行。

被操作系统强制剥夺(非自愿性/抢占式切换 - Involuntary/Preemptive)

线程本身还想继续运行,但操作系统基于某种策略决定中断它。

  • 时间片耗尽(Time Slice Exhausted):这是抢占式调度的基石。每个线程被分配一个固定的时间片(Quantum,通常是几毫到几百毫秒)。当线程用完了它的时间片,操作系统会触发一个时钟中断(Timer Interrupt),中断处理程序会检查时间片,如果耗尽,则调用调度器切换到另一个就绪线程。
  • 更高优先级线程就绪:如果一个处于睡眠状态的高优先级线程等待的事件发生了(例如它等的I/O完成了),它会立刻变为就绪状态。调度器可能会抢占当前正在运行的低优先级线程,即使其时间片还没用完,也要马上切换到高优先级线程,以保证系统的响应性。

被硬件中断打断

为了响应硬件的各种事件设计出来的,中断程序会打断进程的正常执行。例如,当前CPU正在全力执行一些程序,这个时候我们挪了挪鼠标,按了下键盘。CPU就必须中断正在执行的程序,转而去响应这些硬件的事件。

总结

线程上下切换开销小,进程直接切换开销大。线程切换虽然开销小,但是还是有消耗,不能盲目添加过多的线程。

  • 当发生线程间的上下文切换时,如果两个线程属于同一个进程,切换会非常“廉价”。
    • 不需要切换内存地址空间(页表)、文件描述符表等资源。只需要切换线程的私有资源(寄存器、栈指针、程序计数器等)。
  • 如果两个线程属于不同进程,切换则更“昂贵”。
    • 除了切换线程的私有资源,还必须切换整个内存地址空间(通过切换页表寄存器实现),这会导致Translation Lookaside Buffer (TLB) 被刷新,开销更大。

创建线程

继承 Thread 类

1
2
3
4
5
6
7
8
9
10
11
12
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("继承Thread类创建的线程:" +
Thread.currentThread().getName());
}

public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}

实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口创建的线程:" +
Thread.currentThread().getName());
}

public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();

Thread lambdaThread = new Thread(() -> {
System.out.println("Lambda线程: " + Thread.currentThread().getName());
});

lambdaThread.start();
}
}

实现 Callable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000); // 模拟耗时操作
return "Callable执行结果: " + Thread.currentThread().getName();
}
public static void main(String[] args) throws Exception {
Callable<String> callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);

Thread thread = new Thread(futureTask);
thread.start();

// 获取执行结果(会阻塞直到任务完成)
String result = futureTask.get();
System.out.println("结果: " + result);

System.out.println("结果: ");
}

}

使用线程池

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
28
public class MyThreadPool {
public static void main(String[] args) {
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(4);

// 提交Runnable任务
executor.execute(() -> {
System.out.println("Runnable执行结果: " +
Thread.currentThread().getName());
});

// 提交Callable任务
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Callable执行结果: " + Thread.currentThread().getName();
});

try {
String result = future.get();
System.out.println("Callable结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}

// 关闭线程池
executor.shutdown();
}
}

Java Thread 类常用方法

方法名 作用描述 返回值 注意事项
start() 启动线程,使其进入就绪状态 void 只能调用一次,第二次会抛 IllegalThreadStateException
run() 线程要执行的任务内容 void 不要直接调用,应该通过 start() 间接调用
sleep(long millis) 让当前线程休眠指定毫秒数 void 静态方法,会抛 InterruptedException
sleep(long millis, int nanos) 更精确的休眠(毫秒+纳秒) void 静态方法,实际精度依赖操作系统
yield() 提示调度器当前线程愿意让出CPU void 静态方法,只是提示,不保证一定暂停
join() 等待该线程终止 void 会抛 InterruptedException
join(long millis) 最多等待该线程终止指定毫秒数 void 超时后继续执行,抛 InterruptedException
interrupt() 中断目标线程 void 设置中断标志位,非强制终止
isInterrupted() 测试线程是否被中断 boolean 实例方法,不清除中断状态
static interrupted() 测试当前线程是否被中断 boolean 静态方法,会清除中断状态
setPriority(int priority) 设置线程优先级(1-10) void 只是提示,具体取决于JVM和OS实现
getPriority() 获取线程优先级 int 默认是5(NORM_PRIORITY
setName(String name) 设置线程名称 void 便于调试和监控
getName() 获取线程名称 String 默认格式:"Thread-" + n
static currentThread() 获取当前执行线程的引用 Thread 非常重要的静态方法
getState() 获取线程状态 Thread.State 返回枚举:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isAlive() 测试线程是否存活 boolean 线程启动后且未死亡返回 true
setDaemon(boolean on) 设置是否为守护线程 void 必须在 start() 前调用,默认为false
isDaemon() 测试是否为守护线程 boolean 守护线程不会阻止JVM退出
stop() 已废弃:强制停止线程 void 不安全,会导致资源未释放,不要使用!
suspend() 已废弃:挂起线程 void 容易导致死锁,不要使用!
resume() 已废弃:恢复挂起的线程 void 配套 suspend(),同样不要使用!

线程的6种状态

线程状态 说明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕

image-20250902161929338