我们提供安全,免费的手游软件下载!
volatile是一种轻量级的同步机制,用于解决可见性和有序性问题,但并不保证原子性。
volatile的作用包括:
volatile通过内存屏障来维护可见性和有序性。硬件层的内存屏障主要分为两种:Load Barrier(读屏障)和 Store Barrier(写屏障)。在Java内存屏障中,涉及四种屏障的排列组合。
插入内存屏障相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作时,Java内存模型会在写操作后插入一个写屏障指令,将之前的写入值都刷新到内存。
当对volatile变量进行写操作时,JVM会向处理器发送一条Lock#前缀的指令,实现两个步骤:
这是因为缓存一致性协议,每个处理器通过总线嗅探和MESI协议来检查自己的缓存是否过期。当处理器发现自己的缓存行对应的内存地址被修改,会将当前处理器的缓存行置为无效状态。处理器进行修改操作时,会重新从系统内存中将数据读取到处理器缓存中。
缓存一致性协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态。当其他CPU需要读取这个变量时,就会从内存重新读取。
因此,volatile写是先插入内存屏障,然后再更新对应的主存地址的数据。
根据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关系:
因此,线程A将volatile变量flag更改为true后,线程B能够迅速感知。
为了性能优化,JMM在不改变正确语义的前提下,允许编译器和处理器对指令序列进行重排序。JMM提供了内存屏障来阻止这种重排序。
JMM会针对编译器制定volatile重排序规则表。
为了实现volatile内存语义,编译器在生成字节码时会在指令序列中插入内存屏障,针对编译器来说,JMM采取了保守的策略:
volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
在多线程环境中,原子性指一个操作或一系列操作要么完全执行,要么完全不执行,不会被其他线程的操作打断。
尽管volatile关键字可以确保一个线程对变量的修改对其他线程立即可见,但对于读-改-写的操作序列来说是不够的。这是因为这些操作序列本身并不是原子的,例如:
public class Counter {
private volatile int count = 0;
public void increment() {
count++; // 这实际上是三个独立的操作:读取count的值,增加1,写回新值到count
}
}
在这个例子中,尽管count变量被声明为volatile,但increment()方法并不是线程安全的。多个线程同时调用increment()可能导致count的值只增加了1,而不是期望的2,因为count++操作不是原子的。
为了保证原子性,可以使用synchronized关键字或者java.util.concurrent.atomic包中的原子类(如AtomicInteger),这些机制能够保证此类操作的原子性。
来自一线程序员Seven的探索与实践,持续学习迭代中~
本文已收录于我的个人博客: https://www.seven97.top
公众号:seven97,欢迎关注~
热门资讯