1. 为什么要学习多线程?
首先相信各位小伙伴在学习 JavaSE 的时候,肯定写过一些小游戏吧,比如猜数字,关机小程序...但是如果现在要在猜数字小游戏上面加上一个功能,设定20秒没猜中,就判定游戏失败,那么请问,以没学过多线程的角度来看,你要如何增加这样的一个计时功能呢?即一遍玩游戏,一边计时。
上述只是方便各位理解多线程,本质多线程的意义就是能充分利用 CPU 的多核资源,解决并发编程的问题!
现在市面上买到笔记本所配的CPU,比如是八核十六线程,这里是指该 CPU 有八个物理核心,而每个物理核心上又虚拟了两个逻辑核心(超线程技术),对应的就是十六个线程,所以通过设备管理器会显示出十六个核心,一个线程对应一个核心(逻辑核心)。
所以上述说到的十六个线程指CPU最多可以同时有十六个线程处理任务!
2. 进程的认识
2.1 进程与线程的关系
相信大家都有打开过自己电脑的任务管理器,里面可以发现运行着各种软件,我们认为一个跑起来的程序,就是一个进程,进程与线程是包含的关系,一个进程中可以有多个线程!
其实也有很多软件是采用多进程实现并发编程的,比如谷歌浏览器,每打开一个页面就打开了一个进程,为什么可以采取多进程实现并发编程,但是还要使用多线程呢?
这里给各位小伙伴举个例子,相信大家就很容易理解了:
如果说,篮球哥火了,最近篮球的订单量特别大,一个工厂的生产线都已经忙不过来了,于是我的贴心助手给了我两套解决方案:
第一套方案:
这是第一种解决方案,就是让我再租个地,在建一个工厂,搞一套机器设备。
显然呀,这又要去租个新的地了,又要盖工厂,还得有一套新的物流体系,哎呀,这真的是又费钱又费力气呀,于是我摇摇头,这不行,换方案!其实上述这样的方案,就类似于多进程!
第二套方案:
这是第二种解决方案,再原有的工厂内部,再开辟一条生产流水线。
此时场地和物流体系,是不是就可以复用原来场子的呀,也不用再去租场地建厂子了,共用一个场子就好了!很明显第二种解决方案,成本上比第一种小很多!
根据上述的例子,就可以把篮球哥的工厂想象成进程,工厂里的生产流水线想象成线程!
总结:
一个进程最少包含一个线程,也可以包含多个线程,同一个进程里的多个线程之间,共用了同一份资源(主要是 内存 和 文件描述符表) 比如:在线程1 new 的对象,线程2,线程3都可以使用,在线程1打打开的文件,线程2,线程3都可以使用。
接着回答上面的问题,能使用多进程编程,为什么还要用线程?
其实上述举的例子也能清晰说明,创建一个进程开销是很大的!建工厂,搞生产线,弄一套物流...而创建线程只需要在原有的进程里开辟,比如只用在原来工厂多搞一套生产线即可。
销毁进程开销也很大呀!假如篮球哥不火了,订单变小了,只需要一个厂生产了,另一个厂就得销毁掉,还得拆物流,拆生产线,拆工厂,而如果是一个工厂有两套流水线,只需要拆其中一套流水线即可。
对于CPU来说,调度进程开销也比较大(后续讲解)
有了上述的情况线程就应运而生了,线程也叫做轻量级进程,这样的出现,在解决并发编程问题的前提下,让创建,销毁,调度的速度更快!
2.2 进程间如何通信?
针对进程使用的内存空间,进行了 "隔离",即引入了虚拟地址空间,代码里不在直接使用真实的物理地址了,而是使用虚拟的地址,由操作系统和专门的硬件负责进行虚拟地址到物理地址的转换。主要目的是为了防止进程1内存越界,影响到线程2的运行,这里我们简单了解,不深入讨论。
虽然有了进程隔离,但还是会引入新的问题,有些时候,不同的进程之间,需要进行数据的交互,这就涉及到进程之间如何通信呢?
其实是在隔离的基础上,开了个口子,搞一个多个进程都能访问到的 "公共空间",基于这个空间来进行交互数据即可,我们后续主要讲述:基于文件交互,基于网络交互。现在简单了解下即可。
3. 计算机如何描述进程的?
3.1 概述
进程是一个重要的软件资源,是由操作系统内核负责管理的。
使用了结构体(C语言的结构体)来描述了进程的属性,对于这个描述进程属性的结构体有一个名字,叫做 PCB(进程控制块),每个线程也对应一个 PCB!所以一个进程中可以有多个线程,也就是多个 PCB!
如何把这些PCB管理起来呢?用一个类似于双向链表的结构,把多个 PCB 给串到一起。
创建一个进程,本质就是创建一个 PCB,给插入到链表中
销毁一个进程,本质就是把链表上的 PCB 节点给删除掉
通过任务管理器看到的进程列表,本质就是遍历这个 PCB 链表
注意:一个进程里可以有多个线程,每个线程对应一个PCB,但是这些 PCB 并不是完全独立的,而是有共享的部分,比如一个进程里的多个 PCB 之间,pid是一样的(表示这个进程唯一标识),内存指针和文件描述符表也是一样的!
那么现在我们就要了解 PCB 包含了哪些描述进程的特征!
3.2 PCB 是如何描述进程的?
3.2.1 pid
pid 这个是进程里身份唯一的标识符,虽然一个进程里有多个线程,而这每个线程都对应一个 PCB 但是这些 PCB 的pid 都是一样的!用来标识这些线程是属于哪个进程的,也是这个进程的唯一标识符!
3.2.2 内存指针
内存指针,指向了该进程占用的内存是哪些,同理,虽然一个进程里包含多个线程,但是多个线程共享都是进程的资源,也就是利用了同一块内存,所以一个进程中线程对应PCB里内存指针,也是一样的!
3.3.3 文件描述符表
文件描述符表,表示打开的硬盘上的文件等其他资源,比如打开一个 .txt 文件,也就会对应在文件描述符表上新增一个表项(类似于数组的结构),后续学到的网络编程,也会被当成文件,也会在文件描述符表上申请表项,跟详细内容在文件 IO 章节讲解。
3.3.4 进程调度相关属性(下述属性每个线程都有自己的)
线程的状态:
就绪状态:随叫随到,线程随时准备去 CPU 上执行
运行状态:正在 CPU 上执行的,很多操作系统,不会明确区分就绪和运行状态
阻塞状态:短时间内无法到CPU上运行了(后续文章会详细讲解)
优先级:
先让哪个线程在 CPU 上运行,后让哪个线程在 CPU 上运行,这也是有一定的优先级的,操作系统进线程调度的时候并不是很公平的。
上下文:
操作系统在进行线程谢欢的时候,就需要把进程执行的"中间状态"记录下来,保存好,下次这个线程再到 CPU 上去运行的时候,就可以恢复上次的状态,继续往下执行,本质上可以理解为 "存档和读档",就是 CPU 中各个寄存器的值,保存上下文,就是把这些 CPU 寄存器的值记录保存到内存中(PCB),回复上下文,就是把内存中这些寄存器的值恢复回去。
记账信息:
操作系统,统计每个线程在 CPU 上占用的时间和执行的指令数目,根据这个来决定下一阶段如何调度。
3.3.5 总结
PCB 这里包含的属性是非常多的,不止我们上面所说的 pid,内存指针,文件描述符表,调度相关属性,还有其他内容,上述内容只是一些核心的属性,对于我们程序猿来说,了解以上属性已经是差不多了。
但是从调度相关属性开始,就有些小伙伴看不明白了,什么是调度?
4. 线程的调度
如何理解调度?从字面理解,你的领导将把你从北京公司调度到上海分公司去,而在 CPU 中,调度就是把一个线程拿到 CPU 核心上执行任务!
进程是 CPU 资源分配的最小单位,而线程是 CPU 调度的最小单位!
要了解线程的调度,首先要知道两个概念,并行和并发:
并行:简单来说,两个线程在 CPU 的两个核心上运行
并发:简单来说,两个线程在一个 CPU 核心上运行,对两个线程进行快速切换
并行好理解,并发可能就不太好理解了!
举个例子,张三喜欢打游戏,突然女朋友给打电话了,此时张三的脑子里就有两个信号,第一个处理游戏,第二个处理女朋友的电话,可是既不能放下游戏,也不能挂掉女朋友电话呀,于是张三就一边打游戏,一边接电话,只要张三脑子在游戏电话这两个任务中切换的够快,不错过游戏的内容,也不错过女朋友的电话内容,此时就能做到无缝衔接!
放在 CPU 中,先运行一下QQ音乐,再运行一下再运行下画图板,再运行一下QQ音乐,再运行画图板,只要切换的速度足够快(3.2GHz,每秒约运行32亿条指令),从我们的角度看是感知不到了,看起来好像是这几个线程再同时运行。
有了上面的理解,因此往往把并行和并发统称为并发,除非显式声明,否则后续我们说到的并发都是 并行+并发
问题来了,CPU 的核心数有限,如果一个核心上并发运行多个线程,虽然我们知道是进行快速的切换,但是他们是随机切换的!也就是 CPU 是随机调度的,线程是抢占式执行的!这一点后续内容会用代码给大家验证!此处大家了解即可。
5. 线程的缺点
前面我们谈到,一个进程中可以有多个线程,就如同我们上述举的工厂的例子有两条生产线,现在假设要生产 100 个篮球,一条生产线生产一个篮球需要花 2 个小时,那么 100 个篮球就需要花 200 个小时,如果是两条生产线,大概只需要花 100 个小时左右,那是不是生产线越多就越好呢?工厂能不能放下那么多生产线呢?
到这里问题来了,进程中的线程是不是也是越多越好呢?这里通过一个形象的例子来演示:
滑稽老铁今天过生日,喊了自己的一个好哥们一起吃蛋糕:
注意:上述情况房子就像进程,里面有多个线程,共享这个蛋糕的资源,两个人吃蛋糕可能绰绰有余,速度也不快不慢,但是我们往后看:
这时候另一个滑稽老铁说,就咱们俩个人啊,一点气氛都没有,蛋糕那么大,吃都吃不完!能不能多喊几个人来啊?这样才不会浪费蛋糕呀!而且要是能把俺暗恋的对象也喊来该多好!
于是滑稽老铁说,你想要气氛对吧,那俺就把咱的好兄弟都喊过来!
此时滑稽越多,是不是吃蛋糕的速度就越快呢?并不是,桌子旁边的位置是有限的,就好比 CPU 的核心数也是有限的,如果滑稽老铁太多了(线程),就会导致正在吃蛋糕的老铁没办法安心吃,一般一张桌子只能坐 12 个人,难不成一个人吃一口就换另一个人上来吃吗?这样的开销反而浪费在了 "换人吃蛋糕" 这个时间上,就好比如开销浪费在线程调度上了。
此时还有一个问题,一个滑稽老铁吃了一口下来了,空的这个座位有两个滑稽都想坐上去,可能就会打起来,谁也让着谁,会引发线程安全问题,也就是线程不安全!
更有一个特殊的情况,两个滑稽老铁争蛋糕上仅一份的巧克力,滑稽1把巧克力给抢走了!滑稽2抢不到巧克力就生气!拿起蛋糕往滑稽1脸上拍。
也就是如果一个线程抛异常处理的不好,很有可能把整个进程给带走了,其他老铁都吃不成蛋糕了,其他线程也就挂了!
总结:
线程模型,天然就是资源共享的,多个线程争抢同一个资源,容易有线程安全问题。
进程模型,天然是资源隔离的,很少有线程安全问题,进程之间通信,如果多个进程访问公共资源,才可能会出现线程安全问题。
本期只是简单带大家认识下多线程和进程,更多的是理论概念,后续博主则会根据代码实例,来进一步带大家走进多线程编程!
下期预告【多线程】Thread类