基本概念
1.并发:多个线程操作相同的资源,保证线程安全,合理利用资源
2.高并发:服务能同时处理很多请求,提高程序性能
3.CPU多级缓存
由来:我们知道,内存(存储数据)和CPU(处理数据)是计算机的核心组件,但CPU的频率却远远快于内存,CPU需要等待主存从而造成了资源浪费,为了避免CPU和内存速度不匹配的问题,缓存应运而生
缓存的意义:(1)时间局部性:某个数据被访问,那么在不久的将来它可能再次被访问
(2)空间局部性:某个数据被访问,那么与它相邻的数据也可能被访问
缓存一致性(MESI):
modified状态图示多核情况下,处理器0要修改缓存行x=3,则需如下操作:
(1)、通知处理器1,处理器2我要修改此行了,你们的缓存行x=3变为无效状态
(2)、处理器1,处理器2接收到通知,回应0,我知道你要修改了,我放弃了x=3缓存行
(3)、处理器0这时的缓存行x=3变为独享状态,和主存一致
(4)、修改0中的x=3为x=5,变为modified状态
注意:修改之前应该为共享(shared)状态,shared状态下数据是无法被修改的
起初0,1,2共享状态,1,2接到通知变为无效状态,0则变成独享状态,缓存行修改后与主存不一致,这时则是被修改状态。
由上分析,我们很容易理解下面两幅图示
exclusive sharedinvalid状态为空,没有保存有效数据。
注:三幅图来自《大话处理器》
MESI消息
不同CPU的缓存之间以及主存之间的通信?
上文所述通知我要修改x=3缓存行了,回复我知道了这些其实就是消息和通信,载体为CPU的共享总线,下面介绍几种MESI消息:
Read:当CPU在自己的cache中没有发现需要的物理地址,就会发送一条“READ”消息,该消息包括缓存行需要读的物理地址。
Read Response: 是回复“Read”消息的。“Read Response”消息是由内存或者其他CPU缓存提供的。如果其他缓存请求一个处于“modified”状态的数据,则本地缓存必须提供“Read Response”消息。这个很容易理解,别的CPU在请求本地缓存中的数据,而这份数据还没有刷新到内存,所以必须告诉其他CPU该数据的最新值。接收到”Read Response”消息后,该数据的缓存状态就由”invalid”变成了”share”或者”exclusive”,这取决于”Read Response”的提供者是内存还是其他CPU缓存。
Invalidate:“ invalidate” 消息包含要使无效的缓存行的物理地址。其他的缓存必须从它们的缓存中移除相应的数据并且响应此消息。当CPU要对一个变量进行写操作,而此变量处于只读状态(share),就需要发送“invalid”消息。由于一个变量被多个CPU缓存,所以单个CPU的改写会造成缓存不一致,所以在写之前必须告诉其他CPU你们缓存的值马上就要过时了。接受到”invalidate”消息的CPU就会把本地缓存中的对应数据无效掉。
Invalidate Acknowledge:一个接收到“invalidate”消息的 CPU必须在移除指定数据后响应一个“invalidate acknowledge”消息。这个消息就是告诉“invalidate”消息的提供者“我已经知道你要更改这个数据了,我放弃使用自己缓存中的拷贝!”
Read Invalidate:”read invalidate”消息包含要缓存行读取的物理地址。同时指示其他缓存移除数据。因此,它包含一个”read”和一个”invalidate”。“read invalidate”也需要“read response”以及”invalidate acknowledge”消息集。
“Read Invalidate”消息的发送时机有两个:第一个是CPU对一个数据进行原子读写操作,但是该数据没有在本地CPU的缓存中,在其他CPU缓存中可能有该数据的拷贝。所以它需要发送一条“Read Invalidate”消息,它不仅需要读取该数据的最新值,还要无效掉其他的CPU缓存(它马上就要改写该数据)。
Writeback:“writeback”消息包含要回写到内存的地址和数据。这个消息允许缓存在必要时换出“modified”状态的数据以腾出空间。消息的发送时机是,CPU把本地缓存中的数据刷新到内存中,而该数据是share状态(只读),它需要告诉其他CPU”我不再使用这些缓存数据了”
CPU多级缓存乱序优化
原理:处理器为了提高运算速度而做出违背代码原有顺序的优化。乱序执行初始目的是为了提高效率,但是它看来其好像在这多核时代不尽人意,其中的某些"自作聪明"的优化导致多线程程序产生各种各样的意外。例如两条指令,一条是准备数据,一条是标识数据准备完成,这种在多核情况下是不安全的。
java内存模型(JMM)
规定了一个线程如何和何时可以看到由其他线程修改后的共享变量的值,以及在必须时如何同步的访问共享变量。
java内存模型如图,对象的引用存放在线程栈上,但是这个对象却是存放在堆当中,如果两个线程同时调用同一个对象的同一个方法,他们将都可以访问这个对象的成员变量,但是每一个线程都拥有了这个成员变量的私有拷贝。
计算机架构模型简示速度:寄存器>高速缓存>缓存
线程间的通信线程A和线程B如何通信?
1、线程A将改变后的共享变量的值更新到住内存当中
2、线程B从主内存读取由线程A刷新之后的值
*这就是为什么多线程操作同一个变量时是线程不安全的,线程A尚未将更新后的值刷新回主内存,线程B就从主内存读取了共享变量的值,读取到了错误的数据,从而导致最终的结果不对。从而引入了同步手段。
java内存模型同步操作与规则
同步操作与规则(1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
(8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行
同步规则分析:
1)不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步会主内存中
2)一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
3)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
4)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
5)如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
6)对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
总结:
并发有什么问题?
(1)安全性:多个线程共享数据时可能会产生于期望不相符的结果(未做同步操作的情况下)
(2)活跃性:某个操作无法继续进行下去时,就会发生活跃性问题。比如:死锁、饥饿等问题
(3)性能:线程过多时会使得:CPU频繁切换,调度时间增多;同步机制;消耗过多内存,任何事情都不是绝对的,多线程既能提高性能也可能降低性能,关键是要结合实际场景,看瓶颈到底在哪里?一味提高线程数肯定是不对的。
什么时候考虑多线程?
(1)速度:同时处理多个请求,响应更快;复杂的操作可以分成多个进程(或线程)同时进行,可以简单理解为异步请求,不必等待此次进程的结果,继续往下执行。
(2)设计:程序设计在某些情况下更简单,也可以有更多的选择
(3)资源利用:CPU能够等待IO的时候能够做一些其他的事情,这是从成本和速度两个角度出发的,系统卡慢的时候多从程序内部去分析,只是横向增加机器的性能是没用的,机器再好,你始终是单线程,或者说始终只使用了资源的百分之一,这样既浪费了资源,也不能有效的提高响应的速度。
网友评论