volatile底层实现原理

一、并发编程中的三个特性

  • 原子性: 一个或多个操作为一个整体,要么全部执行要么都不执行,synchronized 可以保证代码块的原子性和共享变量的可见性
  • 可见性: 当多个线程共享同一变量时,若其中一个线程对该共享变量进行了修改,那么该修改对其他线程是立即可见的
  • 有序性: 程序执行的顺序与代码的先后顺序相同

二、JMM内存模型

在这里插入图片描述

2.1 JMM数据原子操作

在这里插入图片描述
在这里插入图片描述

2.2 缓存一致性协议(MESI)

多个CPU从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存中的数据,该数据会马上同步回主内存,其它CPU通过总线嗅探机制可以感知数据的变化,从而使自己缓存中的数据失效!

在这里插入图片描述

三、指令重排

   在不影响单线程程序执行结果的前提下,计算机为了最大程度发挥机器的性能,会对机器指令进行重排序优化。指令重排不会对单线程程序的结果产生影响,但他可能导致多线程程序出现非预期结果。
在这里插入图片描述

重排序会遵循as-if-serial和happens-before规则:

1. as-if-serial不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变, 编译器,runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作进行重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能会被编译器和处理器进行重排序

2. happens-before

  • 1.程序顺序原则:在一个线程内必须保证语义串行性,也就是说按照代码顺序执行

  • 2.锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)操作之前,也就是说,如果对一个锁解锁后,再加锁,那么加锁动作必须在解锁动作之后(同一个锁)

  • 3.volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单地说,volatile变量再每次被线程访问时,都强迫从主内存中读取该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,不同时刻,不同的线程总是能够看到该变量的最新值

  • 4.线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见

  • 5.传递性:A先于B,B先于C,那么A必然先于C

  • 6.线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A在线程B的join()方法成功返回后,线程B对共享变量的修改对线程A可见

  • 7.线程中断规则:对线程interrupt()方法的调用先行发生于中断线程的代码检测到中断事件的发生,可通过Thread.interruped()方法检测到线程是否发生中断

  • 8.对象终结规则:一个对象的初始化完成先行发生于该对象的finalize()方法的开始。换句话就是,在对象没有完成初始化之前,是不能调用finalize()方法的

四、CPU层面的内存屏障

  • 写屏障(Store Memory Barrier)通知处理器将写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存
  • 读屏障(Load Memory Barrier)配合写屏障,使得在写屏障之前的内存更新对于读屏障之后的读操作是可见的

- 可见性
 &写屏障(Store Memory Barrier)会保证在该屏障之前的,对共享变量的改动,都会同步到主存中
 &读屏障(Load Memory Barrier)会保证在该屏障之后的,对共享变量的读取,读取的都是主存中的最新数据
- 有序性
 &写屏障(Store Memory Barrier)会确保指令重排时,不会将写屏障之前的代码排在写屏障之后
 &读屏障(Load Memory Barrier)会确保指令重排时,不会将读屏障之后的代码排在读屏障之前

1.如何保证可见性
写屏障(sfence)会保证在该屏障之前的,对共享变量的改动,都会同步到主存中

public void actor2(I_Result r) {
    
    
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障
}

读屏障(lfence)会保证在该屏障之后的,对共享变量的读取,读取的都是主存中的最新数据

public void actor1(I_Result r) {
    
    
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
    
    
 r.r1 = num + num;
 } else {
    
    
 r.r1 = 1;
 }
}

在这里插入图片描述

2.如何保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

public void actor2(I_Result r) {
    
    
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障
}

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

public void actor1(I_Result r) {
    
    
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
    
    
 r.r1 = num + num;
 } else {
    
    
 r.r1 = 1;
 }
}

在这里插入图片描述

五、JVM层面的内存屏障

屏障类型 指令示例 说明
LoadLoad Load1;LoadLoad;Load2 确保Load1的读取操作在Load2及后续所有读取操作之前执行
StoreStore Store1;StoreStore;Store2 在Store2及其后的写操作之前,保证Store1的写操作已刷新到主内存
LoadStore Load1;LoadStore;Store2 在Store2及其后的写操作之前,保证Load1的读操作已结束
StoreLoad Store1;StoreLoad;Load2 保证Store1的写操作已刷新到主内存后,Load2及之后的读写操作才会执行

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果

六、volatile的底层原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 在每个volatile写操作的前面插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • StoreStore屏障将保障上面所有的普通写操作结果在volatile写之前会被刷新到主内存->普通写操作对其他线程可见
  • StoreLoad屏障的作用是避免volatile写操作与后面可能有的volatile读/写操作重排序
    在这里插入图片描述
  • 在每个volatile读操作的后面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障
  • LoadStore屏障用来禁止编译器把上面的volatile读操作与下面的普通读写操作重排序
    在这里插入图片描述
    总之 volatile只能保证可见性和有序性但不能保证原子性,原子性需要通过Synchronized这样的锁机制实现
    在这里插入图片描述

七、相关问题

1.锁机制如何保证共享变量可见性?
当某一个线程进入synchronized代码块前,线程会获得锁,清空工作内存,从主内存拷贝共享变量的最新值到工作内存成为副本,之后执行代码,将修改后的副本的值刷新回主内存中,最后线程释放锁。而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的

2.volatile如何保证共享变量可见性?
&在工作内存中,每次使用volatile变量前必须先从主内存刷新变量的最新值到工作内存
&在工作内存中,每次修改volatile变量后都必须立刻同步回主内存中,用于保证其他线程可以看到当前线程对volatile变量所做的修改

猜你喜欢

转载自blog.csdn.net/qq_40714246/article/details/118966064