-
前言
在前一篇文章末尾我简单介绍了操作系统,在操作系统中有一个核心的概念就是进程,然而从本篇文章起,就要开始JAVA语言多线程的讲解了,所以在此之前,本篇文章作为多线程的前序铺垫,一是介绍进程与线程的相关概念,二是区分进程与线程,三是解释为什么JAVA语言中不推荐使用多进程的方式进行编程而是更多使用多线程式编程,下面开始今天的知识分享吧!!
一、进程的相关概念
1. 什么是进程
我们现在所用的系统都是“多任务操作系统”,同一时刻,可以同时运行多个任务,这些正在运行的程序,就可以成为是“任务”,也叫做“进程”,进程包含了一个程序的所有状态信息,但是进程和程序还是有一些区别的,程序是静止的,可以看作是代码+数据,而进程是动态的,可以看作是程序+运行的上下文,进程具有动态性,独立性和并发性(宏观上,各进程同时运行),如下图所示,就是我电脑上正在运行的程序:
从上图中可以看到,每个任务(进程)在执行的过程中,都需要消耗一定的硬件资源,换而言之,可以认为,计算机中每个进程在运行的时候都需要给他分配一定的系统资源, 这就表明了,在操作系统内部中,进程是操作系统进行资源分配的基本单位。
2. 进程控制块
进程作为操作系统的核心概念,那么操作系统是如何管理进程的呢?其实操作系统在进行管理进程的时候跟学校管理学生或者公司管理员工的基本思路都是一样的,都是先描述,再组织,以学校管理学生为例,学校首先要把一个学生的各种属性表示出来,再使用一定的数据结构把所有的学生信息整理到一起,操作系统也是一样,先把实体属性列出来,由于操作系统一般是用C/C++实现的,所以一般使用的都是结构体来表示进程信息,在操作系统中,把这个用来描述进程实体属性的结构体称为进程控制块(PCB),PCB(Process Control Block)是操作系统学科中的通用概念,其中包含了上百个属性,下面我将选择几个核心属性进行介绍。
①PID进程标识符
PID是进程的身份标识,这里是通过一个简单的不重复的整数来进行区分的,系统会保证在同一个机器上,同一时刻,每个进程的PID都是唯一的,如下图所示:
在这里我们选择一个进程,并且点击结束任务,此时,就是任务管理器获取到你当前选中进程的PID然后调用系统的API,把PID作为参数传过去,从而完成这里杀死进程的操作。
②内存指针
内存指针描述了进程使用内存资源的详细情况,进程运行过程中,需要消耗一些系统资源,其中内存就是一种重要的资源,整个系统中内存有很多,但是这些内存不是可以随意使用的,进程需要先从系统这里申请,然后系统给进程分配一块内存,进程才可以使用,每个进程都必须使用自己申请到的内存,内存指针就是用来描述当前这个进程都能使用哪些内存,一个进程在运行的时候需要有“指令”也需要有“数据”,指令和数据都是加载到内存中的,进程也需要知道自己的内存中哪里存的是指令,哪里存的是数据,这也需要依靠内存指针。
③文件描述符表
文件描述符表描述了这个进程所涉及的硬盘相关的资源,我们的进程经常要访问硬盘,操作系统对于硬盘这样的设备进行了封装,也就是我们电脑上的文件,在上篇文章介绍了冯诺依曼体系结构,里面的内存器=内存+外存(硬盘,U盘,光盘……),操作系统把外存都进行统一的抽象,都是按照“文件”的方式来进行操作,一个进程想要操作文件,就需要先“打开文件”,就是让进程文件描述符表中分配一个表项(构造一个结构体)表示这个文件的相关信息,打开文件这种操作学过C语言都知道需要使用fopen这样的标准函数,在操作系统中提供的API中(Linux为例)打开文件的函数就叫open。
④进程状态
进程状态用来描述某个进程是否能够去CPU上执行,有时候某个进程此时不方便到CPU上执行,例如这个进程正在通过Scanner等待用户输入内容,用户什么时候输入是一个完全不可控的事情,这时候就需要用进程状态来告诉CPU此时进程是否可以去执行,如下图所示,是进程的三种基本状态,每个进程在某一时刻只能处于其中一种状态,各个状态的转换关系也如图所示:
下面简单介绍一下这三种基本状态:
- 运行状态(Running):已经在CPU上执行了;
- 就绪状态(Ready):随时准备好到CPU上执行,操作系统一打招呼就上了;
- 阻塞状态(Blocked):这个进程当前不方便到CPU上执行,不应该调度它(比如,此时进程正在等待IO,来自控制台的输入输出/硬盘的输入输出/网卡的输入输出)。
⑤进程优先级
在多个进程等待系统调度时,多个进程之间调度的先后关系,调度时间都不是那么平均,这些都是可以通过系统API进行调配的,比如在电脑上运行吃鸡和qq,此时吃鸡的优先级会更高,因为qq信息晚1s收到没什么事,但是游戏延迟1s可是会造成非常糟糕的体验。
⑥记账信息
记账信息会针对每个进程占据了多少CPU时间进行一个统计,会根据这个统计结果来进一步调整进程调度的策略,确保每个进程都不至于出现完全捞不到CPU的情况。
⑦上下文
上下文是用来支撑进程调度的重要属性,相当于游戏中的存档与读档,在每个进程运行的过程中会有很多的中间结果,这些中间结果会保存在CPU的寄存器中,上篇文章中以计算3+14为例介绍了指令执行的过程,CPU会先使用寄存器来保存3和14,然后再使用寄存器保存17,假如在寄存器保存完3和14时,这个进程被调度走了,那么在这个进程重新回到CPU上执行时需要将之前在CPU上执行时关键寄存器的数据加载回来,这就需要我们的上下文进行帮助。
操作系统调度进程的过程可以认为是“随机”的,任何一个进程,代码执行到任何一条指令时,都可能被调度出CPU,此时,就需要在进程调度出CPU之前把当前寄存器中的这些信息给单独保存到一个地方,这也就是存档;在该进程下次再到CPU上执行的时候再把这些寄存器的信息给恢复回来,这也就是读档。
所谓的“保存上下文”就是把CPU的关键寄存器中的数据,保存到内存中(PCB的上下文属性中);
所谓的“恢复上下文”就是把内存中(PCB的上下文属性中)的关键寄存器中的数据,加载到CPU的对应寄存器中。
进程控制块将进程描述出来后,再将其组织起来用的是一种类似于链表更复杂的链式结构,在我们任务管理器中看到这些进程时,意味着系统内部在遍历链表,并且打印每个节点的相关信息,如果运行一个新的程序,系统中就会多一个进程,多的这个进程就需要构造出一个新的PCB,并添加到链表上,如果某个运行中的程序退出了,就需要把对应进程的PCB从链表中删除掉,并且销毁对应的PCB资源。
3. 进程调度
一个进程,消耗CPU资源,这是如何消耗的呢?CPU可以看作是一个舞台,执行指令的就是演员(进程要执行指令),一个CPU可能就一个核心,也可能有多个核心,每个核心都是一个舞台,演员需要登上舞台才能进行表演,并且同一时刻,一个舞台上只能有一个演员,当前我使用的这台电脑上有16个逻辑核心,但是我系统上运行的进程远远不止16个,如下图所示:
此时,面对这种狼多肉少的情况该怎么做呢? 这就涉及到一个非常关键的概念,分时复用(并发),例如CPU核心只有一个,先执行进程1代码,执行一会儿后,让进程1下来,进程2上;进程2执行一段时间后,再让进程3上,以此类推,只要切换的速度足够快,人是感知不到这个切换的过程的,所以在我们看来,多个进程就是在“同时执行”的。这里每个进程在某一时刻谁在CPU上执行,执行多久,就是进程调度,针对进程的调度有一系列的调度算法,如:先来先服务算法、短作业优先算法和时间片轮转算法等,这里不对这些算法进行展开介绍了,这里主要介绍进程调度是怎么一回事?,为什么需要进程调度?
现在我们电脑基本都是多核CPU,同时执行进程也就变得更加复杂了,假如我们电脑的CPU是一个四核CPU,此时就可以同时有四个不同的进程在各自的舞台上进行执行,微观上,这几个进程也是“同时执行的”而不是靠快速切换模拟的“同时执行”这种执行方式也称为“并行执行”,与之对应上面提到的并发执行,仍然存在,每个核心仍然需要分时复用,仍然要快速切换。当前我们的计算机执行的过程往往是并行+并发同时存在的,至于两个进程是并行执行还是并发执行,都是看系统如何进行调度,因此,我们往往就把“并行”和“并发”统称为“并发”,对应的编程方式,也就称为“并发编程”,有的浏览器在下载文件的时候就会应用并发编程的方式来加快IO效率。
4. 内存管理
进程如何管理内存是一个非常复杂的事情,这里我们只要知道,每个进程的内存是彼此独立互不干扰的,在通常情况下,进程A不能直接访问进程B的内存,这也是为了系统的稳定性,假如某个进程代码出现了bug(比如内存写越界),此时出错的范围只会影响到自己这个进程,不会影响其他进程,这也称为“进程的独立性”。
5. 进程间通信
虽然有进程独立性,但是有的时候也需要多个进程相互配合,完成某个工作,进程间通信和进程的“独立性”并不冲突,系统提供了一些公共的空间(多个进程都能访问到),让两个进程借助这种公共空间来进行数据交互,操作系统为“进程间通信”提供的具体方式,其中有很多种方式的本质思路都是这种思路,⽬前,主流操作系统提供的进程通信机制有如下:
- 管道
- 共享内存
- 文件
- 网络
- 信号量
- 信号
其中文件与网络是Java程序员主要使用的进程间通信方式,网络是可以支持同一个主机的不同进程,也能支持不同主机的不同进程,这种方式适用性更高,在后端,很可能就是一组服务器,这组服务器之间进行通信,文件这种方式,其实就是两个进程都对一个文件进行操作,在这个文件中完成数据的交互。
二、线程的相关概念
1. 线程存在的意义
本质上来说,引入进程是为了解决“并发编程”这样的问题的,事实上,进程也是可以很好的解决并发编程这样的问题,但是,在一些特定的情况下,进程的表现不尽人意,比如在有些场景下,需要频繁的创建和销毁进程的时候,此时使用多进程编程,系统的开销就会很大。在编写服务器程序使用多进程编程时,由于服务器在同一时刻会收到很多请求,针对每个请求都会创建一个进程,给这个请求提供一定的服务,返回对应的响应,一旦这个请求处理完了,此时这个进程就要销毁了,如果请求很多,就意味着,服务器需要不停的创建新的进程,也不停的销毁旧的进程,这就涉及到了进程频繁创建和释放,这样操作开销大的最关键原因就是资源的申请和释放,进程是资源分配的基本单位,一个进程刚刚启动的时候,首当其冲的就是分配内存资源,进程需要把依赖的代码和数据从磁盘中加载到内存中,然而,从系统分配内存并不是一个容易的事,一般来说,申请内存的时候需要指定一个大小,系统内部就会把大小空闲的内存通过一定的数据结构给组织起来,实际申请的时候就需要去这样的空间中进行查找,找到个大小合适的空闲内存分配过来,总而言之,进程在频繁创建和销毁时开销比较大。
为了解决上述问题,线程就登场了。
2. 什么是线程
线程也可以称为“轻量级进程”,它是在进程的基础上做出了改进,线程保持了独立调度执行,这样的“并发支持”,同时省去了“分配资源”,“释放资源”带来的额外开销,那么线程是如何做到这些的呢?在前面介绍了使用PCB(进程控制块)来描述一个进程,在线程这里使用的是TCB(线程控制块)来进行描述一个线程。
在线程控制块中也有状态、优先级、上下文、记账信息等,其中线程在系统中的调度规则和之前的进程是一样的,在PCB中有个属性是内存指针,在TCB中多个线程的内存指针指向的是同一个空间,这就意味着,只有第一个线程创建的时候需要从系统中分配资源,后续创建的线程就不必再进行分配了,直接共用前面的那份资源就可以了,如下图所示是PCB与TCB内存指针的区别:
除了内存之外文件描述符表(操作硬盘)这个东西也是多个线程共用一份的。当然,不是随便搞两个线程就能资源共享,把能够资源共享的这些线程分成组,称为“线程组”,线程组是进程的一部分。
三、进程与线程的关系与区别
1. 进程与线程的关系
通过上述的描述,不难看出线程属于进程的一部分,下面通过画图的方式来进一步展现线程与进程之间的关系,如下图所示:
通过上图可以看出,进程与线程之间是包含关系,进程是包含线程的,每个进程都可以包含一个或多个线程,由于一个进程中至少包含一个线程,所以也可以说在创建第一个线程的同时,进程也就被创建出来了,在进程创建的时候同时进行了资源的分配,分配的资源包含了所有线程依赖的数据和代码,这些线程各取所需,也可能是有一定公共的,申请的空间不够大可以多次进行申请,在资源分配过程里,不是一锤子买卖。
2. 进程与线程的区别
介绍完进程和线程是什么,这里总结一下进程与线程的区别,大致有以下几点:
- 进程是包含线程的;
- 每个线程是一个独立的执行流,可以执行一些代码,并且单独的参与到CPU的调度中(状态,上下文,优先级……这些线程都有自己的一份,在TCB中);
- 每个进程有自己的资源,一个进程中的线程共用这一份资源(内存空间和文件描述符表);
- 进程和进程之间,不会互相影响,但是如果一个进程中某个线程抛出异常,是可能会影响到其他线程的,可能会把整个进程中所有线程都异常终止;
- 同一个进程中的线程之间,可能会互相干扰,引起线程安全问题;
- 线程不是越多越好,要能够合适,如果线程太多,调度的开销可能会非常明显。
这里要着重记住:进程是资源分配的基本单位,线程是调度执行的基本单位。
·结尾
在我们写代码的时候,可以使用多进程进行并发编程,也可以使用多线程进行并发编程,在Java中,不推荐使用多进程进行并发编程,因为很多和多进程编程相关的API在Java标准库中没有提供,而Java标准库中把系统提供的多线程编程的API都进行了封装,在编写Java代码时就可以使用了,还有一点,多线程在并发编程需要频繁创建销毁的时候效率更高,尤其对于Java进程而言,每个Java进程都是要启动Java的虚拟机的,启动Java虚拟机这样的开销会更大,然而搞多个Java进程就是要启动多个Java虚拟机,这样的开销在多数情况不被接受,所以Java更推荐使用多线程进行并发编程,文章到此就要结束了,本篇文章重点介绍了什么是进程,什么是线程,并介绍了他们之间的关系和联系,如果本篇文章对你有帮助,希望能给博主一个三连鼓励一下吧,您的支持就是我最大的动力,我们下篇文章再见吧┏(^0^)┛~~~~