JavaEE之多线程(进程和线程)详解

😽博主CSDN主页:小源_😽

🖋️个人专栏: JavaEE

😀努力追逐大佬们的步伐~


目录

[1. 前言(操作系统的介绍)](#1. 前言(操作系统的介绍))

[2. 进程](#2. 进程)

[2.1 什么是进程/任务 (Process/Task)](#2.1 什么是进程/任务 (Process/Task))

[2.2 操作系统的进程管理](#2.2 操作系统的进程管理)

[2.2.1 先描述](#2.2.1 先描述)

[2.2.2 再组织](#2.2.2 再组织)

[2.2 进程的内存管理 (Memory Manage)](#2.2 进程的内存管理 (Memory Manage))

[2.3 进程间通信 (Inter Process Communication)](#2.3 进程间通信 (Inter Process Communication))

3.线程

[3.1 什么是线程](#3.1 什么是线程)

[3.2 线程是怎么做到的](#3.2 线程是怎么做到的)

4.小结


1. 前言(操作系统的介绍)

我们知道, 操作系统(Operating System)是一个软件(由代码构成程序)
操作系统有两个主要职责:

  1. 管理各种硬件设备
  2. 给其他软件提供稳定的运行环境
    现代的操作系统有: 1.Windows 2.Linux (程序猿必须要掌握的系统) 3.Mac 4. IOS 5. Android (鸿蒙系统是安卓的换皮系统)

    一个电脑,有很多的硬件,比如 显示器,鼠标,键盘,音箱,摄像头,麦克风,内存,硬盘,显卡,电源,风扇......
    市面上,每一种硬件,都有很多厂商来生产,不同厂商生产的硬件,就会存在差异.即使同一个厂商,不同型号,也会存在差异.
    假如现在要写一个程序,响应鼠标的各种操作.市面上的鼠标种类繁多,总不能给各种不同的鼠标,写不同的代码吧??
    此时,就需要操作系统站出来,统一管理各种不同的硬件设备,给软件提供统一的 api (Application Programming Interface, 即应用程序编程接口)
    (这点和JDBC(Java DataBase Connectivity, 即Java数据库连接)非常相似,JDBC的本质就是,同一套java代码,可以操作不同类型的关系型数据库)
    此时, 程序猿写代码的时候,就可以不用关注硬件的细节差别了,只需调用操作系统的api即可,然后操作系统去控制不同的硬件进行工作,因此,咱们程序猿写代码,不需要面向硬件,只需要面对操作系统即可,硬件非常非常多,而操作系统,就那几个主流的
    然后,我们也知道,JVM (Java Virtual Machine,即Java虚拟机) 又是对系统的抽象封装,
    于是, 最终的结果,咱们java程序猿只需要使用JVM提供的api就可以起到控制不同的操作系统,完成编程的效果了

本章重点:

本文着重讲解了进程和线程的区别和联系


2. 进程

2.1 什么是进程/任务 (Process/Task)

进程就是操作系统提供的一种"软件资源" .我们常用的操作系统,都属于"多任务操作系统"(即同一时刻,可以同时运行多个任务), 比如画图板,浏览器,QQ音乐,系统后台......,这些正在运行的程序,就可以称为"进程" / "任务"

以前的山寨机和早期的诺基亚就是单任务操作系统(没有"后台执行",想要执行另一个程序,必须先把前一个程序退出)


每个任务在执行的时候,都要消耗一定的硬件资源,换而言之,计算机中的每个进程在运行时都需要给它分配一定的系统资源, 进程是系统分配资源的基本单位


2.2 操作系统的进程管理

2.2.1 先描述

使用类/结构体这样的方式,把实体属性列出来,表示进程的结构体,称为PCB(Process Control Block, 即进程控制块)
PCB的重要属性(PCB这个结构体,是一个具有上百个属性的结构体,非常庞大):

  1. pid , 这是进程的身份标识 ,通过一个简单的不重复的整数来进行区分,并且系统保证在同一个机器上同一时刻,每个进程的pid都是唯一的(后续如果针对某个进程进行操作,就可以使用pid来进行区分了) 如果点击结束某个进程,此时,任务管理器就会获取到你选中的pid,然后调用一个系统pid,把pid作为参数穿进去,从而完成杀死进程
  2. 内存指针 (一组),描述了进程都能使用哪些内存. 进程运行过程中,需要消耗一些系统资源,其中内存就是一种重要的资源. 如果把内存想象成一个小旅馆,有很多房间,申请内存就是给旅馆老板说开个房子,申请到了才能使用. 每个进程必须使用自己申请到的内存(一亩三分地). 一个进程跑起来的时候,需要有"指令"也需要有"数据"(指令和数据,都需要加载到内存中),进程也需要知道指令和数据各在哪里 我们做双击exe文件的时候,就会运行"进程",系统先把exe文件里的指令和数据加载到内存中) ,然后再创建进程并开始执行
  3. 文件描述符表 , 描述了进程所涉及的硬盘相关的资源,我们的进程经常要访问硬盘(硬盘,软盘(现在已经不存在),光盘,U盘(flash卡)...... ),所以操作系统对硬盘这样的硬件设备封装成了文件,进行统一的抽象,都是按照"文件"的形式进行操作的. 一个进程想要操作文件,需要先打开文件(就是让你的进程在文件描述符表中分配一个表项(构造一个结构体),用来表示这个文件的相关信息) Java标准库(Java Standard Library)是Java语言提供的一套基本的API库,它包含了大量的类和接口,用于支持Java程序的开发和运行。Java标准库封装的 api,本质上都是调用系统 api

内存,硬盘,网卡等资源在pcb中都容易体现,但是,一个进程消耗CPU资源是什么意思??

如果把CPU比作一个舞台,那么进程要执行的指令就是演员,一个CPU可能有一个核心,也可能有多个核心(每个核心都是一个舞台),演员需要登上舞台,才能表演,我们规定,同一时刻,一个舞台上,只能有一个演员,现在我们的电脑的CPU核心都不止一个,例如我这个电脑只有12个逻辑核心,但是我系统的进程远远超过了12个,导致"狼多肉少",怎么办呢?

所以我们提出一个非常重要的概念,分时复用(并发),就是说让多个演员,轮流登台

2007年以前的CPU都是单核的,但是早就有象 windows xp这样的多任务操作系统了,用到的就是分时复用(并发)

如果CPU核心只有一个,先执行进程1的代码(进程1先登台演出),然后让进程2登台...以此类推,只要切换的足够快,我们人就感知不到了(但是极端情况下,系统进程太多,负担太重,就会出现"卡顿")

后面随着多核CPU的诞生,我们就相当于有好几个舞台,如果我们现在有四个不同的进程,此时此刻,微观上,这四个进程也是"同时执行"的,我们把这个状态叫做"并行执行",并且前面的并发执行仍然存在,总之,这四个"舞台"同时表演,并且各个舞台上的演员轮流登台

我们现代的计算机的执行过程,往往是并发+并行执行同时存在的,但是如果有两个进程,这两个进程是并发还是并行执行取决于系统如何调度(取决于系统调度器模块的实现,我们作为程序猿是干预不了的,并且感知不到)

因此,我们往往把并发和并行统称为"并发",对应的编程方式(解决一个问题,同时搞多个任务来执行,共同协作解决)就称为"并发编程"

PCB中就要提供一些属性,来支持系统完成对这些进程的调度

PCB支持进程调度的属性

4.状态,描述某个进程,是否能够去 cpu上执行. 有的时候,某个进程可能"不太方便",比如某个进程,通过Scanner等待用户输入内容,但是用户什么时候输入,完全是一件不可控的事情,所以进程就分为了两种状态:就绪状态(随时准备去CPU上执行,操作系统"一打招呼",就上了)和阻塞状态(不方便去CPU上执行,不应该去调度他)

5.优先级, 比如多个进程都在等待系统调度,先后关系肯定不是平均的,先调度谁,后调度谁,谁的执行时间长,谁的执行时间短,都是可以通过系统api进行调配的,比如你的电脑上同时打开吃鸡(PUBG)和qq,肯定是吃鸡的优先级更高,反而qq可以在后台运行,收到的消息你没有必要立马去看

6.记账信息, 针对每个进程,占用了多少cpu时间进行统计,根据这个统计结果进一步调整进程调度的策略,在下一轮调度的时候尽可能的避免进程捞不到CPU的情况

7.上下文,是支撑进程调度的重要属性,相当于游戏中的存档和读档

存档:进程在调度出CPU之前,把当前寄存器中的数据,单独保存到内存中(PCB的上下文属性中)

读档:该进程下次再去CPU执行的时候,再把这些寄存器的数据,加载到CPU的对应寄存器中

每个进程在运行过程中,会有很多的中间结果,在CPU的寄存器中

比如:想用寄存器,保存 3 和 14 ,然后再用寄存器保存 17,但是如果3和14在保存17前被调度走了,但是后面再被调度回CPU的时候就可以继续计算并保存17,

因为:操作系统调度进程,过程可以认为是"随机"的,任何一个进程,代码执行到任何一条指令的时候,都可能被调度出CPU,在进程下次调度回CPU的时候,就会继续之前的进度来执行


2.2.2 再组织

使用一定的数据结构,把这些结构体/对象串到一起,比如在 Linux 中,使用链表这样的数据结构把若干个 task_struct 给串起来

当我们看到任务管理器中的进程的时候,意味着系统内部就在遍历链表,并且打印每个节点的相关信息(比如cpu利用率,占用的物理内存,磁盘的利用率,网络利用率等)

如果运行一个新的程序,系统中就会多一个进程,这个多的进程就需要构造一个新的PCB,并且添加到链表上

如果某个运行的程序退出了,就需要把这个进程的PCB从链表中删除,并销毁对应的PCB资源

注意:上述是简化版本(因为咱们java程序猿不需要了解太多的细节),事实上进程的组织方式会更复杂(不是一个链表,而是更加复杂的链式结构)


2.2 进程的内存管理 (Memory Manage)

进程如何管理内存,其实是一个非常非常复杂的问题,我们就不详细讨论了.
但是我们给出一个核心结论: 每个进程的内存,是彼此独立的,互不干扰的.
通常情况下,进程A 不能直接访问B 的内存.
这是因为为了系统的稳定性,如果某个进程代码出先bug(比如内存越界了), 出错影响的范围只是影响了自己的进程,不会影响到其他进程.这个情况,也称为"进程独立性"


2.3 进程间通信 (Inter Process Communication)

虽然上面提到有进程的独立性,但是有时候也需要多个进程相互配合完成某个工作.

我们需要注意进程间通信和进程的独立性并不冲突

这是因为系统提供了一些公共的空间(多个进程都能访问到) ,让两个进程通过这种公共空间来交互数据

操作系统提供进程间通信的具体方式其实有很多种,但本质都是上述思路

文件和网络是我们Java程序猿主要使用的进程间通信方式.其中网络可以支持同一个主机的不同进程,也能支持不同主机的不同进程(适用性更高),我们Java后端,很可能是一组服务器之间进行通信


但是实际上,在Java中不太鼓励进行"多进程编程",所以,线程就更加重要了

因为多任务操作系统,希望系统能够同时运行多个程序,进程可以很好解决"并发编程"这样的问题,但是在一些特定的情况下,进程的表现就不尽人意.

比如,需要频繁的创建和销毁的时候,如果使用多进程编程系统开销就会很大,最关键的原因就在于资源的申请和释放,因为进程是资源分配的基本单位,一个进程刚刚启动的时候,首先就是需要把依赖的代码和数据,从磁盘加载到内存中

我们这里需要知道的是,从系统分配一个内存并非是一件容易的事情.一般来说,申请内存的时候,需要指定一个大小.系统内部是把各种大小的空闲内存,通过一定的数据结构组织起来的

实际申请的时候就需要去这样的空间中进行查找找到一个大小合适的空闲内存,分配过来

结论:进程在进行频繁创建和销毁的时候,开销比较大


3.线程

3.1 什么是线程

线程,就是解决上述问题的方案,线程也可以叫**"轻量级进程''**,在进程的基础上,做出了改进:

保持了独立调度执行这样的"并发支持",同时省去了"分配资源""释放资源"带来的开销


3.2 线程是怎么做到的

前面介绍了使用PCB来描述一个进程,现在也用PCB来描述一个线程

这个图中,是一个进程有一个PCB,但实际上,一个进程可以有多个PCB,意味着一个线程包含了一个线程组(多个线程)

操作系统进行"多任务调度"的时候,本质上是在调度PCB

线程在系统中的调度规则,跟之前进程的是一样的(线程的PCB中也有状态,优先级,上下文,记账信息等)

上面我们说到PCB中有一个属性,叫做内存指针,多个线程的PCB的内存指针,指向的是同一个内存空间,意味着只是创建第一个线程的时候需要从系统分配资源,后面的线程直接共用前面的那份资源就可以了

除了内存,文件描述符表也是多个线程共用一份的

但是不是随便两个线程就能资源共享,我们把能够共享资源的这些线程分为组,称为"线程组",线程组也就是进程的一部分

上图中的代码,包含了所有线程依赖的所有数据和代码.这些线程可能是各取所需,也有可能有一定的共用

在没有线程时,进程需要扮演两个角色(资源分配的基本单位, 调度执行的基本单位)

在有了线程以后,进程只需要专注于资源分配,线程负责调度执行(创建一个进程,同时资源就分配了,同时一个主线程就出来了

因为一个进程至少要包含一个线程,所以创建一个线程,进程也就出来了,资源同时也分配了)

比如:

一个房间内一个桌子一个人吃100只鸡的问题,为了提高吃鸡的效率,用多进程的方案就是变成两个房间两个人各吃50只鸡,但是新创建了一个房间(即创建了新的进程,需要申请更多的资源)

用多线程的方案就是一个房间内变成两个人,仍然能够提高吃鸡的效率,但这种方法消耗的系统资源更小

但是一个房间内并不是引入的人越多越好,即引入的线程是有一定的上限,达到一定数量后,再继续引入新的线程,就没办法提升效率了,因为线程之间会开始相互竞争CPU的资源(CPU的逻辑核心数是有限的),这个情况下,非但不会提升吃鸡的效率,反而会增加调度的开销

比如说两个人同时看上了一个鸡大腿,但是谁能抢到就不一定了,即线程之间起了冲突,就可能导致代码出现一些逻辑上的错误(线程安全问题,这是一个重点,难点,我们后面会提到)

并且多线程中的共享资源有副作用,就是一个线程如果抛出异常但没有处理好时就可能导致整个进程被终止


4.小结

  1. 进程包含线程
  2. 每个线程也是一个独立的执行流,可以执行一些代码,并且单独参与到cpu调度中(状态, 上下文, 优先级, 记账信息,每个线程有自己的一份)
  3. 每个进程有自己的资源,进程中的线程共用这一份资源(内存空间和文件描述符表)
  4. 进程是资源分配 的基本单位,线程是调度执行的基本单位
  5. 进程和进程之间不会互相影响.但如果一个进程中的某个线程抛出异常,可能会影响到其他线程,会把整个进程中的所有线程都异常终止
  6. 同一个进程中的线程可能会互相干扰,引起线程安全问题
  7. 线程不是越多越好,要足够合适,如果线程太多了,调度开销可能会非常明显
相关推荐
农民小飞侠5 分钟前
python AutoGen接入开源模型xLAM-7b-fc-r,测试function calling的功能
开发语言·python
指尖流烟7 分钟前
C#调用图表的使用方法
开发语言·c#
敲代码不忘补水10 分钟前
Python 项目实践:简单的计算器
开发语言·python·json·项目实践
蒟蒻的贤19 分钟前
Web APIs 第二天
开发语言·前端·javascript
ljp_nan26 分钟前
QT --- 初识QT
开发语言·qt
ᅠᅠᅠ@31 分钟前
异常枚举;
开发语言·javascript·ecmascript
编程版小新37 分钟前
C++初阶:STL详解(四)——vector迭代器失效问题
开发语言·c++·迭代器·vector·迭代器失效
陈大爷(有低保)1 小时前
UDP Socket聊天室(Java)
java·网络协议·udp
c4fx1 小时前
Delphi5利用DLL实现窗体的重用
开发语言·delphi·dll
kinlon.liu1 小时前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证