一、进程和线程
1. 进程
什么是进程?
简单来说,
进程就是程序的一次启动和执行
。进程是操作系统中的一个概念,它代表正在运行的程序的实例
。每个进程都有自己的内存空间
、代码和数据
,以及其他操作系统资源
,如文件和设备。进程之间是相互独立的,它们不能直接访问彼此的内存空间,但可以通过特定的机制进行通信。什么是程序?
程序是一组指令的集合
,用来完成特定任务或解决特定问题的一系列计算机代码。程序通常存储在文件中,包括可执行文件或源代码
文件。**进程和程序有什么关系?
从使用者角度来说明,一个程序可以启动多次,那么就会对应多个进程,例如我们在使用浏览器的时候,可以启动很多个浏览器。
从开发者角度来说明,程序是开发人员编写的
源代码
和二进制文件
,但程序本身是静态的,它只是存在于磁盘或其他存储设备中,并不具有执行的能力。 而**进程 是 程序在计算机上运行的实例**
1.1. 进程的大致结构
进程一般,是由程序段,数据段,进程控制块三个部分组成
- **程序段:**一般也被称为代码段,也就是需要执行指令的集合
- **数据段:**是进程的操作数据在内存中的位置,
- **程序控制块(Program Control Block)PCB:**包含进程的描述信息和控制信息,是进程存在的唯一标志
- PCB主要由四大部分组成
- 进程描述信息:进程描述信息是操作系统用来跟踪和管理进程的基本数据结构。它包括了进程的标识符(PID,Process ID)、进程状态(运行、就绪、等待等)、进程所有者、进程的父进程和子进程关系等。这些信息使得操作系统能够有效地管理系统中的所有进程,并在需要时进行调度和资源分配。
- 进程的调度信息:进程的调度信息包括了操作系统用来决定进程执行顺序的相关数据。这些信息通常包括了进程的优先级、调度策略(如先进先出、轮转调度等)、调度队列中的位置等。通过调度信息,操作系统可以决定哪个进程应该优先执行,以及在多任务环境中如何分配系统资源。
- 进程的资源信息:进程的资源信息描述了进程所拥有的系统资源,包括了内存、文件、设备等。这些资源信息包括了进程分配的内存空间、打开的文件描述符、设备访问权限等。操作系统使用这些信息来管理和分配系统资源,以确保进程能够正常运行并访问所需的资源。
- 进程的上下文:进程的上下文是指进程在执行过程中的状态和环境信息。它包括了进程的寄存器内容、程序计数器、堆栈指针以及其他与进程执行相关的状态信息。操作系统在进行进程切换或者处理中断时,需要保存和恢复进程的上下文信息,以确保进程能够从中断或者切换中恢复到正确的执行状态。保存和恢复进程上下文是操作系统进行有效进程调度和管理的关键步骤之一。
- PCB主要由四大部分组成
2.线程
2.1. 线程的基本原理
早期的操作系统确实没有线程的概念,只有进程。在早期的操作系统中,进程是最小的资源分配单位,每个进程拥有独立的地址空间和资源,通过操作系统的调度器进行调度和管理。后来随着计算机的发展,CPU的性能越来越高,为了提高CPU的利用率,进程内部演进并发调度的诉求,于是就发明了线程。
线程指的是,进程代码段的一次顺序的执行流程,线程是CPU调度的最小单位,一个进程可以有多个线程,各个线程之间共享进程的内存空间、系统资源
进程仍然是操作系统的资源的分配的最小单位
在Java程序中当你启动一个Java程序时,实际上会启动至少两个线程。
- 主线程(Main Thread):这是程序开始执行时默认启动的线程,它是程序的入口点。主线程负责执行
main()
方法中的代码,并且在main()
方法执行完毕后,主线程也会随之结束。- 虚拟机线程(JVM Thread):除了主线程之外,Java虚拟机还会启动其他一些线程,用于执行不同的任务,例如垃圾回收、JIT编译、线程调度等。这些线程通常由Java虚拟机自动管理,开发者不需要过多地关注它们。
2.2.线程基本结构
线程的基本信息
- 线程ID:线程唯一标识,同一个进程内的线程的ID不会重复
- **线程名称:**线程名称主要用于标识和区分不同的线程,提高程序的可读性和调试性。在多线程程序中,通常会创建多个线程来执行不同的任务,每个线程可能负责不同的工作,有不同的执行逻辑和处理方式。主要是可以方便用户识别是哪一类线程,用户可以自定义线程名称,如果没有指定,那么系统会默认给线程分配一个名称。
- **线程的优先级:**表示线程的调度的优先级,优先级越高,那么被CPU执行的可能性就越大。
需要注意,在Java中,虽然可以通过设置线程优先级来影响线程调度器的行为,但是最终的调度取决于底层操作系统和Java虚拟机的具体实现。Java线程优先级只是给线程调度器一个提示,告诉它哪些线程可能更重要或更紧急,但并不能保证线程会按照指定的优先级顺序执行。实际上,线程优先级在不同的操作系统和不同的Java虚拟机中可能会有不同的行为
- **线程状态:**当前线程的执行状态,为
新建
,就绪
,阻塞
,运行
,结束
,等状态中的一种 - **其他信息:**例如是否为守护线程等
*程序计数器
- 程序计数器非常重要,它记录者线程下一条指令的代码的内存的中的地址
下面的同过一个简单的程序,来看一下,Java中线程的基本信息
public static void main(String[] args) {
// 创建一个线程 打印出线程所有的基本信息
Thread thread = new Thread(() -> {
logger.error("=============== 线程启动了!==================");
}, "线程1");
// 启动线程
thread.start();
logger.error("线程的id:{}",thread.getId());
logger.error("线程的名称:{}",thread.getName());
logger.error("线程的优先级:{}",thread.getPriority());
logger.error("线程的状态:{}",thread.getState());
logger.error("线程的线程组:{}",thread.getThreadGroup());
logger.error("线程的是否是守护线程:{}",thread.isDaemon());
logger.error("线程的是否是活跃的:{}",thread.isAlive());
logger.error("线程的是否是中断的:{}",thread.isInterrupted());
}
2.3.栈内存
栈内存(Stack Memory)是计算机内存中的一种重要的内存分配方式,它主要用于存储函数调用时的局部变量、函数参数、函数调用返回地址以及一些临时数据。栈内存的管理是由编译器自动完成的,它的特点是后进先出(LIFO,Last In First Out)的数据结构。
- 函数调用: 当一个函数被调用时,系统会为该函数创建一个称为栈帧(Stack Frame)的内存块,栈帧中存储了函数的参数、局部变量以及函数调用返回地址等信息。每次函数调用时,都会在栈上分配一个新的栈帧,函数执行完毕后,其对应的栈帧会被销毁,从而释放相应的内存空间。
- 局部变量: 在函数内部声明的变量通常都存储在栈上。这些变量在函数执行期间可见,当函数执行结束时,它们的内存空间也会被释放。因此,栈内存的生命周期与函数调用的生命周期密切相关。
- 函数参数: 函数的参数通常也会存储在栈上,它们在函数调用时被压入栈中,并在函数执行期间被访问和使用。
- 递归调用: 栈内存也被广泛用于实现递归算法。每当递归函数调用自身时,都会在栈上创建一个新的栈帧,用于存储该次函数调用的参数和局部变量。递归调用结束后,栈帧被销毁,从而释放相关的内存空间。
- 有限空间: 栈内存的大小通常是有限的,这意味着在一段时间内,你可以使用的栈内存空间是有限的。当递归调用层次过深或者局部变量过多时,可能会导致栈溢出(Stack Overflow)错误。
- 高效访问: 由于栈内存的实现方式简单,并且具有连续的内存地址,因此对于处理函数调用和局部变量访问非常高效。
2.4.栈帧
栈帧(Stack Frame)是在函数调用时在栈内存中创建的一个内存块,用于存储函数的参数、局部变量、返回地址以及其他与函数执行相关的信息。每次函数调用时,都会创建一个新的栈帧,函数执行结束后,该栈帧会被销毁,从而释放相应的内存空间。
以下是栈帧的详细介绍:
- 返回地址(Return Address): 栈帧中存储了函数调用后返回到调用点的地址。当函数执行完毕时,程序需要知道从哪里继续执行,因此返回地址被保存在栈帧中。一般来说,函数执行完毕后,会跳转到该地址继续执行。
- 函数参数: 栈帧中包含了函数调用时传递的参数。这些参数被压入栈中,以便函数内部可以访问和使用它们。在函数执行过程中,可以通过栈帧访问这些参数。
- 局部变量: 函数中声明的局部变量也存储在栈帧中。局部变量在函数执行期间可见,只在函数作用域内有效。当函数执行结束时,这些局部变量的内存空间会被释放。
- 返回值: 如果函数有返回值,通常会在栈帧中预留一部分空间来存储该返回值。当函数执行完毕并准备返回时,返回值会被放置到对应的位置,以便调用者可以获取到函数的返回结果。
- 保存的上下文信息: 在一些体系结构中,栈帧可能会存储一些额外的上下文信息,如寄存器的状态或者其他与函数执行相关的信息,以便函数执行完毕后可以恢复调用点的状态。
- 栈指针(Stack Pointer): 栈指针是一个特殊的寄存器,它指向当前栈顶的位置。在函数调用过程中,栈指针会随着栈帧的创建和销毁而移动。当函数调用结束后,栈指针会被调整以释放对应的栈帧空间。
- 栈帧的顺序和布局: 栈帧通常按照一定的布局顺序在栈上分配空间,常见的布局顺序包括参数、返回地址、局部变量等。不同的编程语言和体系结构可能对栈帧的布局有所不同。
栈帧(Stack Frame)是在函数调用时在栈内存中创建的一个内存块,用于存储函数的参数、局部变量、返回地址以及其他与函数执行相关的信息。每次函数调用时,都会创建一个新的栈帧,函数执行结束后,该栈帧会被销毁,从而释放相应的内存空间。以下是栈帧的详细介绍:
- 返回地址(Return Address): 栈帧中存储了函数调用后返回到调用点的地址。当函数执行完毕时,程序需要知道从哪里继续执行,因此返回地址被保存在栈帧中。一般来说,函数执行完毕后,会跳转到该地址继续执行。
- 函数参数: 栈帧中包含了函数调用时传递的参数。这些参数被压入栈中,以便函数内部可以访问和使用它们。在函数执行过程中,可以通过栈帧访问这些参数。
- 局部变量: 函数中声明的局部变量也存储在栈帧中。局部变量在函数执行期间可见,只在函数作用域内有效。当函数执行结束时,这些局部变量的内存空间会被释放。
- 返回值: 如果函数有返回值,通常会在栈帧中预留一部分空间来存储该返回值。当函数执行完毕并准备返回时,返回值会被放置到对应的位置,以便调用者可以获取到函数的返回结果。
- 保存的上下文信息: 在一些体系结构中,栈帧可能会存储一些额外的上下文信息,如寄存器的状态或者其他与函数执行相关的信息,以便函数执行完毕后可以恢复调用点的状态。
- 栈指针(Stack Pointer): 栈指针是一个特殊的寄存器,它指向当前栈顶的位置。在函数调用过程中,栈指针会随着栈帧的创建和销毁而移动。当函数调用结束后,栈指针会被调整以释放对应的栈帧空间。
- 栈帧的顺序和布局: 栈帧通常按照一定的布局顺序在栈上分配空间,常见的布局顺序包括参数、返回地址、局部变量等。不同的编程语言和体系结构可能对栈帧的布局有所不同。
通过一个案例来了解一下栈帧
这是基础函数调用 计算数值累加的方法,下面通过这个方法来具体了解一下栈帧
public class ThreadStackDemo {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
int a = 1;
int b = 1;
Res res = add(a, b);
logger.error("res:{}", res.getData());
}
public static Res add(int a, int b) {
Res res = new Res();
int c = a + b;
res.setData(c);
logger.error("c:{}", c);
return res;
}
}
@Getter @Setter
class Res{
private int data;
}
这段代码的执行原理:
- main() 方法执行:
- 当程序开始执行时,JVM会在主线程的调用栈上创建一个栈帧用于执行
main()
方法。 - 在
main()
方法中,首先声明了两个整型变量a
和b
,它们被分配在main()
方法的栈帧中。 - 然后调用
add(a, b)
方法,这个方法调用会创建一个新的栈帧并压入调用栈。
- 当程序开始执行时,JVM会在主线程的调用栈上创建一个栈帧用于执行
- add() 方法执行:
add()
方法被调用时,JVM在调用栈上创建一个新的栈帧用于执行该方法。- 在
add()
方法内部,声明了一个Res
对象res
,它被分配在add()
方法的栈帧中。 - 接着声明了一个整型变量
c
,也被分配在add()
方法的栈帧中。 - 计算
a + b
,将结果赋值给c
。 - 调用
res.setData(c)
方法,这个方法调用会在当前栈帧中执行。 - 最后返回
res
对象。
- 返回到 main() 方法:
add()
方法执行完毕后,将返回到main()
方法。- 在
main()
方法中,接收add()
方法返回的Res
对象,并将其赋值给res
。 - 使用
logger
输出结果。
- 栈帧的销毁:
- 当
main()
方法执行完毕后,主线程的栈帧被销毁。 - 随着
main()
方法的结束,整个程序执行结束,JVM 会关闭。
- 当
2.5.线程的7种状态
在Java中,线程可以处于不同的状态,这些状态反映了线程在其生命周期中的不同阶段和行为。
- 新建状态(NEW): 当线程对象被创建但尚未启动时,线程处于新建状态。此时,线程对象被分配了内存,但尚未调用
start()
方法启动线程。在新建状态下,线程并不会占用系统资源。 - 就绪状态(RUNNABLE): 当线程处于就绪状态时,表示线程已经被创建并且已经调用了
start()
方法,但是还没有被分配到 CPU 时间片,即线程处于就绪队列中等待系统调度执行。处于就绪状态的线程可能随时被调度执行,取决于操作系统的调度算法。 - 运行状态(RUNNING): 当线程处于运行状态时,表示线程已经获得了CPU时间片,正在执行任务。处于运行状态的线程可能在任何时候被系统中断或者主动放弃CPU控制权。
- 阻塞状态(BLOCKED): 当线程被阻塞时,处于阻塞状态。线程可能因为多种原因进入阻塞状态,比如等待锁的释放、等待输入/输出完成、等待其他线程执行完毕等。当满足某个条件时,线程会从阻塞状态转移到就绪状态,等待再次被调度执行。
- 等待状态(WAITING): 当线程处于等待状态时,表示线程暂时停止执行,直到接收到通知或者被中断。线程可能调用了
Object.wait()
方法、Thread.join()
方法,或者调用了一些阻塞方法时会进入等待状态。 - 超时等待状态(TIMED_WAITING): 类似于等待状态,但是在指定的时间内会自动返回。线程可能因为调用了带有超时参数的
Thread.sleep()
、Object.wait(timeout)
方法,或者调用了带有超时参数的Thread.join(timeout)
方法时会进入超时等待状态。 - 终止状态(TERMINATED): 当线程执行完毕或者因为异常退出时,处于终止状态。线程对象的生命周期结束,线程被销毁,不再执行任何任务。
3.线程和进程的区别
线程和进程是操作系统中管理和调度的两个基本概念,它们之间有着明显的区别:
- 定义:
- 进程是程序执行时的一个实例。它包含了程序代码、数据以及进程的执行环境。
- 线程是进程中的一个执行单元,是程序执行流的最小单元,一个进程可以包含多个线程。
- 资源分配:
- 进程是系统分配资源的基本单位。每个进程拥有独立的内存空间、文件句柄等资源。
- 线程是进程内的资源共享单位。同一进程内的所有线程共享相同的内存空间和其他资源。
- 并发性:
- 进程是独立运行的程序,不同进程之间相互独立,彼此不受影响。
- 线程是进程内的执行流,同一进程内的多个线程共享进程的资源,可以同时执行并且可以共享数据。
- 开销:
- 进程之间切换需要的开销较大,因为它们拥有独立的地址空间和资源。
- 线程之间切换的开销相对较小,因为它们共享同一进程的地址空间和资源。
- 通信与同步:
- 进程之间通信需要采用特定的通信机制,如管道、消息队列、共享内存等。
- 线程之间通信可以直接读写共享数据,但需要注意同步问题,防止数据竞争和死锁。
- 稳定性:
- 进程之间的错误不会相互影响,一个进程的崩溃不会影响其他进程的稳定性。
- 线程之间共享同一进程的资源,一个线程的错误可能会导致整个进程崩溃。
- 创建和销毁:
- 创建和销毁进程的开销较大,需要分配和释放大量的系统资源。
- 创建和销毁线程的开销相对较小,因为它们共享进程的资源,只需分配和释放少量的内存空间即可。