是什么
JMM(Java Memory Model)是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。
如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,那是很大的问题。
如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等,JMM让我们只需要用同步工具和关键字就可以开发并发程序。
最重要的3点内容
重排序:
好处:提高处理速度
重排序3种情况:
1,编译器优化:包括JVM,JIT编译器等
2,CPU指令重排:就算编译器不发生重排序,CPU也可能对指令进行重排
3,内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题
可见性:
为什么会有可见性问题:
CPU有多级缓存,导致读的数据过期
高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层
线程间对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的
如果所有核心都只用一个缓存,那么也就不存在可见性问题
每个核心都会将自己需要的数据读到独占内存种,数据修改后也是写入到缓存中,然后等待刷入到主存中,所以会导致有些核心读取的值是一个过期的值
主内存和抽象内存:
Java作为高级语言,屏蔽了底层细节,用JMM定义了一套读写内存数据的规范,抽象为主内存和本地内存
主内存和抽线内存的关系:
1,所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝
2,线程不能直接读写主内存中的变量,而是只能操作自己的工作内存中的变量,然后再同步到主内存中
3,主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成
happens-before:
happens-before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before
两个操作可以用happens-before来确定它们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的
能保证可见性的措施:
volatile、synchronized、Lock、并发集合、Thread.join()、Thread.start()等
volatile:
是什么:
volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为
如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改
但是开销小,相应的能力也小,volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用
适用场合:
不适用:a++;
适用场合1:boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是由原子性的,而volatile又保证了可见性,所以就足以保证线程安全
适用场景2:作为刷新之前变量的触发器
作用:
1,可见性:读一个volatile变量之前,需要先使用相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存
2,禁止指令重排序优化:解决单例双重锁乱序问题
volatile和synchronized的关系:
volatile可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是由原子性的,而volatile又保证了可见性,所以就足以保证线程安全
总结:
1,volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步
2,volatile属性的读写操作都是无锁的,它不能替代synchronized,因为他没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以是低成本的
3,volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序
4,volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主存中读取
5,volatile提供了happens-before保证,对volatile变量v的写入,happens-before所有其他线程后续对v的读操作
6,volatile可以使得long和double的赋值是原子的
synchronized可见性:
synchronized不仅保证了原子性,还保证了可见性
进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。
原子性:
定义:一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的
Java中的原子操作:
除long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作
所有引用reference的赋值操作,不管是32位机器还是64位机器
java.util.concurrent.atomic.*包中所有类的原子操作
long和double的原子性:
对于64位的值的写入,可以分为两个32位的操作进行写入,不具备原子性,可以使用volatile关键字解决。在32位JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的。实际开发中,商用Java虚拟机不会出现此问题