目录
1.存储设备与CPU执行效率的冲突以及解决方案
- CPU进行计算时,需要与存储设备(内存)进行交互,存储和读取数据。由于计算机的存储设备的读取和写入速度和cpu运算速度有几个数量级的差距。这个时候,I/O速度成了执行效率的瓶颈。
- 为了解决上面的问题,在存储设备和CPU之间加上了一层读写速度与CPU运算速度比较接近的高速缓存(Cache)作为缓冲。将运算需要使用的数据复制到缓存中,让运算能快速进行;当运算结束后能再从缓存同步回内存之中。
- 但引入了高速缓存,又带来了新的问题。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享一块主内存。当多个处理器的运算任务用到了同一块主内存的数据,那会导致各自的高速缓存中的数据与主内存中的数据不一致。同步回主内存的数据又以哪个高速缓存为准。这就是高速缓存带来的数据不一致问题。
- 为了解决上面的缓存不一致问题,需要各个处理器在访问缓存时遵循一些协议,比如MSI、MESI、MOSI、Synapse、Firefly、Gragon等。可以去看博客学习下MESI协议,挺有意思的:并发研究之CPU缓存一致性协议(MESI)
2.Java内存模型
Java内存模型的主要目标是定义各个变量的访问规则。Java内存模型规定所有的变量都存储在主内存中(这里的主内存只是虚拟机内存的一部分,与上面提到的主内存是有区别的),每个线程都有自己的工作内存(可与上面的高速缓存类比),工作内存中保存了主内存的变量的主内存副本拷贝。
3. 内存间的交互操作
3.1 8个基本指令
上图中,工作内存和主内存之间怎么进行交互操作,怎么保存线程之间的数据一致性(类比我们之前提到过的缓存一致性协议)。Java内存模型定义了8种操作来完成,虚拟机实现时需要保证这八种操作是原子操作(对于double、long类型的变量,load、store、read、write操作在某些平台允许例外)
lock
(锁定):作用于主内存,它把一个变量标识为一条线程独占的状态;unlock
(解锁):主用于主内存,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;read
(读取):作用于主内存,将一个变量的值从主内存传输到线程的工作内存,以便随后load使用;load
(载入):作用于工作内存的变量,它把read操作得到的值放入工作内存的变量副本中;use
(使用):作用于工作内存的变量,将工作内存一个变量的值传递给操作引擎;assign
(赋值):作用于工作内存的变量,它将一个从执行引擎接收到的值赋给工作内存的变量;store
(存储):作用于工作内存的变量,它把工作内存一个变量的值传送到主内存中,以便随后的write操作使用write
(写入):作用于主内存的变量,它把store操作从工作内存得到的值
3.2 8个基本指令的基本规则
java内存模型规定了8个原子指令在操作时必须遵循以下的规则:
- read和load、store和write操作不同单独出现;
- 不允许一个线程丢弃一个变量最近的assign操作,也就是在这个线程结束前,必须把工作内存改变了的变量同步回主内存;
- 不允许一个线程无原因(没有发生任何assign操作)的把数据从工作内存同步回主内存;
- 一个新的变量只能从主内存中诞生,不允许在工作内存中使用一个未被初始化(load或assign)的变量;
- 一个变量在同一时刻只允许一个线程对其进行lock操作,但lock操作能被同一线程执行多次,多次lock后,执行相同次数的unlock,变量才会被解锁;
- 如果对一个变量执行了lock操作,那么将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign;
- unlock操作前,必须有lock操作;也不允许去unlock其他线程lock的变量;
- unlock前,必须先把此变量同步回主内存(store、write操作)。
3.3 volatile关键字
掌握以下三个知识点:
- 保证了变量可见性;
- 禁止重排序;
- 没有实现原子性
底层是通过lock前缀指令实现的,关于lock前缀,参见volatile与lock前缀指令
lock指令的几个作用:
- 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
- lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
- 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序
4. 原子性、可见性、有序性
4.1 原子性
由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store、write
(大致可以认为基本数据类型的访问时具备原子性的,但实际long和double是非原子协定的,但基本可以忽略,hotspot虚拟机实现了long和double操作的原子性,只是虚拟机规范没有规定而已)
如果我们需要更大范围的原子性,可以使用lock和unlock执行。对应的就是synchrinized
关键字的monitorenter
和monitorexit
进行使用
4.2 可见性
可见性是指一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile
、synchronized
、final
能实现变量的可见性
4.3 有序性
Java程序中天然的有序性可以总结为:如果在本线程进行观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无须的。volatile
和synchronized
能保证有序性