JMM(Java 内存模型 Java Memory Model,简称 JMM) 本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式,并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见。
关键技术点围绕多线程的原子性、可见性和有序性展开的。
JMM定义了线程和主内存之间的抽象关系:
1.线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
2.每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
总结:
1.我们定义的所有共享变量都储存在物理主内存中
2.每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
3.线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
4.不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
1.可见性
可见性指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中。
导致不可见原因:每个线程都有自己的工作内存,线程都是从主内存中拷贝共享变量的副本值;
每个线程都是在自己的工作内存中操作共享变量的
解决方法:
方式一:加锁 synchronized 修饰代码块和方法
在线程获取到锁时,会清空自己的工作内存,再从主存中读取最新的值
方式二:对共享的变量进行volatile 关键字修饰 只能修饰实例变量和类变量
一旦有线程改变了volatile修饰共享变量的值,其他线程会立即失效,其他线程如果要读取共享变量的值,只能从主内存中重新读取最新的值
2.原子性
指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰。
解决方法:
方法一: 加锁 synchronized (悲观锁)
方法二:原子类 CAS比较交换机制 (乐观锁)--Compare And Swap(比较再交换)
3.有序性(指令重排)
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。
指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读",简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。
解决方法:内存屏障
内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。
基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障(写写屏障)。
在每个volatile写操作的后面插入一个StoreLoad屏障(写读屏障)。
在每个volatile读操作的后面插入一个LoadLoad屏障(读读屏障)。
在每个volatile读操作的前面插入一个LoadStore屏障(读写屏障)。
Java内存屏障主要有Load和Store两类。
对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。
对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
对于Load和Store,在实际使用中,又分为以下四种:
LoadLoad 屏障序列:Load1,Loadload,Load2 确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。
StoreStore 屏障序列:Store1,StoreStore,Store2 确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。
LoadStore 屏障序列: Load1; LoadStore; Store2 确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。
StoreLoad 屏障序列: Store1; StoreLoad; Load2 确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。