正文
一、CPU缓存结构
由于CPU的运算速度比主存(物理内存)的存取速度快很多,为了提高处理速度,现代CPU不直接和内存进行通信,而是在CPU和主存之间设计了高速缓存(Cache),越靠近CPU层的高速缓存速度越快,容量越小。如下图
每一级高速缓存中所存储的数据都是下一级高速缓存中的一部分,L1最靠近CPU所以读取速度最快。L1和L2高速缓存只能被一个单独的CPU内核使用,L3高速缓存可以被同一个CPU芯片上的所有CPU内核共享,而主内存可以由系统中所有的CPU共享。CPU读取数据时,首先从L1层高速缓存中读取数据,如果没有读取到,再到L2,L3高速缓存中读取数据,如果都没有读取到数据,就会去主存中读取数据。
二、并发编程的三大问题
原子性
原子性就是指不可中断的一个或者一些列操作,不能被线程调度机制打断的操作。如下面代码
public class Test01 { private int count=0; public void increase(){ System.out.println(count++); } }
将该java文件通过一下指令编译成class文件。javac -encoding UTF-8 Test01.java。通过javap反编译。
可知++操作并不是原子性的,而是进行了取值,运算,赋值,返回四个操作,在多线程并发的情况下,会发生原子性的问题,所以不是线程安全的。
可见性
一个线程对共享变量的修改,另一个线程能够立即可见,那么这个变量具有内存可见性。
JAVA内存模型
Java内存模型只是一种规范,抽象的概念,不是具体存在的。注意和JVM内存结构不同。
java内存模型规定
1、所有的变量存储在主内存中
2、每一个线程都有自己的工作内存,且对变量的操作都是在自己的工作内存中进行的。
3、不同的线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递(线程通信)
在java中,所有的局部变量,方法定义的参数都不会在线程之间共享,所以也就没有可见性的问题。而定义在堆中的共享变量,类,数组元素等都是线程共享的,存在可见性的问题。解决可见性问题可以使用volatile关键字解决。
有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。但是在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,它不保证程序各个语句的执行顺序严格按照代码顺序执行,但是它会保证程序最终的结果和顺序执行结果一致。但是这只是针对单线程运行,如果在多线程条件下,重排序会出现内存可见性问题,导致线程不安全。
重排序分三种类型
1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
JAVA中synchronized、volatile可以解决重排序。
三、JMM内存模型
JMM规定将所有的变量(不包括局部变量)都存放在公共内存中,当线程使用变量时会把主存的变量复制到自己的工作空间(私有内存),线程对变量的读写操作,都是在自己的工作空间内完成的。操作完成之后,再把自己私有内存的变量刷新到主内存中。但是如果两个或者多个线程同时操作同一个变量就会发生可见性问题。
JMM定义了一套自己的主存与工作内存之间的交互协议,即变量如何从主存到工作内存,又如何从工作内存写入到主内存。该协议有8种操作,并且要求JVM具体实现必须保证其中每一种操作都是原子性的。
这八种操作必须满足以下规则
不允许read和load、store和Write操作单独出现。以上两个操作必须按照顺序执行,但没有保证必须连续执行,也就是在read/load、store/write之间可以插入其他指令。
不允许一个线程丢弃它最近的assign操作,也就是线程使用assign赋值之后,必须Write到主存中。
不允许一个线程无原因(没有assign操作)把数据从线程的工作内存同步到主存中。
一个新的变量只能从主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,也就是说一个变量必须经过load和assign之后行use和store。
一个变量在同一时刻只允许一个线程给它执行lock操作。但lock操作可以被同一个线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,才会释放对象。
如果对一个变量执行lock,将会清空工作内存中此变量的值,执行引擎使用这个变量前,需要重新load或者assign操作初始化变量的值。
如果一个变量事先没有被lock锁定,就不允许执行unlock,也不允许unlock被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须把此变量同步回主存(执行 store和Write)
四、JMM如何解决有序性问题
JMM提供了自己的内存屏障指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和CPU重排序(不是所有的编译器重排序都要禁用)。
JMM内存屏障主要有Load和Store两类
Load Barrier(读屏障):在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主存中加载数据。
Store Barrier(写屏障):在写指令之后插入写屏障,能让写入缓存的最新数据写回主存。
但是在实际使用时,会对Load Barrier和Store Barrier 组合使用
LoadLoad(LL)屏障:在执行预加载的指令序列中,通常需要显示的声明LoadLoad屏障,因为这些Load指令可能会依赖其他CPU执行的Load指令结果。
StoreStore(SS)屏障:通常情况下CPU不能保证从高速缓存向主存按顺序刷新数据,那么就需要SS,屏障。例如 store1;storestore;store2; 在Store2以及后续的写入操作之前,可以及时看到Store1对变量的操作。
LoadStore(LS)屏障:该屏障用于在数据写入操作执行前确保完成数据读取。
StoreLoad(SL)屏障:该屏障用于数据在读取执行操作之前,确保完成数据的写入。是开销最大,兼具其他三种屏障效果,在现代的多核CPU大多支持该屏障。
参考:
《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著