JMM(Java Memory Model)
是一种规范
是工具和关键字的原理
volatile, synchronized, Lock 等的原理都是JMM
最最重要的3点内容:重排序,可见性,原子性
重排序
-
什么是重排序
线程内部两行代码的执行顺序和java 文件中的执行顺序不一致,指令并没有按照最初编写的代码顺序执行,而是改变了,这就是重排序。
-
重排序的好处:提高处理速度
-
重排序的3种情况:
- 编译器做优化
- CPU指令重排,就算编译器不重排,CPU也可能会对指令重排
- 内存的“重排序”:这个“重排序”不是真正的重排序,是现象上的重排序,看上去和重排序一样的效果,因为缓存的存在(主存和本地内存),不一定实时保持一致,所以可能会表现出和重排序一样的现象
可见性
为什么有可见性问题
CPU有多级缓存导致读的数据过期
- 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间多了cache层
- 线程间对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。
- 如果所有核心都只用一个缓存就不存在内存可见性问题了
- 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存。所以会导致有的核心读到的数据是一个过期值。
什么是主内存和本地内存
Java屏蔽底层细节,用JMM定义了一套读写内存的规范,抽象了主内存、本地内存的概念,使我们不再需要关心一级缓存,二级缓存的问题。
这里说的本地内存并不是给每个线程真的分配一块内存,而是JMM对寄存器,一级缓存,二级缓存等做的抽象。
主内存和本地内存的关系
JMM的规定:
- 所有的变量都存贮在主内存中,同时每个线程都有自己独立的工作内存,工作内存的变量是主内存的拷贝。
- 线程不能直接读写主内存中的变量,只能操作自己工作内存中的变量,然后再同步到主内存中。
- 主内存是多个线程共享的,但线程之间不共享工作内存,如果线程间需要通信,必需通过主内存中转。
Happens-Before原则
什么是Happens-Before
是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before。
如果一个操作happens-before于另一个操作,那么第一个操作对于第二个操作是可见的。
happens-before规则有哪些?
- 单线程规则
- 锁操作(Synchronized和Lock)
- volatile修饰变量
- 线程join
- 传递性: 如果hb(AB),且hb(BC),则hb(AC)成立
- 工具类的happens-before原则
1)线程安全的容器,get一定能看到之前的put动作
2)CountDownLatch
3)Semaphore 信号量
4)CyclicBarrier
volatile关键字
volatile 是什么
volatile是一种同步机制,比Synchronized和Lock相关类更轻量,使用volatile并不会发生上下文切换等开销很大的行为。
开销小,能力也小,volatile做不到Synchronized那样的原子保护,只在很有限的场景下发挥作用。
volatile的适用场合
不适用:a++
适用场合1:如果一个变量自始至终只有被各个线程直接赋值,没有别的操作,可以用volatile代替Synchronized和原子变量,因为赋值自身是原子的,volatile保证可见,所以就线程安全了。
适用场合2:作为变量的触发器
volatile的作用:可见性,禁止重排序
- 可见性:读一个volatile变量之前,需要先使响应的本地内存的变量失效,这样就必需到主内存读取最新值。写一个volatile属性会立刻刷到主内存。
- 禁止指令重排序:例如可以解决单例双重锁乱序的问题。
volatile和Synchronized的关系
如果一个变量自始至终只有被各个线程直接赋值,没有别的操作,可以用volatile代替Synchronized。
volatile提供了happens-before保证。
volatile可以使得long和double的赋值是原子的。
Synchronized不仅保证了原子性,还保证了可见性。不需要多解释了
原子性
什么是原子性
一系类的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,原子是最小单位,是不可分割的。
Java中的原子操作有哪些
- 除long和double之外的基本类型(byte, short, int, float, boolean, char)的赋值操作。
- 所有引用reference的赋值操作,不管32位还是64位机器。
- java.util.concurrent.atomic下面的所有原子类的操作
long 和double的原子性
问题描述:long, double 由于是64位的值,每次写入会被看作是两次单独的写入,每次写入32位,写一半,导致写入不是原子的操作。这种行为是特定于虚拟机实现的,Oracle文档中:“鼓励”虚拟机避免拆分64位,如果可以写一次就行不要拆分,但是鼓励程序员按照拆分64位的情况处理,建议将其声明为volatile的变量。
思考:在32位的jvm上,long,double的操作不是原子的,但是在64位的jvm上是原子的。
现实:但从实际上说,现有的商用JVM,都在虚拟机实现的层面上保证了long, double的写入是原子的了,也都听从了Oracle文档的“鼓励”