<续 上一篇博客,写到的PCB中的重要属性>
1.pid,进程的标识符
2.内存指针,进程使用的内存在哪里,哪个部分放代码/指令,哪个部分放数据。
3.文件描述符表,进程使用的硬盘的相关信息
支持进程调度的属性:
4.状态
5.优先级
6.记账信息
7.上下文(是PCB中的数据结构,相当于是在内存上的)
注意:2~7,都是数据结构,不是简单的int变量之类的
【上下文】是支持进程调度的重要属性,相当于 游戏 中的 存档 和 读档。
关于 原理 & 原因 的解释:
每个进程在运行的过程中,就会有很多的中间结果,在CPU的寄存器中
操作系统调度进程,过程可以认为是“随机”的。
任何一个进程,代码直行到任何一条指令的时候,都可能被调度处CPU
在进程下次调度回CPU的时候,继续之前的进度来执行。
因此
就需要在进程调度出CPU之前,把当前寄存器中的这些信息,给单独保存到一个地方。——存档
在该进程下次再去CPU上执行的时候,再把这些寄存器的信息给恢复回来。——读档
举例,在计算 “3+14” 的过程中,
a.先使用寄存器,保存 3 和 14
b.调度走了
c.……
d.调度回来
e.然后再使用寄存器,保存17
总结:
所谓的“保存上下文”,就是把CPU的关键寄存器中的数据,保存到 内存 中 (PCB的上下文属性中);
所谓的“回复上下文”,就是把内存中的关键寄存器中的数据,加载到CPU的对应寄存器中。
内存分配——内存管理(Memory Manage)
核心结论:每个进程的内存,是彼此独立的,互不干扰的。
进程如何管理内存,其实是一个非常复杂的事情 ~ ~ 咱们就不详细讨论了 ~ ~
通常情况下,进程A不能直接访问进程B 的内存。(共享内存,但是共享内存也是有限制的,不是所有的内存空间都能共享)
这样,“不能直接访问”的机制,有益于系统的稳定性。如果某个进程代码出BUG(比如内存写越界),出错影响的范围,只是影响到自己这个进程,不会影响到其他进程。(如果系统上的一个进程崩溃,影响了其他进程,会是以一种非常糟糕的体验)
这个情况,也称为“进程独立性”
ps:虚拟内存,是进程独立性 的 底层支撑。也能对于内存不够 / 分时复用 起到一定的支持 ~ ~
进程间通信(Inter Process Communication)
系统提供一些 公共的空间(多个进程都能访问到的),让两个进程借助这种公共空间来交互数据。(操作系统提供的“进程间通信”具体方式,其实有很多种,本质都是上述思路)
——多个进程互相配合,完成某个工作。
虽然有进程的独立性,但是有时候也需要,多个进程相互配合,完成某个工作。
进程间通信,和 进程的“独立性”,并不冲突
【进程间通信方式-举例】
1.管道
2.共享内存
3.文件
4.网络
3、4 是Java程序猿主要使用的进程间通信方式。
网络,是可以支持同一个主机的不同进程,也能支持不同主机的不同进程(适用性更高 ~ ~)
后端这里,很可能是一组服务器,这一组服务器之间,进行通信 ~ ~
后端和前端进行交互,也需要 ~ ~
5.信号量
6.信号
!!!注意:上述对于进程的讨论,只是序幕,实际上,在Java中,不太鼓励“多进程编程”
线程,更加重要了
进程 && 线程
——本质上来说,进程解决“并发编程”这样的问题。
多任务操作系统,希望系统能够同时运行多个程序 ~
如果是单任务都系统,则完全不涉及进程,也不需要管理进程,更不需要调度进程……
事实上,进程,是可以很好的解决并发编程的问题的。(有些操作系统,上面就只有进程这个概念)
但是,在一些特定的场景下,需要频繁的创建和销毁进程的时候,此时使用多进程编程,系统开销就会很大
原因:
早期编写一个服务器程序——
互联网发展的早期,“网站/web开发”这件事开始出现了,最早的web开发(比PHP更早),是使用C语言来写的服务器程序(基于一种CGI 这样的技术——一种基于多进程的编程模式)
服务器同一时刻会收到很多请求
针对每个请求,都会创建出一个进程,给这个请求提供一定的服务,返回对应的响应。一旦这个请求处理完了,此时这个进程就要销毁了。
如果请求很多,意味着你的服务器就需要不停的创建新的进程,也不停地销毁旧的进程。
频繁的创建和释放,这样的操作,开销是比较大的!!!(其中最关键的原因,是资源的申请和释放,因为进程是资源(CPU,硬盘,内存,网络带宽……)分配的基本单位,一个进程,刚刚启动的时候,首当其冲,就是内存资源,进程需要把依赖的代码和数据从磁盘加载到内存中,,,从系统分配一个内存,并非是一件容易的事情!!)
一般来说,申请内存的时候,需要指定一个大小,系统内部就把各种大小的空闲内存,通过一定的数据结构,给组织起来了。实际申请的时候,就需要去这样的空间中进行查找。找到个大小合适的空闲内存,分配过来
提问:
如果有一个很大的进程,照顾到这么大的内存咋办???
答:
系统就会报错呗~就启动不了了
(这一点在Windows上不明显,在Linux上是更明显的)
进程在调度的时候是“分时复用”的,
进程消耗的内存,也是在内存空间上“分时复用”的
当前没在运行的进程,所消耗的内存空间,可以暂时不必真正放到“内存上”,可以暂时放到硬盘的 特定区域(swap空间)
等进程真正执行的时候,再把这些内存数据装载进去
这样就可以有效的保证正在运行的进程内存比较充裕 ~ ~
平时感知到的是系统越来越卡,
很少会直接内存不足直接报错
Linux上就不是了,Linux上swap可以配置,如果没有配置swap,并且运行消耗内存很大的程序,就容易出现这样的问题
结论:
进程在进行频繁创建和销毁的时候,开销比较大(开销主要体现在 资源的申请 和 释放 上)
【线程】就是解决上述问题的方案
线程——也可以称为“轻量级进程”,在进程的基础上,做出了改进。
保持了独立调度执行,这样的“并发支持”,同时省去“分配资源”“释放资源”带来的额外开销。
问:线程是如何做到的呢?(针对上述改进)
前面介绍了,会使用PCB来描述一个进程,现在,也使用PCB来描述一个线程。
上图需注意的地方:
线程的pcb中,用于调度的属性,还是单独有各自的,共享资源相关的属性就是共用的了~
倒也不是随便两个线程,就能资源共享。
把能够资源共享的这些线程,分成组,称为“线程组”
再换句话讲,线程组,也就是进程的一部分 ~ ~
即:
线程—(多个 、能共享资源的)—>线程组—>进程的一部分
进程和线程的关系:
上图中,共享的这部分内存空间,包含了所有线程依赖的所有的数据和代码。这些线程可能是各取所需,也可能是有一定的公共的 ~ ~
有线程之前:
进程需要扮演两个角色:资源分配的基本单位、调度执行的 基本单位
有了线程之后,两个角色就分开了:
进程专注于 资源分配了(创建进程,资源就分配了。只不过,一个进程中至少要包含一个线程 ~ ~)
线程负责 调度执行了。
创建第一个线程的同时,进程也就出来了
问题:是不是第一个线程申请的空间要足够大,才能容纳后面的线程数据?
答:也不一定非得足不足够大,程序运行过程中,还可以再申请,不是一锤子买卖。
关于进程刚和线程,举个例子:
不举例子了
【小结】进程和线程的关系/区别,非常经典,非常高频的面试题!!(注意:这种经典面试题,给出的回答,都不要可以去背,而是要写博客,用自己的话来总结表述)
1.进程是包含线程的
2.每个线程,也是一个独立的执行流,可以执行一个代码,并且单独的参与到CPU调度中。(状态、上下文、优先级、记账信息,每个线程有自己的一份)
3.每个进程有自己的资源,进程中的线程共用这一份资源(内存空间 和 文件描述符表)
由“2/3”=》进程是资源分配的基本单位,线程是调度执行的基本单位
4.进程和进程之间,不会互相影响。如果同一个进程中的某个线程,抛出异常,是可能会影响到其他进程,会把整个进程中的所有线程都异常终止。(掀桌)
5.同一个进程中的线程之间,可能会互相干扰,引起线程安全问题。
6.线程也不是越多越好,要能够合适,若果线程太多了,调度开销可能非常明显。
多线程编程——介绍&用法
写代码的时候,可以使用多进程进行并发编程,也可以使用多线程进行并发编程。
[多进程并发编程] 在Java中,不太对推荐,因为,很多和多进程编程相关的api,在Java标准库中,都没有提供。
(另外,如果需要频繁的创刊销毁,那么多线程在并发编程的时候,效率更高。尤其是对于Java进程,是要启动Java虚拟机——JVM的,这个事情的开销,更大,每个进程都要对应一个各自的Java虚拟机,每个独立的进程都会启动一个单独的 JVM,一个Java程序可以对应多个进程,然后一个进程又可以包含多个线程,一个 Java 程序默认情况下运行在一个 JVM 进程中,详细可以看一下这个:https://juejin.cn/post/6844903881063792647)
[多线程并发编程]系统提供了多线程的api,Java标准库,把这些api封装了,在代码中就可以使用了。
【使用】
Java提供的api,Thread 这样的类 ~ ~
原因:
Java标准库中,有一个特殊的包,java.long
这个包中的类,不需要导包。比如:String,Thread
1.没有被public修饰的类,包级作用域,就是只能在当前包里被其他的类使用
2.一个.java文件中,只能有一个public的类 ~ ~
1.run()方法,就类似于main方法,是一个java进程(程序)的入口方法,是一样的道理
一个进程中,至少会有一个线程,
这个进程中的第一个线程,也就称为“主线程”
main方法,也就是主线程的入口方法。
2.run()方法的特性
此处的run()方法,不需要程序员手动调用,会在合适的时机(线程创建好了之后),被jvm自动调用。这种风格的函数(不需要手动调用,会自动调用的函数),称为——回调函数(callback)
3.回调函数,是编程中非常重要的概念
(1)C进阶——指针进阶:函数指针a.实现转移表,降低圈复杂度(写个计算器)b.作为回调函数(自己实现qsort)(泛型编程 就是为了解决void的问题的)
(2)Java数据结构——优先级队列PriorityQueue:在使用的时候,不是随便一个对对象就能插入其中的,需要指定比较规则(能进行比较的对象) ~ ~
a.Comparable b.Comparator
其中,compareTo 和 compare,这两个方法,就属于是“回调函数”
4.方法重写——override
本质上是让你能够对现有的类,进行扩展
一句话:重写现有的类中的run()方法,以满足自己的业务需求。
注意:
不写override注解,也能实现重写。但是,有注解,可以起到让代码进行自动检查的作用,避免参数写错 而 导致 没有构成重写。(报错,让错误更容易被发现)
类似功能的还有:final、throws
注意:
创建继承于Thread类的子类、创建对应类的对象,都不是执行run()方法的开始。
//根据创建好的Mythread类,创建对象
//MyThread t = new MyThread();
Thread t = new MyThread();//向上转型
//调用 Thread 的 start 方法,才会真正调用系统的api,在系统内核中创建出线程。(线程就会执行上面写好的run方法了)
t.start();
当引入多线程之后,代码中就可以同时具备多个执行流了!!!
——用专业术语