volatile是什么
volatile是JVM中最轻量的同步机制。
作用:
- 64位写入的原子性。(不是传统的原子性:复合操作不具有原子性)
- 内存可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入,这个新值对于其他线程来说是立即可见的。
- 禁止重排序(实现有序性)。
如何保持可见性
不加volatile可能导致的问题
volitale不能保证原子性
不加volitale:
加了volitale,不到10w
虽然保证了count++之后的count值保证了可见性,但是count++过程没有保证原子性
要达到10w,对m方法加synchronized
问:是否需要加volatile
答:需要
new对象分三步:1.申请内存,赋值为默认值,2.初始化值,3.将指针指向申请的内存
指令重排可能会将2,3步换位置
如果不加volatile,当第一个线程sync初始化一半(1,3完成,INSTANCE不为null),此时第二个线程进入拿到初始化一半的对象,读取值错误
单例模式
对变量值加了 volitile 之后,一个线程中的改变,在另一个线程中可以立刻看到。
饿汉式
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就完成实例化
* Class.forName("")
* (话说你不用的,你装载它干啥)
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
懒汉式
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr06 {
private static volatile Mgr06 INSTANCE; //JIT
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
懒汉式双重检查为了防止并发,volitale为了防止重排序导致的拿到未初始化的对象
volatile复合操作
public class Main {
public volatile int n;
public static void main(String[] args) {
}
public void add() {
n++;
}
}
********************************************************
javap -c ../out/production/untitled/Main.class
********************************************************
Compiled from "Main.java"
public class Main {
public volatile int n;
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: return
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field n:I
5: iconst_1
6: iadd
7: putfield #2 // Field n:I
10: return
}
实现原理
JMM层:内存屏障
深入理解java内存屏障(volatile实现原理)_内存屏障底层原理_java伟大航路的博客-CSDN博客
定义四种内存屏障是为了维护JMM内存模型,主要是以下三个准则:
(1) 所有volatile读写之间相互序列化。volatile属性进行写操作后,其他CPU能马上读到最新值。
(2) volatile读取操作之后发生的非volatile读写不能乱序到其之前。非volatile读写发生在volatile读之前,可以乱序到其之后。
(3) volatile写操作之前发生的非volatile读写不能乱序到其之后。非volatile读写发生在volatile写之后,可以乱序到其之前。
第一点很好理解,就是volatile的可见性要求。
第二点是为了维护happens-before准则。保证前后操作基于最新的volatile读。给个具体场景进行理解:比如我要在进行volatile读后,根据读到的值进行一些代码逻辑操作,如果这些逻辑重排到了volatile读之前,则可以理解这些逻辑代码都是基于一个旧 volatile 值做的,即逻辑上不满足volatile是最新的值。
第三点和第二点类似,也是为了维护happens-before准则。保证volatile写后数据最新。比如我先进行一段代码逻辑,再进行volatile写,如果这些逻辑重排到volatile写之后,当其他cpu看到volatile写操作时,就无法确实volatile写操作之前的操作是否已经确实地发生了
JMM定义的内存屏障一共有4种:
屏障类型 | 指令说明 |
StoreStore | 写写屏障,插入两个写之间,volatile写之前,禁止前面的普通写或volatile写与当前的volatile写发生重排序 |
StoreLoad | 写读屏障,插入volatile写之后,禁止当前的volatile写与后面的volatile读发生重排序 |
LoadLoad | 读读屏障,插入 volatile读之后,禁止当前的volatile读与后面普通读或volatile读发生重排序 |
LoadStore | 读写屏障,插入volatile读之后,禁止当前的volatile读与后面的普通写或volatile写发生重排序 |
由3个准则可推导
第一步\第二步 | 普通读 | 普通写 | volatile读 | volatile写 |
---|---|---|---|---|
普通读 | LoadStore | |||
普通写 | StoreStore | |||
volatile读 | LoadLoad | LoadStore | LoadLoad | LoadStore |
volatile写 | StoreLoad | StoreStore |
可怕的jvm源码分析(不重要了解即可)
深入理解java内存屏障(volatile实现原理)_内存屏障底层原理_java伟大航路的博客-CSDN博客
x86处理器硬件层:lock前缀,MSEI协议
lock前缀指令的作用
1. 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
2. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。
3. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效。
MSEI协议
首先,volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性(后面会细说这个一致性)的一种方法
对于汇编指令中执行加锁操作的变量,MESI协议在以下两种情况中也会失效:
a. CPU不支持缓存一致性协议。
b. 该变量超过一个缓存行的大小,缓存一致性协议是针对单个缓存行进行加锁,此时,缓存一致性协议无法再对该变量进行加锁,只能改用总线加锁的方式
MESI有四种状态:
四种状态间的转换关系如下:
M状态下,对应的转换如下:
(1)本地读:由于可以直接从本缓存行读取,所以状态仍然是M
(2)本地写:也是直接修改本地的缓存行,因此仍然是M
(3)远程读:由于缓存行的数据和主内存不同,为了让远程能够得到最新的数据,必需将缓存行的数据同步到主内存,然后远程从主内存中读取该数据。这是缓存行和主内存的数据保持了一致,而且数据在本地和远程cache中,因此状态编程S
(4)远程写:同上,缓存需要同步到主内存,然后由远程进行数据修改。这时,缓存行的数据不是远程修改后的数据,因此缓存需要编程无效I。
E状态下的状态转换:
(1)本地读:保持不变
(2)本地写:写了之后,本地的缓存和主存的缓存不一致了,状态变成M
(3)远程读:这时有多个cache共享该数据,状态变成了S
(4)远程写:远程写了之后,本地的缓存和修改后的数据不一致了,状态变为I
S状态下的状态转换:
I状态下的状态转换
————————————————
版权声明:本文为CSDN博主「don't_know」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/besthezhaowen/article/details/125575374