进程和线程
进程
所谓计算机程序 Program,其实就是通过执行一系列指令来完成某一个任务。当你启动一个程序时,操作系统(OS)会将其加载到内存中,并在内存中申请一块固定地址的命名空间(address space),并在此命名空间内执行相关指令。聪明人应该已经听出来了,这不就是"进程 Process" 嘛。没有错,某种程度上我们确实可以将进程理解为一个程序的。
线程
线程就是在进程内部,一系列可执行的独立指令。而这些独立指令最终是被 CPU 执行。为了更大利益化的使用 CPU,在一个进程Process内,可以存在多个线程。这就让程序有了并发执行指令的能力,增加了程序的吞吐量。
CPU 和 并发
实际上大多数情况下,"并发"只是 CPU 给我们造成的一种"幻觉"。只有在线程数小于 CPU 核数时,才是真正意义的并发。
假设我们有一个 4 核CPU,然后同时开启4个线程执行任务,那在同一时间CPU的4核会分别执行1个线程的指令,如下图:
但是如果我们开启 8 个线程执行任务,因为CPU只有4核,所以只能通过切换上下文(switch-context)的方式来实现并发的"幻觉"。也就是在不同的线程间切换,因为切换时间极快,使用户从视觉上感知就是同时在执行的。如下图
Thread 奇技淫巧
线程是一把"双刃剑",使用合理能够提高系统效率,使用不当也会造成揠苗助长。
假设我们有一个 int 类型变量 count,我们分别使用 1000 个线程使其自增1,以及使用单个线程使其自增1000次。我们可以对比下这2种方式所耗的时间,代码如下:
public class SumOfNums {
volatile int count = 0;
static long start, end;
synchronized void increase_with_multi_thread() {
count++;
if (count == 2000) {
end = System.currentTimeMillis();
System.out.println("multi thread, start: " + start + " end:" + end + " time: " + (end - start));
}
}
void increase_with_single_thread() {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
count++;
}
end = System.currentTimeMillis();
System.out.println("single thread, start: " + start + " end:" + end + " time: " + (end - start));
}).start();
}
public static void main(String[] args) {
SumOfNums sumOfNums = new SumOfNums();
start = System.currentTimeMillis();
sumOfNums.increase_with_single_thread(); // 1. 单线程自增 1000 次
// 2. 创建 1000 个线程,使其自增1
for (int i = 1; i <= 1000; i++) {
Thread t = new Thread(sumOfNums::increase_with_multi_thread);
t.start();
}
}
}
执行上述代码,得到如下结果:
single thread, start: 1685429904704 end:1685429904706 time: 2
multi thread, start: 1685429904704 end:1685429904748 time: 44
可以看出,创建过多的线程并没有提高工作效率,反而比单线程慢超过20倍!
问题分析
在上述将 count 自增的操作属于 CPU密集型任务,计算结果高度依赖 CPU 的计算效率。之所以在之前的结果里多个线程的效率慢,是因为我们创建了 1000 个线程,个数已经完全超出了CPU核数,所以 CPU 不得不在多个线程中进行上下文切换,这个操作会严重影响程序运行效率。
除了 CPU 密集型任务,还有一种 IO 密集型任务。假设我们需要读取磁盘空间上的 20 个文件,并对其内容进行计算。在磁盘读取时,CPU是处于 IDLE(闲置) 状态。因为磁盘读取操作是发生在特殊硬件设备(disk 驱动)中。此时不需要 CPU 的参与,只有读完磁盘之后,剩下的操作才交给 CPU 进行处理。假设我们还是在4核CPU上,使用 4 个线程执行此任务,使用整个过程如下图所示:
可以看出,每一个线程被相应的分配给 CPU 的每个核。线程是充分利用了,但是当代码执行到磁盘读取操作时,CPU核就会处于 IDLE 状态,浪费了CPU资源。对于这种情况,如果我们就可以通过将线程数提高到8个来提高工作效率,如下图:
效率提高的原因是,在之前4个线程的 IDLE 状态,此时因为有额外线程需要执行。所以 CPU 会继续执行其它额外的线程任务,充分利用了 CPU 资源。总结就是:与其 IDLE,不如压榨!
Thread 挖一挖
在现代操作系统中,Thread一般有2种类型:Kernel Thread 和 User Thread。
Kernel Thread
内核线程,又可以被叫做 OS Thread,也就是操作系统线程。Kernel 线程是由操作系统内核来管理,每一个线程都需要包含线程状态、优先级,以及一些其它属性。这类线程相对较重,需要调用操作系统的API来创建、维护管理。
User Thread
User Thread就是用户线程,是处于应用层面的一种数据结构体,比如我们使用 Java SDK 中的 Thread就是User Thread。在这种数据结构内部,同样也定义了线程的状态、优先级等属性。
但实际上User Thread 无法被直接执行,User Thread 需要 mapping 到 Kernel Thread 之后,被解释成指令后再执行。一共有3种mapping方式:
M:1 model: 所有的User Thread 都与1个Kernel Thread进行mapping
1:1 model: 1个User Thread对应2个Kernel Thread
M:N model: 所有的User Thread 对应的是系统层的 Kernel Thread Pool
Java Thread
上面说的 mapping 方式是大多操作系统会使用的模型方式,那么在 Java JVM中是使用何种方式呢?
Green Thread
在 早期版本,Java 提供的是 Green Thread 模型方式,所有线程都是由 JVM 管理和调度。并且 Green Thread 与 Kernel Thread 的 mapping 模式是 M:1 方式,所以Green Thread的运行速度是极快的。但是 Green Thread 有一个比较大的缺陷:无法针对多核处理器进行扩展。
Native Thread
从 JDK 1.2 之后,Java 就放弃了 Green Thread 模型方式,改用 Native Thread 模型方式了。Native 同样也是有 JVM 管理,但是一定程度上对底层的Kernel有依赖;另外,Native Thread 与 Kenel Thread 的 mapping 模式是 1:1 模式。
改用 Native Thread之后,这几番操作下来,在一定程度上提高了线程的运行效率,但是带来的后果就是线程的创建和销毁操作变得异常笨重。这也是为什么我们在项目中会使用线程池来缓存线程、提高运行效率的原因。
虚拟线程
Java 19 版本引入了虚拟线程 Virtual Thread,它是一种轻量级的线程。虚拟线程解决了 Native Thread 的缺陷,创建和销毁过程不再那么笨重,不需要操作系统的参与。
可以通过如下方式,使用虚拟线程:
for (int i = 0; i < 5; i++) {
Thread vThread = Thread.ofVirtual().start(() -> System.out.println("Hello World"));
}
或者使用Executors创建虚拟线程:
public static void main(String[] args) {
var executor = Executors.newVirtualThreadExecutor();
for (int i = 0; i < 5; i++) {
executor.submit(() -> System.out.println("Hello World"));
}
executor.awaitTermination();
System.out.println("All virtual threads are finished");
}
如果你喜欢本文
长按二维码关注