单页面零售网站,手机主题制作网站,周口建设网站,昌吉网站建设电话介绍
volatile是轻量级的同步机制#xff0c;volatile可以用来解决可见性和有序性问题#xff0c;但不保证原子性。
volatile的作用#xff1a;
保证了不同线程对共享变量进行操作时的可见性#xff0c;即一个线程修改了某个变量的值#xff0c;这新值对其他线程来说是…介绍
volatile是轻量级的同步机制volatile可以用来解决可见性和有序性问题但不保证原子性。
volatile的作用
保证了不同线程对共享变量进行操作时的可见性即一个线程修改了某个变量的值这新值对其他线程来说是立即可见的。禁止进行指令重排序。
底层原理
内存屏障
volatile通过内存屏障来维护可见性和有序性硬件层的内存屏障主要分为两种Load Barrier,Store Barrier即读屏障和写屏障。对于Java内存屏障来说它分为四种即这两种屏障的排列组合。
每个volatile写前插入StoreStore屏障为了禁止之前的普通写和volatile写重排序还有一个作用是刷出前面线程普通写的本地内存数据到主内存保证可见性每个volatile写后插入StoreLoad屏障防止volatile写与之后可能有的volatile读/写重排序每个volatile读后插入LoadLoad屏障禁止之后所有的普通读操作和volatile读操作重排序每个volatile读后插入LoadStore屏障。禁止之后所有的普通写操作和volatile读重排序
插入一个内存屏障相当于告诉CPU和编译器先于这个命令的必须先执行后于这个命令的必须后执行。对一个volatile字段进行写操作Java内存模型将在写操作后插入一个写屏障指令这个指令会把之前的写入值都刷新到内存。
可见性原理
当对volatile变量进行写操作的时候JVM会向处理器发送一条Lock#前缀的指令 而这个LOCK前缀的指令主要实现了两个步骤
将当前处理器缓存行的数据写回到系统内存将其他处理器中缓存了该数据的缓存行设置为无效。
原因在于缓存一致性协议每个处理器通过总线嗅探和MESI协议来检查自己的缓存是不是过期了当处理器发现自己缓存行对应的内存地址被修改就会将当前处理器的缓存行置为无效状态当处理器对这个数据进行修改操作的时候会重新从系统内存中把数据读到处理器缓存中。 缓存一致性协议当CPU写数据时如果发现操作的变量是共享变量即在其他CPU中也存在该变量的副本会发出信号通知其他CPU将该变量的缓存行置为无效状态因此当其他CPU需要读取这个变量时就会从内存重新读取。 总结一下
当volatile修饰的变量进行写操作的时候JVM就会向CPU发送LOCK#前缀指令通过缓存一致性机制确保写操作的原子性然后更新对应的主存地址的数据。处理器会使用嗅探技术保证在当前处理器缓存行主存和其他处理器缓存行的数据的在总线上保持一致。在JVM通过LOCK前缀指令更新了当前处理器的数据之后其他处理器就会嗅探到数据不一致从而使当前缓存行失效当需要用到该数据时直接去内存中读取保证读取到的数据时修改后的值。
有序性原理
volatile 的 happens-before 关系
happens-before 规则中有一条是 volatile 变量规则对一个 volatile 域的写happens-before 于任意后续对这个 volatile 域的读。
//假设线程A执行writer方法线程B执行reader方法
class VolatileExample {int a 0;volatile boolean flag false;public void writer() {a 1; // 1 线程A修改共享变量flag true; // 2 线程A写volatile变量} public void reader() {if (flag) { // 3 线程B读同一个volatile变量int i a; // 4 线程B读共享变量……}}
}根据 happens-before 规则上面过程会建立 3 类 happens-before 关系。 根据程序次序规则1 happens-before 2 且 3 happens-before 4。 根据 volatile 规则2 happens-before 3。 根据 happens-before 的传递性规则1 happens-before 4。 因为以上规则当线程 A 将 volatile 变量 flag 更改为 true 后线程 B 能够迅速感知。
volatile 禁止重排序
为了性能优化JMM 在不改变正确语义的前提下会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。
Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
JMM 会针对编译器制定 volatile 重排序规则表。 为了实现 volatile 内存语义时编译器在生成字节码时会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
对于编译器来说发现一个最优布置来最小化插入屏障的总数几乎是不可能的为此JMM 采取了保守的策略。 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
volatile 写是在前面和后面分别插入内存屏障而 volatile 读操作是在后面插入两个内存屏障。 为什么不能保证原子性
在多线程环境中原子性是指一个操作或一系列操作要么完全执行要么完全不执行不会被其他线程的操作打断。
volatile关键字可以确保一个线程对变量的修改对其他线程立即可见这对于读-改-写的操作序列来说是不够的因为这些操作序列本身并不是原子的。考虑下面的例子
public class Counter {private volatile int count 0;public void increment() {count; // 这实际上是三个独立的操作读取count的值增加1写回新值到count}
}在这个例子中尽管count变量被声明为volatile但increment()方法并不是线程安全的。当多个线程同时调用increment()方法时可能会发生以下情况
线程A读取count的当前值为0。线程B也读取count的当前值为0在线程A增加count之前。线程A将count增加到1并写回。线程B也将count增加到1并写回。
在这种情况下虽然increment()方法被调用了两次但count的值只增加了1而不是期望的2。这是因为count操作不是原子的它涉及到读取count值、增加1、然后写回新值的多个步骤。在这些步骤之间其他线程的操作可能会干扰。
为了保证原子性可以使用synchronized关键字或者java.util.concurrent.atomic包中的原子类如AtomicInteger这些机制能够保证此类操作的原子性
public class Counter {private AtomicInteger count new AtomicInteger(0);public void increment() {count.getAndIncrement(); // 这个操作是原子的}
}在这个修改后的例子中使用AtomicInteger及其getAndIncrement()方法来保证递增操作的原子性。这意味着即使多个线程同时尝试递增计数器每次调用也都会正确地将count的值递增1。
关于作者
来自一线程序员Seven的探索与实践持续学习迭代中~
本文已收录于我的个人博客https://www.seven97.top
公众号seven97欢迎关注~