一、并发与高并发理解
1. 并发
1)同时拥有两个或者多个线程,如果程序在单核处理器上运行多个线程将交替地换入或者换出内存,这些线程是同时“存在”的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上,此时,程序中的每个线程都将分配到一个处理器核上,因此可以同时运行
2)多个线程操作相同的资源,保证线程安全,合理使用资源
2. 并行
1)系统中有多个任务同时存在可称之为“并发”,系统内有多个任务同时执行可称之为“并行”;并发是并行的子集
2)如果说并发就是在一台处理器上"同时"处理多个任务,那么并行就是在多台处理器上同时处理多个任务;个人理解是,在单核CPU系统上,并行是无法实现的,只可能存在并发而不可能存在并行
3. 高并发
1)高并发( High Concurrency) 是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够"同时并行处理"很多请求
2)服务能同时处理很多请求,提高程序性能
3)如系统集中收到大量的请求(12306的抢票系统),导致系统在某段时间内执行大量的操作,包括对资源的请求、数据库的操作等等,如果高并发处理不好,不仅仅降低用户的体验度,请求时间变长,同时也可能导致系统宕机,甚至导致OOM(Out Of Memory)异常,如果想要系统适应高并发状态,就要有多个方面进行系统优化,包括硬件、网络、系统架构、开发语言的选取、数据结构的应用、算法的优化等等,这个时候谈论的是如何提供现有程序的性能,对高并发场景提供一些解决方案、手段等等
二、cpu多级缓存
1. CPU多级缓存
1)组件
(1)Main Memory : 主存
(2)Cache : 高速缓存,数据的读取和存储都经过此高速缓存
(3)CPU Core : CPU核心
(4)Bus : 系统总线
2)说明
(1)CPU Core 与 Cache 之间有一条快速通道,Main Memory 与 Cache 关联在 Bus 上,同时 Bus 还用于其他组件 的通信
(2)在高速缓存出现后不久,系统变得愈加复杂,高速缓存与主存之间的速度差异被拉大,直到加入了另一级缓存(L2 Cache),新加入的这级缓存比第一缓存更大、更慢。由于加大一级缓存的方法从经济利益的角度考虑是行不通的,所以这才有了二级缓存,甚至现在有些系统拥有三级缓存
(3)对于各级的Cache,访问速度是不同的,理论上说L1 Cache(一级缓存)有着跟CPU寄存器相同的速度,但L1 Cache有一个问题,当需要同步Cache和内存之间的内容时,需要锁住Cache的某一块("术语是Cacheline"),然后再进行Cache或者内存内容的更新,这段期间这个Cache块是不能被访问的,所以L1 Cache的速度就没寄存器快,因为它会频繁的有一段时间不可用
3)为什么需要CPU CACHE
(1)CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源,这样会使CPU花费很长时间等待数据到来或把数据写入内存
(2)所以Cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:CPU - > CACHE - > MEMORY)
4)CPU Cache有什么意义
缓存的容量远远小于主存,因此出现缓存不命中的情况在所难免,既然缓存不能包含CPU所需要的所有数据,那么Cache的存在真的有意义吗?
CPU缓存存在的意义分两点(局部性原理):
(1)时间局部性:如果某个数据被访问,那么在不久的将来它很可能被再次访问
(2)空间局限性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问
5)缓存原理:
1)缓存的工作原理是当CPU要读取一个数据时,首先从缓存中查找,如果找到就立即读取并运送给CPU处理;如果没有找到,就用相对慢的速度内存中读取并运送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。
2)正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在缓存中,大约10%需要从内存读取。
3)实际工作时,CPU往往需要重复读取同样的数据块,而缓存容量的增大,可以大幅度提升CPU内部读取数据的命中率,而不用再到内存或者硬盘上寻找,以此提高系统性能
2. 缓存一致性(MESI)
它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
1)概念
(1)缓存一致性用于保证多个CPU Cache之间缓存共享数据的一致性,定义了CacheLine四种状态,而CPU对Cache的四种操作,可能会产生不一致的状态,因此缓存控制器监听到本地操作和远程操作的时候 ,需要对Cache Line作出相应的修改,从而保证数据在多个缓存之间的一致性
(2)Cache Line :是cache与内存数据交换的最小单位,根据操作系统一般是32byte或64byte。在MESI协议中,状态可以是M、E、S、I,地址则是cache line中映射的内存地址,数据则是从内存中读取的数据
(3)在MESI协议中,每个Cache的Cache控制器不仅知道自己的读写操作,而且也监听(snoop)其它Cache的读写操作。每个Cache line所处的状态根据本核和其它核的读写操作在4个状态间进行迁移
2)MESI四种状态
(1)M: Modified 修改,指的是该缓存行只被缓存在该CPU的缓存中,并且是被修改过的,因此他与主存中的数据是不一致的,该缓存行中的数据需要在未来的某个时间点(允许其他CPU读取主存相应中的内容之前)写回主存,然后状态变成E(独享)
(2)E:Exclusive 独享 缓存行只被缓存在该CPU的缓存中,是未被修改过的,与主存的数据是一致的,可以在任何时刻当有其他CPU读取该内存时,变成S(共享)状态,当CPU修改该缓存行的内容时,变成M(被修改)的状态
(3)S:Share 共享,意味着该缓存行可能会被多个CPU进行缓存,并且该缓存中的数据与主存数据是一致的,当有一个CPU修改该缓存行时,其他CPU是可以被作废的,变成I(无效的)
(4)I:Invalid 无效的,代表这个缓存是无效的,可能是有其他CPU修改了该缓存行
3)Cache 操作: MESI协议中,每个cache的控制器不仅知道自己的操作(local read和local write),通过监听也知道其他CPU中cache的操作(remote read和remote write)。对于自己本地缓存有的数据,CPU仅需要发起local操作,否则发起remote操作,从主存中读取数据,cache控制器通过总线监听,仅能够知道其他CPU发起的remote操作,但是如果local操作会导致数据不一致性,cache控制器会通知其他CPU的cache控制器修改状态
4)状态间的相互转换关系
3. 乱序执行优化
处理器为提高运算速度而做出违背代码原有顺序的优化
1)CPU乱序执行优化不会对结果造成影响,在单核时代,处理器保证做出的优化,不会导致执行的结果远离预期的目标,但是在多核环境下并非如此
2)首先在多核环境中,同时会有多个核执行指令,每个核的指定都可能会被乱序优化,另外,处理器还引用了L1、L2等缓存机制,每个核都有自己的缓存,这就导致了逻辑次序上后写入内存的数据,未必真的最后写入,最终带来了这样的一个问题:如果我们不做任何防护措施,处理器最终得到的结果和我们逻辑得出的结果大不相同
3)比如我们在其中的一个核中执行数据写入操作,并在最后写一个标记,用来标记数据已经准备好了,然后从另外一个核上,通过那个标志,来判断数据是否已经就绪,这种做法它就存在一定的风险,标记位先被写入,但数据操作并未完成(可能是计算为完成、也可能是数据没有从缓存刷新到主存当中), 最终导致另外的核使用了错误的数据
三、java内存模型
1. java内存模型
为了屏蔽掉各种硬件和操作系统的内存访问差异,实现让Java程序在各种平台下都能达到一致的并发效果,JMV规范中定义了JMM (Java Memory Model (Java 内存模型))。 JMM是一种规范,它规范了JVM与计算机内存是如何协同工作的,它规定一个线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步的访问共享变量
1)Heap(堆):Java里的堆是一个运行时的数据区,堆是由垃圾回收来负责的,堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,他是在运行时动态分配内存的,Java的垃圾回收器会定时收走不用的数据。但是也有缺点,由于是要在运行时动态分配内存,因此存取速度相对较慢。
2)Stack(栈): 存取速度比Heap要快,仅次于计算机中的寄存器,栈中的数据是可以共享的(?可能是类地址的引用?),但是它的缺点是,存在栈中数据的大小和生存期必须是确定的,缺乏灵活性,主要存放一些基本类型的变量
3)JMM要求调用栈和本地变量存放在线程栈中,对象存放在堆上。一个本地变量可能指向一个对象的引用,引用这个本地变量是存放在线程栈上,而对象本身是存放在堆上的。
4)一个对象可能包含方法,这些方法可能包含本地变量,这些本地变量还是存放在线程栈中,即使这些方法所属的对象存放在堆上。一个对象的成员变量可能会随着这个对象自身存放在堆上,不管这个成员对象是原始类型还是引用类型,静态成员变量跟随着类的定义一起存放在堆上。存放在堆上的对象,可以被所持有对这个对象引用线程的访问。
5)当一个线程可以访问一个对象的时候,它也可以访问该对象的成员变量,如果两个线程同时调用同一个对象的同一个方法,将会都访问该对象的成员变量,但是每一个线程都拥有了这个成员变量的私有拷贝
2. 计算机内存硬件架构
1)CPU,一台现代计算机拥有两个或多个CPU,其中一些CPU还有多核,从这一点可以看出,在一个有两个或多个CPU的现代计算机上,同时运行多个线程是非常有可能的,而且每个CPU在某一个时刻,运行一个线程是肯定没有问题的,这意味着,如果Java程序是多线程的,在Java程序中,每个CPU上一个线程是可能同时并发执行的。
2)CPU Refisters(寄存器),每个CPU都包含一系列的寄存器,它们是CPU内存的基础,CPU在寄存器中执行操作的速度远大于在主存上执行的速度,这是因为CPU访问寄存器的速度远大于主存
3)Cache(高速缓存),由于计算机的存储设备与处理器运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高级缓存来作为内存与处理器之间的缓冲,将运算需要使用到的数据复制到缓存中,让运算能快速的进行,当运算结束后,再从缓存同步到内存中。这样处理器就无需等待缓慢的内存读写,CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度要慢
4)Main Memory(主存),随机存取存储器(random access memory,RAM)又称作“随机存储器",一个计算机包含一个主存,所有的CPU都可以访问主存,主存通常比CPU中的缓存大得多
3. JVM 与 Computer
1)JVM 与 Computer 内存架构存在差异,硬件内存并无区分栈与堆,对于硬件而言,所有的栈和堆都分布在主内存中,可能会出现在高速缓存、寄存器中
4. Java内存模型抽象结构
1)每个线程之间共享变量都存放在主内存里面,每个线程都有一个私有的本地内存
2)本地内存是Java内存模型中抽象的概念,并不是真实存在的(他涵盖了缓存写缓冲区。寄存器,以及其他硬件的优化)
3)本地内存中存储了以读或者写共享变量的拷贝的一个副本
4)从一个更低的层次来说,线程本地内存,他是CPU缓存,寄存器的一个抽象描述,而JVM的静态内存存储模型,他只是一种对内存模型的物理划分而已,只局限在内存,而且只局限在JVM的内存
5)如果线程A和线程B要通信,必须经历两个过程
(1)A将本地内存变量刷新到主内存
(2)B从主内存中读取变量
5. Java 内存模型 - 同步操作与规则
1)同步八种操作
(1)lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值存放工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
(7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传递到主内存中,以便随后的write的操作
(8)write(写入):作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
2)同步规则
(1)如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作,但Java内存模型只要求上述操作必须按顺序执行,而没有保证是连续执行
(2)不允许read和load、store和write操作之一单独出现
(3)不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
(4)不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
(5)一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作
(6)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次与执行lock后,只有执行相同次数的unlock,变量才会被解锁。lock和unlock必须成对出现
(7)如果一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
(8)如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量
(9)对一个变量执行unlock操作之前,必须先把变量同步到主内存中(执行store和write操作)
四、并发的优势、风险
1. 优势
1)速度
(1)系统可以同时处理多个请求,等待的时间变短,那么相应的响应更快
(2)另一方面,复杂的操作可以分成多个进程同时进行
2)设计
(1)总结:程序设计在某些情况下更简单,也可以有更多的选择
(2)举例:我们有一个应用程序需要从本地系统中读取和处理两个文件,如果你想编写程序,读取和处理数据的时候,你就必须记录哪个文件的读取和处理的状态。相反,如果你使用多线程并发处理,你就可以启动两个线程,每一个线程读取和处理一个文件的读取和操作,线程会在磁盘读取文件的过程中被阻塞,在等待的时候,其他的线程能够使用CPU去处理已经读取完的文件,其结果就是,磁盘总是繁忙的读取文件到内存中,这回带来磁盘和CPU利用率的提升,而且每个线程只需要记录一个文件,因此这个方式也很容易被编程实现
3)资源利用
(1)CPU能够在等待IO的时候做一些其他的事情
(2)举例:依然是上面的例子,当某个应用程序从本地系统中读取和处理文件,从磁盘中读取文件的时候呢,大部分的CPU,时间都用于等待磁盘去读取数据,在这段时间里CPU非常的空闲。它可以做许多事情,通过改变操作顺序更高的利用CPU资源。(这里所说的IO可以是磁盘的IO、网络的IO、或者是用户输入,通常情况下:网络和磁盘的IO比CPU和内存的IO慢很多)
2. 风险
1)安全性
(1)总结:多个线程共享数据时可能会产生于期望不相符的结果
2)活跃性
(1)总结:某个操作无法继续进行下去时,就会发生活跃性问题。比如死锁、饥饿等问题…
(2)比如:我们已经知道了,多个线程共享数据时会产生于期望不符的结果,因此我们就会尝试通过加锁等方式来保证我们得到的结果能够符合预期。这个时候呢如果加锁、解锁操作不当、或者有什么异常情况发生,这个时候可能就会引发死锁
3)性能
(1)总结:线程过多时会使得:CPU 频繁切换,调度时间增多;同步机制;消耗过多内存
(2)说明1:当 CPU 调度不同线程时,它需要更新当前执行线程的数据,程序指针,以及下一个线程的相关信息。这种切换会有额外的时间、空间消耗,我们在开发中应该避免频繁的线程切换
(3)说明2:多环境情况下必须使用同步机制,这导致了很多编译器想做的优化被抑制。线程过多还会消耗过多内存
参考网址
JAVA并发编程与高并发解决方案 - 并发编程 一 之 并发相关知识
注:文章是经过参考其他的文章然后自己整理出来的,有可能是小部分参考,也有可能是大部分参考,但绝对不是直接转载,觉得侵权了我会删,我只是把这个用于自己的笔记,顺便整理下知识的同时,能帮到一部分人。
ps : 有错误的还望各位大佬指正,小弟不胜感激