ToB企服应用市场:ToB评测及商务社交产业平台

标题: volatile关键字最全原理分析 [打印本页]

作者: 诗林    时间: 2024-9-28 19:33
标题: volatile关键字最全原理分析
介绍

volatile是轻量级的同步机制,volatile可以用来解决可见性和有序性问题,但不保证原子性。
volatile的作用:
底层原理

内存屏障

volatile通过内存屏障来维护可见性和有序性,硬件层的内存屏障重要分为两种Load Barrier,Store Barrier,即读屏障和写屏障。对于Java内存屏障来说,它分为四种,即这两种屏障的分列组合。
插入一个内存屏障,相称于告诉CPU和编译器先于这个下令的必须先执行,后于这个下令的必须后执行。对一个volatile字段进行写操作,Java内存模子将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。
可见性原理

当对volatile变量进行写操作的时候,JVM会向处理器发送一条Lock#前缀的指令

而这个LOCK前缀的指令重要实现了两个步调:
缘故原由在于缓存同等性协议,每个处理器通过总线嗅探和MESI协议来检查自己的缓存是不是过期了,当处理器发现自己缓存行对应的内存地点被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读随处理器缓存中。
缓存同等性协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。
总结一下:
有序性原理

volatile 的 happens-before 关系

happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  1. //假设线程A执行writer方法,线程B执行reader方法
  2. class VolatileExample {
  3.     int a = 0;
  4.     volatile boolean flag = false;
  5.    
  6.     public void writer() {
  7.         a = 1;              // 1 线程A修改共享变量
  8.         flag = true;        // 2 线程A写volatile变量
  9.     }
  10.    
  11.     public void reader() {
  12.         if (flag) {         // 3 线程B读同一个volatile变量
  13.         int i = a;          // 4 线程B读共享变量
  14.         ……
  15.         }
  16.     }
  17. }
复制代码
根据 happens-before 规则,上面过程会创建 3 类 happens-before 关系。

由于以上规则,当线程 A 将 volatile 变量 flag 更改为 true 后,线程 B 能够迅速感知。
volatile 禁止重排序

为了性能优化,JMM 在不改变正确语义的前提下,会答应编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。
Java 编译器会在天生指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
JMM 会针对编译器订定 volatile 重排序规则表。

为了实现 volatile 内存语义时,编译器在天生字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不大概的,为此,JMM 接纳了保守的计谋。
volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。


为什么不能保证原子性

在多线程情况中,原子性是指一个操作或一系列操作要么完全执行,要么完全不执行,不会被其他线程的操作打断。
volatile关键字可以确保一个线程对变量的修改对其他线程立即可见,这对于读-改-写的操作序列来说是不够的,由于这些操作序列自己并不是原子的。思量下面的例子:
  1. public class Counter {
  2.     private volatile int count = 0;
  3.    
  4.     public void increment() {
  5.         count++; // 这实际上是三个独立的操作:读取count的值,增加1,写回新值到count
  6.     }
  7. }
复制代码
在这个例子中,尽管count变量被声明为volatile,但increment()方法并不是线程安全的。当多个线程同时调用increment()方法时,大概会发生以下情况:
在这种情况下,虽然increment()方法被调用了两次,但count的值只增加了1,而不是盼望的2。这是由于count++操作不是原子的;它涉及到读取count值、增加1、然后写回新值的多个步调。在这些步调之间,其他线程的操作大概会干扰。
为了保证原子性,可以使用synchronized关键字或者java.util.concurrent.atomic包中的原子类(如AtomicInteger),这些机制能够保证此类操作的原子性:
  1. public class Counter {
  2.     private AtomicInteger count = new AtomicInteger(0);
  3.    
  4.     public void increment() {
  5.         count.getAndIncrement(); // 这个操作是原子的
  6.     }
  7. }
复制代码
在这个修改后的例子中,使用AtomicInteger及其getAndIncrement()方法来保证递增操作的原子性。这意味着纵然多个线程同时实行递增计数器,每次调用也都会正确地将count的值递增1。
关于作者

来自一线程序员Seven的探索与实践,持续学习迭代中~
本文已收录于我的个人博客:https://www.seven97.top
公众号:seven97,接待关注~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4