声明:所有图片来源于网络
cpu 多核并发缓存架构
空间局部性原则
Java 内存模型 JMM
Java 线程内存模型跟cpu 缓存模型类似, 是基于 cpu 缓存模型来建立的, Java 线程内存模型是标准化的, 屏蔽掉了底层不同计算机的区别。
- 主内存
- 工作内存
主内存中的共享变量
在工作内存中有一个共享变量的副本
JMM 数据原子操作
- read(读取): 从主内存读取数据
- load(载入): 将主内存读取到的数据写入工作内存
- use(使用): 从工作内存读取数据来计算
- assign(赋值): 将计算好的值重新赋值到工作内存中
- store(存储): 将工作内存数据写入主内存
- write(写入): 将 store 过去的变量值赋值给主内存中的变量
- lock(锁定): 将主内存变量加锁, 标识为线程独占状态
- unlock(解锁): 将主内存变量解锁, 解锁后其他线程可以锁定该变量
Java 内存模型执行流程(不加Volatile关键字):
1、每个cpu从主内存中读出数据(共享变量)
2、然后载入到各自相应的工作内存
3、线程从工作内存读取数据(共享变量副本)开始使用, 在使用过程中,某些线程有可能会进行赋值操作(即改变工作内存中的数据),这时会导致线程间的数据不再一致
4、当执行store 操作后,会将工作内存数据写入主内存
5、然后将 store 过去的变量值赋值给主内存中的变量,由于某些线程改变了工作内存中的数据,所以在写入主内存后,会对主内存中的变量值进行修改, 而其他一些线程的值却从未改变
JMM 缓存不一致问题
早期为了解决缓存不一致问题, cpu 设计者通过总线加锁的方式进行解决。
总线加锁(性能太低)
比如,第一个线程t1先执行,在 read 操作之前对主存进行lock操作,即在总线级别对主内存加一把锁,此时如果还有其他的cpu线程也要访问主存变量,通过总线read数据时,该数据已经被加了锁,那就只能等t1把锁释放之后,其它线程才能读取该数据。而该锁会一直到t1执行完write操作,将数据同步回主内存时才会在总线上释放掉。
显然,这种方式几乎变成了串行执行,大大降低了性能,根本没有利用多核cpu提高性能的优势。MESI 缓存一致性协议
cpu 总线嗅探机制(监听)数据在回写到主内存的过程中,会通过总线,其它 cpu 通过 cpu 的总线嗅探机制对总线进行嗅探(监听),一旦嗅探到自己工作内存拥有的变量,这些 cpu 会马上把自己工作内存中的该变量失效掉,若要使用该变量,需得重新到主内存中读取,(注意,这里数据在写入到主内存之前的 store 阶段就对该数据进行lock加锁(缓存行锁),直到真正写入到主内存后unlock,使得锁粒度大大减小),使得其他 cpu 在将变量失效后可以在主内存中读取最新的值,Volatile 的底层实现基本就是利用这样一种机制。
Volatile 可见性底层实现原理
- Volatile 缓存可见性实现原理
底层实现主要是通过汇编 lock 前缀指令, 它会锁定这块内存区域的缓存并回写到主内存, 此操作被称为”缓存锁定”, MESI 缓存一致性协议机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存值通过总线回写到内存会导致其它处理器相应的缓存失效。
可见性、原子性与有序性
并发编程三大特性:
可见性:
原子性:
有序性:
volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
Volatile保证可见性与有序性,但是不保证原子性,保证原子性则需要借助synchronized 这样的锁机制。
原因:当一个cpu的工作内存中的变量值发生改变时,其他工作内存中的该变量值并不一定马上发生改变,因为这中间还有一个回写到主内存的操作,只有回写到主内存以后,其他cpu线程才可以read操作。如下情况:在一个cpu线程(t1)对变量进行回写的过程中,还没有写入到主存,此时,另一个工作空间中线程(t2)对该变量也进行了赋值操作,也进行回写操作,但是肯定只有一个cpu可以抢先回写(假如t1线程对应的变量被先回写),那么其他cpu会通过总线嗅探机制,对工作空间中的该变量失效,即t2线程对应的变量也会失效掉(即使它已经进行了赋值操作,其实相当于先前的赋值操作白做了),需要重新load值。

