Javaer都知道,我们在编译器上面编写的Java代码经过编译后会形成字节码,然后由类加载器加载到JVM中,JVM在执行字节码时,将它们转换成一条条的汇编指令,最终由CPU的寄存器来运行,在CPU执行这些汇编的过程中需要读取数据或者写入数据,但CPU能读取的数据只能来自计算机中的内存,随着科技的发展,像Intel的部分CPU频率特别是睿频后已经到达了4.3GHZ了,但内存发展就比较缓慢,比如顶级的内存就3600MHZ左右,因此就造成了CPU的处理速度已经远远超过了内存的访问速度,正常情况都是千倍的速度差距。
CPU缓存模型
因为速度差距过大的原因,如果还是采用CPU直接读取内存上面的数据,就会导致CPU资源严重的浪费!于是那些生产CPU的科技公司就设计出了,在CPU和内存之间增加一层缓存的方案,刚才刻意到京东查了一下,现在的CPU基本都是三级缓存了,L1 ,L2 ,L3 缓存,我从百度图片找来了两张图(如有侵权,请联系我,我马上删除)
通过这两张图,我们就可以更加直观地感受到CPU缓存和内存在访问上面的速度的差距了,至于CPU核心的计算速度,和他们相比又是另一个级别的差距了。
那么在有了CPU缓存之后,我们就可以在程序运行的过程中,先从内存拷贝一份数据到CPU缓存中,然后CPU计算都操作缓存里的数据,等执行完成的时候,再把缓存中的数据更新到内存里,从而增加CPU的使用效率。
在引入CPU缓存之后,主了提高CPU的使用效率之外,还带来了一个数据不一致的问题。比如i++这一个操作,在引入了CPU缓存之后,他具体的情况是这样的:
1:将内存中的i复制一份到CPU缓存当中
2:CPU核心读取CPU缓存中的i
3:对i执行+1操作
4:将更新后的i写回CPU缓存
5:将CPU缓存中的i更新到内存中
对于单线程来说,这完成不会有什么问题,但是对于多线程来说,就会出现错误了,因为每个线程都有自己的工作空间。比如,现在有线程A和线程B同时对i执行i++操作,我们假设i一开始为0,我们期望最后的结果是2,但是最后的结果可能1:比如:
1:线程A将内存中的i复制一份到CPU缓存当中,此时 i = 0;
2:线程B将内存中的i复制一份到CPU缓存当中,此时 i = 0;
3:线程A对应的CPU核心1读取CPU缓存中的i,并执行+1操作,然后把更新后的i写回CPU缓存(i=1)
4:线程B对应的CPU核心2读取CPU缓存中的i,并执行+1操作,然后把更新后的i写回CPU缓存(i=1)
5:线程A将CPU缓存中的i更新到内存(i=1)
6:线程B将CPU缓存中的i更新到内存(i=1)
出现这种情况的原因也是很简单的,比如多个CPU核心都从内存拷贝了一份数据到各自的缓存当中,然后直接拿缓存中的数据来执行+1操作,最后再把数据刷新内存,于是就造成了这个问题。由于Demo过于简单,我就不给出来了。下面我们回顾一下历史,看看这个问题是怎么被解决的,其实解决这个问题的方案有两种:
第一种是早期的方案,因为CPU和计算机的其他组件通信是通过总线来进行的,
比如数据通信就是通过数据总线来进行,如果一个CPU核心要操作某个数据了,
就通过向总线发送一个LOCK#的信号来获取总线锁,那么其他CPU核心就被阻塞了,
从而只有一个CPU核心能对内存进行访问。
但是这种方案明显效率是比较低的,于是就提出了第二方案:
通过缓存一致性协议来解决数据不一致的问题,即CPU在操作CPU缓存中的数据时,
如果发现它是一个共享变量(其他CPU也缓存了一个副本),那么他会进行以下的两种操作:
(1) 读操作,只会将数据单纯读到寄存器,不做额外处理
(2) 写操作,发出一个信号告诉其他CPU核心,你缓存的数据已经无效啦,让其他CPU在读取共享变量时,不得不重新去内存中重新拿过数据。
至此CPU缓存模型我们已经介绍的差不多了,下一篇我们去了解Java内存模型,有了CPU缓存模型和Java内存模型的知识,我们重新认识Java高并发又是另一种理解境界,下期见。