为什么会有可见性问题
- 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层
- 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。如果所有个核心都只用一个缓存,那么也就不存在内存可见性问题了。
- 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。
什么是主内存和本地内存
Java 作为高级语言,屏蔽了CPU cache等底层细节,用 JMM 定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM 抽象了主内存和本地内存的概念。
主内存和本地内存的关系
- 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
- 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成
- 所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
什么是happen-before原则
- 解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before。
- 如果两个操作不具备happens-before,那么JVM是可以根据需要自由排序的,但是如果具备happens-before(比如新建线程时,run方法里面的语句一定发生在thread.start()之前),那么JVM也不能改变它们之间的顺序。
Happens-Before规则有哪些?
volatile是什么
- volatile是一种同步机制,比synchronized或者lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为
- 如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改
- 开销小,相应的能力也小,虽然是volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用
volatile的适用场合
- boolean flag,如果一个共享变量自始至终只被各个线程赋值(不依赖于之前的状态),而没有其他的操作,那么就可以用volatile来代替synchronized或者代替其他原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全
- 作为刷新之前变量的触发器
volatile的两点作用
- 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存
- 禁止指令重排序优化:解决单例双重锁乱序问题
volatile总结
- 适用场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag。或者作为触发器,实现轻量级同步。
- volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互坼性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样编译器就不会对这个属性做指令重排序。
- volatile可以使得long和double的赋值是原子的
synchronized保证可见性
- synchronized既保证了原子性,又保证了可见性
- synchronized不仅让被保护的代码安全,还近朱者赤