一、三大性质
原子性、有序性、可见性
java内存模型中定义了8种操作都是原子的:lock、unlock、read、load、use、assign、store、write
如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用,反应到java代码中就是---synchronized关键字,也就是说synchronized满足原子性。volatile不满足原子性。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
synchronized和volatiled都满足有序性。
synchronized和volatiled都满足可见性。
二、JMM
主内存对应PC的内存,Java线程工作内存对应CPU的高速缓存
三、volatile关键字
作用:
1、volatile可以保证可见性,A线程修改了volatile值,B线程立马可见。
2、volatile不能保证原子性:
类似于i++操作,涉及到读取i到栈顶,提取加1,结果压栈,写回主内存。
多个线程进行++,使用volatile还是会有线程安全问题。
一个线程++后,可以立马写回主内存,其他cpu执行其他线程,cpu高速缓存内的i值也会失效,而后更新。
但是其他线程读取到栈顶的i值已经不是最新的了,结果还是会错误。
所以volatile可以用在多线程对变量原子性操作的场景,来确保线程安全。
如果是非原子性操作,可以考虑AtomicInteger或者synchronized。
volatile可见性的实现原理:
使用了内存屏障,来屏蔽操作系统优化作出的某些不安全的指令重排。
内存屏障的理解:
1、后续的Load肯定不能重排序到我Store前面。
2、Store的时候,Store的对象肯定都初始化完成了。
比如StoreLoad 屏障
执行顺序: Store1—> StoreLoad—>Load2
确保Load2和后续的Load指令读取之前,Store1的数据对其他处理器是可见的。
具体是通过一个lock 寄存器+0(空操作)指令实现的,这个操作会将变量修改store,write主内存,并使其他cpu高速缓存内该变量失效,重新拉取。相当于分布式缓存的更新策略。先更新主存,再删除缓存。
如果是单cpu,那么不不需要内存屏障来禁止指令重排,因为指令重排,还是会保证语义正确,不会影响最终结果,产生不安全的原因多核多线程。
volatile双锁单例中的应用:
public class Singleton {
public static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
单例变量如果不用volatile修饰:
new Singleton(),执行改行代码需要三个步骤,①开辟内存空间②初始化数据③刷新实例回主存。现在,线程A进入双重非空判断,创建实例,如果操作系统指令重排,使②③颠倒,那么实例先刷新回主存,还没有初始化,这时候线程B进入第一重非空判断,不为空,直接返回了不完整的实例,这就出现了线程安全问题。
所以用volatile修饰以后,内存屏障禁止指令重排,首先必须完全初始化,才会刷新回主存,其次其他线程的load操作需要在store之后。