名称描述
主内存 :多个线程间共享的内存,在java中是堆内存
工作内存:每个线程独立的内存空间,cpu不能直接访问主内存的数据,需要将数据拷贝到工作内存中。
总线:物理意义上存在的,用于传输数据。因为有总线嗅探机制的存在,CPU具有监听总线上数据传输的能力。
缓存行:是传输数据的最小单元,大小是固定的,包含了状态(M,E,S,I),地址和数据值
保证线程可见性
为什么要保证可见性?
因为多个线程如果同时要操作一个变量时,线程操作的只是备份到工作线程中的变量,就会存在多个线程中的变量不一致的情况。
如何保证可见性
1.cpu的原子操作
1)read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中
2)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
3)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。比如++操作
4)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
5)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
6)write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
7)lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状 。
8)unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
2.cpu总线嗅探机制和MESI(缓存一致性)
1)所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线
2)缓存本身是独立的,但是内存是共享资源,所有的主内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)
3)CPU通过嗅探总线上的数据来更新自己的缓存行状态。当一个处理器执行store操作时,数据到达总线的时候,其它处理器都会得到通知,并将缓存行的状态置为失效。
3.lock指令
1)如果处理器缓存数据被修改,则立即将当前处理器缓存行的数据写回系统内存
2)开启总线嗅探机制和激活缓存一致性协议,当一个线程完成回写,其他线程的缓存行状态改为失效
3)在执行store操作之前,对主内存中缓存行对应的内存区域加锁,执行lock原子操作,并在完成write之后,再执行unlock原子操作,对内存行解锁。
4)相当于一个内存屏障,确保指令重排序时不会把其后面的指令重排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面,即在执行到内存屏障这句指令时,前面的操作已经全部完成。
4.执行流程
1)将主内存变量 read——>load 到工作内存中
2)use——>assign 使用修改变量
3)assign 触发lock锁住主内存变量,在执行了 store——>write 操作后会释放锁unlock,并且由于嗅探机制检测到缓存失效,会让其他线程重新读取最新数据,以此实现线程可见。
禁止指令重排序
什么是指令重排序
指令重排是在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行,以尽可能充分地利用CPU。
指令重排序产生的问题
单例模式的双重检测问题
单利模式的几种实现方式
//饿汉式 在初始化的时候就创建了实例
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
//懒汉式 获取的时候创建实例,因为线程安全问题加同步锁
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//双检锁机制 为了减少锁住的代码
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
双检锁是否需要加volatile
新建对象的时候,其实是执行了四个指令
1)开辟内存空间,设置默认值
2)修改为初始的真实值
3)赋值
4)返回
如果发生指令重排序,有可能出现开辟了内存空间,没有设置为真实值,就赋值了,导致虽然拿到了对象,但是得到的值并不是真实值,只是默认值。这种情况一般不会出现,只有在并发量超高的情况下才有可能发生。
volatile为什么不是原子性
volatile实际上是一系列的原子性操作,如果只是单一的操作,是原子性的,例如简单的赋值 int i=1; i++就不是原子性操作
首先,线程1通过read+load+use+assign对a执行了++,此时,在缓存1中,a=2。但是在执行assign之前,被线程2抢先一步。