Java并发编程:理解进程和线程

最早的计算机只有每次用户给出指令,计算机就执行一次,没有指令时就会停止运行。很多时候,计算机就这样闲置着。

批处理操作系统

后来,批处理操作系统出现了,它能够一次接受一系列指令,然后按顺序执行。用户可以将想要执行的程序写在磁带上,然后让计算机读取并执行,将输出结果写在另一盘磁带上。

批处理操作系统在一定程度上提高了计算机的效率,但由于批处理操作系统的指令执行模式仍然是串行的,内存中始终只有一个程序在运行,后续程序需要等待前一个程序执行完成后才能开始执行。而前一个程序有时会因 I/O 操作、网络等原因阻塞,所以批处理操作效率并不高。

进程

随着技术的发展,人们对计算机性能的要求越来越高,现有的批处理操作系统已无法满足人们的需求。批处理操作系统的瓶颈在于内存中只有一个程序,那么内存中能否有多个程序呢?显然这是可以的。于是就提出了进程的概念。

进程是应用程序在内存中分配的空间,即正在运行的程序。例如,运行一个 Java 程序的本质就是启动一个 Java 虚拟机进程,这意味着一个正在运行的 Java 程序就是一个 Java 虚拟机进程。

例如,以下是一个简单的 Java 程序:

java 复制代码
public class SimpleJavaApp {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

操作系统可以同时运行多个进程,如 chrome、QQ 等,它们互不干扰。同时,进程会保存程序在每个时刻的运行状态。

此时,CPU 使用时间片轮转调度算法来运行进程:CPU 为每个进程分配一个时间段,称为时间片。如果时间片结束时进程仍在运行,则将该进程挂起,并将 CPU 分配给另一个进程(这个过程称为上下文切换)。如果进程在时间片结束前阻塞或完成,则 CPU 立即切换,无需等待时间片用完。

当一个进程被挂起时,它会保存当前进程的状态(进程标识、进程使用的资源等)。下次切换回来时,会根据之前保存的状态进行恢复并继续执行。

使用进程和时间片轮转调度算法的操作系统在宏观层面上似乎在同一时间段内运行多个进程;虽然并发在宏观层面上看起来有多个任务在执行,但实际上,对于单核 CPU,在任何特定时刻只有一个任务占用 CPU 资源。

并发与并行

刚刚提到了并发,这里简要解释一下并发与并行的区别。

并行:在同一时刻,多个任务在多个 CPU 上同时执行。

并发:在同一时刻,多个任务在单个 CPU 上交替执行。

线程

虽然进程的出现大大提高了操作系统的性能,但随着时间的推移,人们不满于一个进程一次只能做一件事。如果一个进程有多个子任务,这些子任务只能一个接一个地执行,这极大地影响了效率。

例如,当你使用浏览器浏在下载文件时不能继续浏览其他网页,这显然无法满足人们的需求。

这些子任务能否同时执行呢?因此,人们提出了线程的概念,让一个线程执行一个子任务。这样一个进程就可以包含多个线程,每个线程负责一个单独的子任务。

例如,在以下用 Java 实现的多线程代码中,在主线程中开启了两个线程,分别负责两个任务,一个打印"Hello world",一个打印"Hello java"。

java 复制代码
class SayHelloThread extends Thread {
    public void run() {
        System.out.println("Hello");
    }
}

class SayJavaThread extends Thread {
    public void run() {
        System.out.println("Java");
    }
}

public class MultiThreadJavaApp {
    public static void main(String[] args) throws InterruptedException {
        SayHelloThread sayHelloThread = new SayHelloThread();
        SayJavaThread sayJavaThread = new SayJavaThread();
        sayHelloThread.start();
        sayJavaThread.start();
        // 主线程睡眠
        Thread.sleep(5000);
    }
}

输出:

复制代码
Hello
Java

上述输出结果并不总是相同,因为sayHelloThread并不总是先获得执行机会。

在使用线程后,下载文件时,让负责下载功能的线程执行下载。用户浏览网页时,执行负责浏览网页功能的线程。然后,通过时间片轮转,用户可以感觉到两个功能同时在运行。

进程与线程的区别

进程和线程的引入极大地提高了操作系统的性能,那么它们之间有什么区别呢?

  1. 资源占用:进程是操作系统分配资源的基本单位,它包含程序执行的一个实例,包括代码、数据和系统资源(如内存、文件、设备等)。每个进程都有独立的内存空间和系统资源,互不干扰。线程是操作系统调度的基本单位,即 CPU 分配时间的单位。多个线程共享同一进程的内存空间和系统资源。

  2. 数据交换:进程独立执行,有自己的地址空间和系统资源,通过进程间通信(IPC)进行数据交换。线程是进程内的一条执行路径。同一进程中的多个线程共享内存空间和系统资源,所以线程之间的数据交换更简单、快速。

  3. 开销:由于进程有独立的内存空间和系统资源,创建和销毁进程的开销相对较大,需要保存和恢复更多的上下文信息。线程共享进程的内存空间和系统资源,所以创建和销毁线程的开销较小,只需要保存和恢复少量上下文信息。

  4. 并发:进程是独立的执行单元,有自己的调度算法,在并发条件下更稳定可靠。由于线程共享进程资源,线程之间的调度和同步更复杂,需要更多注意处理并发条件。这也是我们后续文章将重点关注的问题。

基于上述差异,我们可以看到,对于一个进程内的多个任务实现并发,最好的方法是使用多个线程而不是多个进程,但需要特别注意并发逻辑的处理。

上下文切换

上下文切换(有时也称为进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指在某个时间点 CPU 寄存器和程序计数器的内容。

其中,寄存器是 CPU 内部少量的访问速度很快的存储,通常用于存储和访问计算过程中的中间值,以提高计算机程序的运行速度。

程序计数器是一个专用寄存器,用于指示 CPU 当前正在执行的指令序列中的位置,存储的值是正在执行的指令的位置或下一条要执行的指令的位置,具体实现取决于特定系统。

CPU 通过为每个线程分配 CPU 时间片并通过时间片轮转调度算法执行任务来实现多线程机制。当一个任务执行完一个时间片时,它将切换到下一个任务,在切换之前,会保存前一个任务的状态,以便下次切换回该任务时可以重新加载该任务的状态。因此,从保存到重新加载一个任务的过程就是一次上下文切换。

例如,现在线程 A 的时间片已执行完,需要切换到线程 B,需要以下三个步骤:

  1. 先挂起线程 A,并将其状态从 CPU 保存到内存中。

  2. 在内存中,获取下一个线程 B 的上下文,并将其恢复到 CPU 的寄存器中以执行线程 B。

  3. 当线程 B 执行完毕,根据程序计数器指向的位置恢复线程 A。

上下文切换通常计算密集,这意味着此操作会消耗大量 CPU 时间,所以线程不是越多越好。如何减少系统中的上下文切换次数是提高多线程性能的关键问题。我们将在未来的文章中讨论这个问题。

相关推荐
福建佰胜张工15 分钟前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
念何架构之路15 分钟前
go-grpc设计思想
开发语言·后端·golang
catoop18 分钟前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x1 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事1 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客2 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1882 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn2 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su3 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
码事漫谈3 小时前
AI 读你的提示词,其实很没耐心
后端