多道程序设计技术是在计算机主存中同时存放几道相互独立的程序,它们在操作系统控制之下,相互穿插的运行。
多道程序运行的特征:
1、多道:计算机主存中同时存放几道相互独立的程序。
2、宏观上并行:同时进入系统的几道程序都处于运行过程中,即它们都开始运行,但都未运行完毕。
3、微观上串行:从微观上看,主存中的多道程序轮流或分时地占有处理机,交替运行。
多道程序设计技术是在计算机主存中同时存放几道相互独立的程序,它们在操作系统控制之下,相互穿插的运行。
多道程序运行的特征:
1、多道:计算机主存中同时存放几道相互独立的程序。
2、宏观上并行:同时进入系统的几道程序都处于运行过程中,即它们都开始运行,但都未运行完毕。
3、微观上串行:从微观上看,主存中的多道程序轮流或分时地占有处理机,交替运行。
转载于:https://www.cnblogs.com/luo841997665/p/4657409.html
采用多道程序设计可以提高处理器的利用率。多道程序设计技术充分发挥了处理器与外围设备以及外围设备之间的并行工作能力,从而提高处理器和其他各种资源的利用率。下面从程序的顺序执行、程序的执行环境和程序的并发执行几方面介绍多道程序设计模型。
一、程序的顺序执行
程序是一个在时间上按严格次序前后相继的操作序列,这些操作是机器指令或高级语言编写的语句。人们习惯的传统程序设计方法是顺序程序设计,计算机也是以顺序方式工作的:处理器一次执行一条指令,对内存一次访问一个字节或字,对处部设备一次传送一个数据块。顺序处理也是人们习惯的思考方法,为了解决一个复杂的问题,人们把它分解成一些较为简单、易于分析的小问题,然后逐个解决。也可以把一个复杂的程序划分为若干个程序段,然后按照某种次序逐个执行这些程序段。
我们把一个具有独立功能的程序独占处理器直到得到最终结果的过程称为程序的顺序执行。程序的顺序执行具有如下特点。
1.顺序性
程序所规定的动作在机器上严格地按顺序执行。每个动作的执行都以前一个动作的结束为前提条件,即程序和机器执行它的活动严格一一对应。
2.封闭性
程序运行后,其计算结果只取决于程序自身,程序执行得到的最终结果由给定的初始条件决定,不受外界因素的影响。程序所使用的资源(包括处理器、内存、文件等)是专有的,这些资源的状态(除了初始状态外)只有程序本身的动作才能改变。
3.程序执行结果的确定性
也称为程序执行结果与时间无关性。程序执行的结果与它的执行速度无关,即处理器在执行程序时,任意两个动作之间的停顿对程序的计算结果都不会产生影响。
4.程序执行结果的可再现性
如果程序在不同的时间执行,只要输入的初始条件相同,则无论何时重复执行该程序都会得到相同的结果。
程序的顺序性和封闭性是一切顺序程序所应具有的特性,从这两个特性出发,不难引出程序执行时所具有的另外两个特性。顺序程序与时间无关的特性,可使程序的编制者不必去关心不属于他控制的那些细节(如操作系统的调度算法和外部设备操作的精确时间等);顺序程序执行结果的可再现性,则对程序检测和校正程序的错误带来了方便。
二、程序的并发执行
所谓程序并发执行,是指两个或两个以上程序在计算机系统中,同时处于已开始执行且尚未结束的状态。能够参与并发执行的程序称为并发程序。程序的并发执行,可以充分利用系统的资源,提高计算机的处理能力。但是,程序的并发执行产生了一些和程序顺序执行时不同的特性。程序的并发执行有如下特征。
1.在执行期间并发程序相互制约
资源的共享和竞争存在于多道程序的并发执行中,从而制约了各道程序的执行速度。由于本来并无逻辑关系的程序之间产生了相互制约的关系,而各个程序活动的工作状态与所处环境有密切关系,使并发程序的执行出现了“执行——暂停——执行”的活动现象。
2.程序与计算不再一一对应
在并发执行中,允许多个用户进程调用一个共享程序段,从而形成了多个“计算”。如在分时系统中,一个编译程序往往同时为几个用户提供编译服务,该编译程序便对应了几个“计算”。
3.并发程序的执行结果不可再现
并发程序执行结果与其执行的相对速度以及并发执行的多道程序之间的相互关系有关,导致并发程序的执行结果不可再现,即执行结果是不确定的。
4.程序的并行执行与程序的并发执行
多道程序的并发执行是指它们在宏观上,即在某一段时间周期内是同时进行的(这个时间周期,比处理器的指令处理周期要长得多,但是从操作人员的感觉来看,仍然时一个瞬间)。但从微观上看,除了多处理器系统外,在单处理器系统中,这些程序仍然是顺序执行的。
程序的并行执行与程序的并发执行,这两者存在着差别。前者是指不论从宏观的时间周期上看,还是从微观上看,若干程序确实在同时运行;而程序的并发执行,如果在单处理器系统中,它们在宏观上是同时进行的,但在微观上,这些程序仍然是顺序执行的。
英文Logic components;运算逻辑部件。可以执行定点或浮点算术运算操作、移位操作以及逻辑操作,也可执行地址运算和转换。
寄存器部件,包括寄存器、专用寄存器和控制寄存器。 通用寄存器又可分定点数和浮点数两类,它们用来保存指令执行过程中临时存放的寄存器操作数和中间(或最终)的操作结果。 通用寄存器是中央处理器的重要部件之一。
英文Control unit;控制部件,主要是负责对指令译码,并且发出为完成每条指令所要执行的各个操作的控制信号。
其结构有两种:一种是以微存储为核心的微程序控制方式;一种是以逻辑硬布线结构为主的控制方式。
微存储中保持微码,每一个微码对应于一个最基本的微操作,又称微指令;各条指令是由不同序列的微码组成,这种微码序列构成微程序。中央处理器在对指令译码以后,即发出一定时序的控制信号,按给定序列的顺序以微周期为节拍执行由这些微码确定的若干个微操作,即可完成某条指令的执行。
简单指令是由(3~5)个微操作组成,复杂指令则要由几十个微操作甚至几百个微操作组成。
1. 指令高速缓存,俗称指令寄存器 : 它是芯片上的指令仓库,有了它CPU就不必停下来查找计算机内存中的指令,从而大幅提高了CPU的运算速度。
2. 译码单元,俗称指令译码器 : 它负责将复杂的机器语言指令解译成运算逻辑单元(ALU)和寄存器能够理解的简单格式,就像一位外交官。
3. 控制单元 : 既然指令可以存入CPU,而且有相应指令来完成运算前的准备工作,背后自然有一个扮演推动作用的角色——它便是负责整个处理过程的操作控制器。根据来自译码单元的指令,它会生成控制信号,告诉运算逻辑单元(ALU)和寄存器如何运算、对什么进行运算以及对结果进行怎样的处理。
4. 寄存器 : 它对于CPU来说非常的重要,除了存放程序的部分指令,它还负责存储指针跳转信息以及循环操作命令,是运算逻辑单元(ALU)为完成控制单元请求的任务所使用的数据的小型存储区域,其数据来源可以是高速缓存、内存、控制单元中的任何一个。
5. 逻辑运算单元(ALU) : 它是CPU芯片的智能部件,能够执行加、减、乘、除等各种命令。此外,它还知道如何读取逻辑命令,如或、与、非。来自控制单元的讯息将告诉运算逻辑单元应该做些什么,然后运算单元会从寄存器中间断或连续提取数据,完成最终的任务。
6. 预取单元 : CPU效能发挥对其依赖非常明显,预取命中率的高低直接关系到CPU核心利用率的高低,进而带来指令执行速度上的不同。根据命令或要执行任务所提出的要求,何时时候,预取单元都有可能从指令高速缓存或计算机内存中获取数据和指令。当指令到达时,预取单元最重要的任务就是确保所有指令均排列正确,然后发送给译码单元。
7. 总线单元 : 它就像一条高速公路,快速完成各个单元间的数据交换,也是数据从内存流进和流出CPU的地方。
8. 数据高速缓存 : 存储来自译码单元专门标记的数据,以备逻辑运算单元使用,同时还准备了分配到计算机不同部分的最终结果。
我们都知道CPU的根本任务就是执行指令,对计算机来说最终都是一串由“0”和“1”组成的序列。CPU从逻辑上可以划分成3个模块,分别是控制单元、运算单元和存储单元,这三部分由CPU内部总线连接起来。如下所示:
控制单元:控制单元是整个CPU的指挥控制中心,由指令寄存IR(InstrucTIon Register)、指令译码器ID(InstrucTIon Decoder)和操作控制器OC(OperaTIon Controller)等,对协调整个电脑有序工作极为重要。它根据用户预先编好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。操作控制器OC中主要包括节拍脉冲发生器、控制矩阵、时钟脉冲发生器、复位电路和启停电路等控制逻辑。
运算单元:是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。
存储单元:包括CPU片内缓存和寄存器组,是CPU中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU访问寄存器所用的时间要比访问内存的时间短。采用寄存器,可以减少CPU访问内存的次数,从而提高了CPU的工作速度。但因为受到芯片面积和集成度所限,寄存器组的容量不可能很大。寄存器组可分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据。而通用寄存器用途广泛并可由程序员规定其用途,通用寄存器的数目因微处理器而异。这个是我们以后要介绍这个重点,这里先提一下。
我们将上图细化一下,可以得出CPU的工作原理概括如下:
总结一下,CPU的运行原理就是:
1. 取指令:CPU的控制器从内存读取一条指令并放入指令寄存器。指令的格式一般是这个样子滴:
操作码就是汇编语言里的mov,add,jmp等符号码;操作数地址说明该指令需要的操作数所在的地方,是在内存里还是在CPU的内部寄存器里。
指令译码:指令寄存器中的指令经过译码,决定该指令应进行何种操作(就是指令里的操作码)、操作数在哪里(操作数的地址)。
执行指令,分两个阶段“取操作数”和“进行运算”。
修改指令计数器,决定下一条指令的地址。
大多数使用虚拟存储器的系统都使用一种称为分页(paging)。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页框(frame).页和页框的大小必须相同。接下来配合图片我以一个例子说明页与页框之间在MMU的调度下是如何进行映射的:
在这个例子中我们有一台可以生成16位地址的机器,它的虚拟地址范围从0x0000~0xFFFF(64K),而这台机器只有32K的物理地址,因此他可以运行64K的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放64K程序的外部存储器(例如磁盘或是FLASH)以保证程序片段在需要时可以被调用。在这个例子中,页的大小为4K,页框大小与页相同(这点是必须保证的,内存和外围存储器之间的传输总是以页为单位的),对应64K的虚拟地址和32K的物理存储器,他们分别包含了16个页和8个页框。
关于MMU,我后面再来介绍。
目录
头发很多的程序员:『师父,这个批量处理接口太慢了,有什么办法可以优化?』
架构师:『试试使用多线程优化』
第二天
头发很多的程序员:『师父,我已经使用了多线程,为什么接口还变慢了?』
架构师:『去给我买杯咖啡,我写篇文章告诉你』
……吭哧吭哧买咖啡去了
在实际工作中,错误使用多线程非但不能提高效率还可能使程序崩溃。以在路上开车为例:
在一个单向行驶的道路上,每辆汽车都遵守交通规则,这时候整体通行是正常的。『单向车道』意味着『一个线程』,『多辆车』意味着『多个job任务』。
单线程顺利同行
如果需要提升车辆的同行效率,一般的做法就是扩展车道,对应程序来说就是『加线程池』,增加线程数。这样在同一时间内,通行的车辆数远远大于单车道。
多线程顺利同行
然而成年人的世界没有那么完美,车道一旦多起来『加塞』的场景就会越来越多,出现碰撞后也会影响整条马路的通行效率。这么一对比下来『多车道』确实可能比『单车道』要慢。
多线程故障
防止汽车频繁变道加塞可以采取在车道间增加『护栏』,那在程序的世界该怎么做呢?
程序世界中多线程遇到的问题归纳起来就是三类:『线程安全问题』
、『活跃性问题』
、『性能问题』
,接下来会讲解这些问题,以及问题对应的解决手段。
有时候我们会发现,明明在单线程环境中正常运行的代码,在多线程环境中可能会出现意料之外的结果,其实这就是大家常说的『线程不安全』。那到底什么是线程不安全呢?往下看。
原子性
举一个银行转账的例子,比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元,两个操作都成功才意味着一次转账最终成功。
试想一下,如果这两个操作不具备原子性,从A的账户扣减了1000元之后,操作突然终止了,账户B没有增加1000元,那问题就大了。
银行转账这个例子有两个步骤,出现了意外后导致转账失败,说明没有原子性。
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子操作:即不会被线程调度机制打断的操作,没有上下文切换。
在并发编程中很多操作都不是原子操作,出个小题目:
i = 0; // 操作1
i++; // 操作2
i = j; // 操作3
i = i + 1; // 操作4
上面这四个操作中有哪些是原子操作,哪些不是的?不熟悉的人可能认为这些都是原子操作,其实只有操作1是原子操作。
操作1:对基本数据类型变量的赋值是原子操作;
操作2:包含三个操作,读取i的值,将i加1,将值赋给i;
操作3:读取j的值,将j的值赋给i;
操作4:包含三个操作,读取i的值,将i加1,将值赋给i;
在单线程环境下上述四个操作都不会出现问题,但是在多线程环境下,如果不通过加锁操作,往往可能得到意料之外的值。
在Java语言中通过可以使用synchronize或者lock来保证原子性。
可见性
talk is cheap,先show一段代码:
/**
* Author: leixiaoshuai
*/
class Test {
int i = 50;
int j = 0;
public void update() {
// 线程1执行
i = 100;
}
public int get() {
// 线程2执行
j = i;
return j;
}
}
线程1执行update方法将 i 赋值为100,一般情况下线程1会在自己的工作内存中完成赋值操作,却没有及时将新值刷新到主内存中。
这个时候线程2执行get方法,首先会从主内存中读取i的值,然后加载到自己的工作内存中,这个时候读取到i的值是50,再将50赋值给j,最后返回j的值就是50了。原本期望返回100,结果返回50,这就是可见性问题,线程1对变量i进行了修改,线程2没有立即看到i的新值。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
如上图每个线程都有属于自己的工作内存,工作内存和主内存间需要通过store和load等进行交互。
为了解决多线程可见性问题,Java语言提供了volatile
这个关键字。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通共享变量不能保证可见性,因为变量被修改后什么时候刷回到主存是不确定的,另外一个线程读的可能就是旧值。
当然Java的锁机制如synchronize和lock也是可以保证可见性的,加锁可以保证在同一时刻只有一个线程在执行同步代码块,释放锁之前会将变量刷回至主存,这样也就保证了可见性。
关于线程不安全的表现还有『有序性』,这个问题会在后面的文章中深入讲解。
上面讲到为了解决可见性
问题,我们可以采取加锁方式解决,但是如果加锁使用不当也容易引入其他问题,比如『死锁』。
在说『死锁』前我们先引入另外一个概念:活跃性问题
。
活跃性是指某件正确的事情最终会发生,当某个操作无法继续下去的时候,就会发生活跃性问题。
概念是不是有点拗口,如果看不懂也没关系,你可以记住活跃性问题一般有这样几类:死锁
,活锁
,饥饿问题
。
(1)死锁
死锁是指多个线程因为环形的等待锁的关系而永远的阻塞下去。一图胜千语,不多解释。
(2)活锁
死锁是两个线程都在等待对方释放锁导致阻塞。而活锁
的意思是线程没有阻塞,还活着呢。
当多个线程都在运行并且修改各自的状态,而其他线程彼此依赖这个状态,导致任何一个线程都无法继续执行,只能重复着自身的动作和修改自身的状态,这种场景就是发生了活锁。

如果大家还有疑惑,那我再举一个生活中的例子,大家平时在走路的时候,迎面走来一个人,两个人互相让路,但是又同时走到了一个方向,如果一直这样重复着避让,这俩人就是发生了活锁,学到了吧,嘿嘿。
(3)饥饿
如果一个线程无其他异常却迟迟不能继续运行,那基本是处于饥饿状态了。
常见有几种场景:
高优先级的线程一直在运行消耗CPU,所有的低优先级线程一直处于等待;
一些线程被永久堵塞在一个等待进入同步块的状态,而其他线程总是能在它之前持续地对该同步块进行访问;
有一个非常经典的饥饿问题就是哲学家用餐问题
,如下图所示,有五个哲学家在用餐,每个人必须要同时拿两把叉子才可以开始就餐,如果哲学家1和哲学家3同时开始就餐,那哲学家2、4、5就得饿肚子等待了。
前面讲到了线程安全和死锁、活锁这些问题会影响多线程执行过程,如果这些都没有发生,多线程并发一定比单线程串行执行快吗,答案是不一定,因为多线程有创建线程
和线程上下文切换
的开销。
创建线程是直接向系统申请资源的,对操作系统来说创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度等。
线程创建完之后,还会遇到线程上下文切换
。
CPU是很宝贵的资源速度也非常快,为了保证雨露均沾,通常为给不同的线程分配时间片
,当CPU从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等,这个开关被称为『上下文切换』。
一般减少上下文切换的方法有:无锁并发编程
、CAS 算法
、使用协程
等。
多线程用好了可以让程序的效率成倍提升,用不好可能比单线程还要慢。
用一张图总结一下上面讲的:
文章讲了多线程并发会遇到的问题,你可能也发现了,文章中并没有给出具体的解决方案,因为这些问题在Java语言设计过程中大神都已经为你考虑过了。
Java并发编程学起来有一定难度,但这也是从初级程序员
迈向中高级程序员
的必经道路,接下来的文章会带领大家逐个击破!
不如顺手点个赞?!
作者:雷架
Github 开源项目:https://github.com/smileArchitect/JavaMap
作者简介:
☕读过几年书:华中科技大学硕士毕业;
😂浪过几个大厂:华为、网易、百度……
😘一直坚信技术能改变世界,愿保持初心,加油技术人!
微信搜索公众号【爱笑的架构师】
,关注这个对技术有追求且有趣的打工人。