"i need your breath"
作者:Mylvzi
文章主要内容:操作系统,进程与线程
操作系统本质上就是一个"搞管理"的软件
日常生活中经常听到windows操作系统,Linux操作系统,Mac os操作系统,这些操作系统本质上都是一个个用于管理计算机的软件,当然这个软件十分复杂
操作系统的功能主要有两点
说明:给各种软件提供一个稳定的运行环境是指各个软件在运行的过程中互相独立,互不干扰(也就是后面要讲的进程的独立性),一个程序的崩溃并不影响到其他程序的正常运行(总不能你女朋友打进来个电话,你游戏就自动退出吧)
管理各种硬件设备:市面上有许许多多的硬件设备,但是操作系统就那几套,它是如何管理各种硬件设备呢?硬件生产的厂商在生产硬件的时候还会开发硬件对应的驱动程序,操作系统通过这些驱动程序就能管理对应的硬件设备,不同的硬件设备有着不同的驱动程序,但是能保证操作系统通过这些驱动程序进行硬件的管理
使用一张图更清晰的了解操作系统的功能
一个操作系统主要由内核+各种应用程序组成,内核是操作系统最核心的部分
硬件的驱动程序就是在内核中运行的,内核也会给许多的应用程序提供支持
进程(task/process)就是正在进行的程序(必须是已经运行起来的),在我们的电脑上有很多的进程,由系统自带的,有我们自己下载的,可以在任务管理器中观察我们当前运行的进程
进程就是跑起来的程序,程序一旦运行,就要吃cpu资源,占用系统内存
所以,一个进程就对应着一定的系统资源,或者说
进程是系统资源分配的基本单位
一个计算机中,大大小小的进程有很多,如何管理这些线程是一个很大的挑战
这么多进程仅需要进行有效的管理,要管理先要明确管理的对象,再去考虑管理的方式,所以进程的管理分为两步
- 描述:使用PCB(进程控制块)来组织进程的各种属性(明确管理的对象)
- 组织:使用双向链表管理管理PCB
使用PCB描述进程的属性,一个进程可能对应着多个PCB,再通过"双向链表"这种数据结构来管理PCB,也就是管理相应的进程
也就是说:
进程的属性都被PCB(进程控制块)组织起来,PCB中有很多重要的属性
一个进程来说可能由多个PCB组织起来,那就对应多个pid
一个chrome浏览器就有多个pid
如何理解内存指针呢?可以以写博客为例
进程所需要的指令:打开CSDN,点击发布文章,敲文字
进程执行所依赖的数据:学习笔记,同学交流
进程执行过程中产生的临时数据:你对某个点很感兴趣,询问了chatgpt,他额外给你补充了一些知识
在之前的计算机中,我们只有一个cpu,也称作"单核操作系统",同一时刻只能有一个进程运行,要进行其他的进程必须先把上一个进程关闭,那又是如何实现多个进程同时运行呢?其核心思路是:通过快速的轮转,在宏观上实现了"同时运行"
就是只要我足够快,在一个进程结束的时候特别快的运行另一个进程,在宏观上就能实现"同时运行"的效果
同时,现在的计算机大多都是"多核操作系统",也就是含有多个cpu,这是就可以把其他进程转移到其他的cpu上,如果两个进程分别在两个cpu上运行,宏观微观上都是同时运行,称作"并行"
一个cpu通过快速的轮转,实现宏观上的同时运行,称作"并发",但实际上我们并不需要严格区分是并发还是并行,同时使用"并发"来统称
状态就是进程当前的运行情况 常见的有两种情况 就绪状态和阻塞状态 就绪状态分为两种,正在运行和准备运行 阻塞状态就是进程暂时不参与调度 最常见的是等待用户输入密码
系统再分配硬件资源的时候并不是一视同仁的,而是根据进程的优先级去分配资源 优先级高的就优先分配更多的资源 优先级低的就晚分配/少分配资源 就像你在打游戏的时候还看着qq,肯定是希望先保证游戏的流畅运行 qq接收消息的优先级就低 其实优先级也是操作系统为了提高效率的一种体现
就是"存档"保存当前进程执行时的cpu状态(更具体的来说是寄存器的状态)这样做是为了当下次再执行当前进程时能够直接利用上次的"存档" 比如你在打游戏的时候女朋友打电话过来了,你肯定得接是吧,就得先退出游戏 先打完电话再重新进入游戏 在你退出打电话的时候进程就保存了当时的运行状态 这样你打完电话之后也能再次回到离开前的游戏界面
存储进程所持有的cpu资源 以便合理的用于系统资源的调度 通过优先级我们为不同的进程分配了不同权重的硬件资源 但是在某些情况下可能会发生一些极端情况 为某个进程分配了过量的资源 而另一个进程就没有被分配到资源导致无法正常运行 记账信息就是为了解决这个问题 通过记录当前进程所持有的硬件资源 当发现不合理的资源调配时及时做出调整
在早期的操作系统中,程序运行时,系统分配的内存是实打实的"物理内存"
当B进程中存在bug,发生了越界访问,导致在同一块内存中的A进程无法运行,我们希望的是当一个程序崩溃的时候另一个程序不受影响,为了解决这个问题引入了虚拟地址空间
注:虚拟地址空间实现了进程之间的隔离,保证了进程的独立性但是在有些需求下各个进程之间是需要合作的,所以在保证进程之间的隔离性的前提下,我们还要想办法让进程之间能够相互通信,进程间的通信方式有很多,常见的有
这些通信的核心思想都是相同的,即:
借用同一块内存空间实现数据的交互。
就像送外卖,外卖小哥和你是互不接触的,但是你们可以通过快递柜实现信息的传递
多进程出现的初心是为了实现"并发编程",就是为了提高程序运行的效率,如今多核cpu的时代,更加突出"并发编程"的重要性,但是多进程编程也有一个致命的缺点,进程的创建,销毁,调度既缓慢又需要开辟响应的内存空间(进程是资源分配的基本单位),即使进程的管理是通过双向链表这样的数据结构实现的,但是随着进程的增多,进程管理所消耗的时间和内存就不容忽视,为了进一步的提高效率,就引入了"线程"
线程是轻量级进程,线程必须依赖于进程,进程包含线程。
可以理解为线程是对进程内部的进一步优化,将一个完整的进程划分为多个"小线程",从而提高效率
一个进程至少要有一个线程,这个线程可以执行一些代码,进程也可以包含多个线程,让这多个线程各自执行一些代码,从而实现并发编程的效果。
我们之前所说的进程的调度,是基于"一个进程只有一个线程"的前提。实际上"进程的调度"是不准确的,应该是"线程的调度",线程是调度执行的基本单位,也就是说每个PCB对应一个线程,每个线程都有其独自的状态,优先级,上下文,记账信息,但是对于同一个进程内部的线程来说,他们公用同一份pid,内存指针,文件描述符表。
那为什么多线程就比多进程效率更高呢?多进程效率低的原因在于进程的调度需要频繁的进行资源分配,内存开辟,而线程的创建,销毁都不需要进行资源分配或者内存开辟,因为线程使用的是进程的资源和进程的内存,而不需要再去硬盘上获取额外的资源(硬盘的读写是一个非常耗时的操作),从而提高了并发编程的效率
逻辑链:
进程包含线程 => 一个进程包含多个PCB =>每个PCB就用来表示一个线程 =>每个线程都有其专属的状态,优先级,上下文,记账信息 => 每个PCB共用同一个进程的内存空间和文件描述符表 =>每个线程都能独自去cpu上进行调度 =>省去了创建,分配内存的步骤 =>提高效率
进程是资源分配的基本单位
线程是调度执行的基本单位
如果再从宏观的角度来看,一个系统含有多个进程,每个进程都有自己的资源
一个进程包含多个线程,每个线程都能独自进行调度,共用内存指针/文件描述符表
既然线程能够提升效率,那么我们只要在一个进程内部尽可能多的创建新的线程,那效率不就会越来越高么?实际上,任何事情都不能过度,线程数目过多,效率会不增反降。进程也是需要去管理的线程的,当线程数量较少时,管理起来很容易,也能够提高效率,但如果线程的数量越来越多,就变得难以管理了,进程管理的难度就会增加,且更加容易出错(管理3人团队远没有管理100人难),线程与线程之间可能会发生矛盾
如果出现同一进程内的两个线程发生冲突,称为"线程不安全问题",这是多线程的重点,同时,如果某个线程出现问题(抛出异常),如果没有妥善处理,可能会导致整个进程都崩溃(比如某个员工一怒之下删除了所有数据库),其他线程也就随之崩溃(也就是说线程之间不是独立的)
我们要知道,不同的操作系统的进程的调度方式是不同的,他们的cpu存储的指令也是不同的,按理说不同的操作系统对应的多线程的实现方式是不同的,但是Java提供了一套规范"api"将不同的操作系统的线程实现方式统一起来,最后再将其封装,从而实现了"跨平台性"
在Java中通过Thread类实现多线程编程
// 创建一个类 继承于Thread类
class MyThread extends Thread {
@Override
public void run() {
// 线程的入口 告诉线程要执行哪些逻辑
System.out.println("hello thread");
}
}
public class Test {
public static void main(String[] args) {
// 首先要实例化出一个Thread类
Thread thread = new MyThread();
// start和run都是Thread类的成员
// run只是告诉线程要去执行那些逻辑
// start是真正的调用系统的api,创建出一个线程,再让线程去执行run
thread.start();
System.out.println("hello main");
}
}
注意:
1.run方法的作用是告诉你线程的执行入口 进入到run方法内部执行代码 就和一个Java程序的入口是main一样
2.start方法的作用的调用系统的api,创建出一个线程,再让线程去执行run
3.start和run的区别就是有没有创建出一个新的线程
下面修改一下代码,将打印的逻辑都放在while(true)循环之中
// 线程的入口 告诉线程要执行哪些逻辑
while(true) {
System.out.println("hello thread");
}
while (true) {
System.out.println("hello main");
}
执行结果:
从这张图中我们可以看出, 两个while循环在"同时执行" =>实现了并发编程,每一个线程都是一个独立的执行流,独立的执行逻辑;还可以看出,具体执行哪一个线程或者线程执行的次数是不确定的,一是因为线程的调度是具有优先级的,但本质上线程的调度取决于cpu上的调度器的实现
如果将main方法中的start方法改为run方法
此时只会执行run方法内部的代码,原因在于start方法调用了系统的api,开辟了一个新的线程,这个新的线程用于执行"hello main"这个代码;而run方法并不会创建出一个新的线程,他只是"主线程"的入口,他只会执行run方法内部的代码。使用start方法时,相当于开辟了两个线程,一个是run方法,另一个是"hello main"对应的方法
如何查看线程的具体执行情况呢?可以使用jdk中的"jconsole"程序
注意:
若jconsole打开之后什么也不显示,可以使用"以管理员身份"打开
未来写一些多线程的程序时,可以通过jconsole来时刻观察线程的运行状态
让线程的while循环转的慢一点-->在循环体内部使用sleep
// 创建一个类 继承于Thread类
class MyThread extends Thread {
@Override
public void run() {
// 线程的入口 告诉线程要执行哪些逻辑
while(true) {
System.out.println("hello thread");
try {
// 休眠1s
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
// 首先要实例化出一个Thread类
Thread thread = new MyThread();
// start和run都是Thread类的成员
// run只是告诉线程要去执行那些逻辑
// start是真正的调用系统的api,创建出一个线程,再让线程去执行run
thread.start();
// thread.run();
while (true) {
System.out.println("hello main");
// 休眠1s
Thread.sleep(1000);
}
}
}
注意:
1.sleep方法是Thread类的一个类方法,可以直接通过类名来访问
2.sleep方法在使用的过程中会抛出异常,不同的位置处理异常的方式会有所差异
在Java中线程的执行和run方法是紧密相连的,run方法告诉线程需要去执行哪些代码 是线程的入口
对于main方法来说,他自己本身就是一个线程,被称为主线程,也就是说每个Java程序必然含有一个主线程
代码中两个都写了while(true)的循环,如果使用start则两个代码都会被执行 如果使用run则只会执行run方法内部的 另一个代码就不会被执行
我们使用t.start只是为了创建出一个新的线程,在这个新的线程里面去执行我们新的任务
如果两个都是while(true)循环,使用t.start两个代码都会执行,对于自定义的线程来说,因为是while(true)循环,run方法永远没有结束,所以会一直执行,同时主线程中的代码也会执行,系统把"hello tThread"看作一个新的线程
如果都是用while(true)循环,而不使用start方法,打印结果是什么呢?只会执行hello Thread,原因在于如果使用run方法并不会将"hello Thread"看作一个线程,可以理解为就是一个简单的方法调用,只有当调用的方法执行完毕之后会执行main方法中剩余的代码,而这个方法调用反而是死循环的,所以"hello main" 根本不会打印,这就是一个简单的方法调用,是存在先后顺序的,而多线程解决的是"并发编程",是同时执行
最后附上思维导图!!!