JMM
# MESI
缓存一致性协议;
由于计算机的内存与CPU的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲。但是也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题:缓存一致性。
MESI协议保证每个缓存中使用的共享变量的副本是一致的;
核心思想:当CPU写数据时,如果操作的变量是共享变量(其它CPU中存在该变量副本),会发出信号通知其它CPU将该变量的缓存行置为无效状态;因此,当其它CPU读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,就会从内存中重新读取;
# volatile与JMM
happens-before 是 JMM 最核心的概念;
JMM把happens-before要求禁止的重排序分为两类:会改变/不会改变程序执行结果的重排序,对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序;
JMM的happens-before向程序员提供了足够强的内存可见性保证;
# 如何保证可见性?
volatile修饰变量:告知程序任何对该变量的访问均需要从共享内存中获取,而对它的修改必须同步刷新回共享内存;
volatile和synchronized和final关键字都能保证可见性;
volatile是禁用CPU缓存实现的,将处理器计算结果全部同步到主内存中,其它线程读取到的数据都是主存中最新值;
volatile是底层靠读屏障,写屏障;
读屏障保证屏障之前的对共享变量的改动都同步到主存中了;
写屏障保证屏障之后的对共享变量读取都是主存中最新的;
synchronized是解锁之前,把所有变量同步回主内存中;
被final修饰的字段在构造器初始化完成,并且构造器没有把this的引用传递出去,该字段对其它线程可见;
# 如何保证有序性?
synchronized:是单线程对一个变量操作;
volatile:底层是Unsafe
类提供的内存屏障方法,对应到JVM上的load,store,read,write等原子方法;
# Volatile原理
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条LOCK前缀的指令,将这个变量所在缓存行的数据写回系统内存;
对volatile变量进行读操作,直接去主内存中读;
在多核处理器下,为了保证各个处理器缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中;
实现原则:
- Lock前缀指令会引起处理器缓存回写到内存;
- 一个处理器的缓存回写到内存会导致其它处理器的缓存无效;
# 双检锁单例
public class Singleton {
public volatile static Singleton INSTANCE = null;
public static Singleton getInstance(){
if(INSTANCE == null){
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
为什么需要两次判空?
主要是synchronzied里面的判空:ab两个线程一开始都判断为空过来抢锁,a抢到锁,创建对象后释放锁,b线程已经进入第一个判断逻辑,还会认为自己是空,抢到锁之后,如果没有第二次判断空,还会创建对象;
为什么需要volatile关键字?
防止指令重排序,创建对象分三步:分配内存,初始化,将instance指向分配的内存;
23容易指令重排序,A线程刚分配内存,还没初始化,将instance指向了分配的内存,此时B线程过来发现不为null,返回了一个还没初始化的对象;