volatile的由浅入深

54 阅读6分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

volatile修改的变量有2大特点

特点:可见性,有序性。

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。

当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量

内存语义:volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

内存屏障

定义:也称为内存栅栏,是同步屏障指令,编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免了代码的重排序。

其实也是一种JVM指令,java内存模型的重拍规则会要求java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些指令,volatile实现了java内存模型中的可见性和有序性,但保证不了原子性。

所有的读操作都能获得内存屏障之前的所有写操作的最新结果(实现可见性)。

注意:对于一个volatile的写,happens-before与任意后续对这个volatile的读,也叫写后读。

按上面理解,volatile凭什么保证可见性和有序性?——–》内存屏障

内存屏障的源码分析:

位于Unsafe类,的以下方法:

public native void loadFence();
public native void storeFence();
public native void fullFence();Copy

于Unsafe.java中的方法名对应,在openJDK8中可找到。

后面可从java的底层原理unsafe.cpp的c++源码中找到对应的方法实现。

在后面的实现中的OrderAccess.hpp中,就存在着volatile内存屏障的四种模型

**loadload() :**保证load1的读操作在load2及后续读取操作之前执行。

**storestore() :**在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存。

**loadstore() :**在store2及其后的写操作执行前,保证load1的读操作结束(数据完全写到主内存中)。

**storeload() :**保证store1的写操作已刷新到主内存之后,load2机器后的读操作才执行。

happens-before的volatile变量规则

如果第一个操作为volatile的读操作,那么后面不管操作是什么,都不能重排序,保证了读之后不会被重排到读之前。

只要我后面的操作是volatile的写操作时,不管之前是什么操作,都不能重排序。保证了写之前的操作不会被重排到volatile写之后。

当第一个操作为volatile写操作,后面的volatile读操作,也不能重排序。

总结

**volatile写:**volatile禁止普通写与volatile的写重排序。volatile相当于一个屏障,把两者隔离开来。

在写操作中,防止上面的volatile写操作与下面可能有的volatile读或者写操作重排序。

最终,重要的一点,也就是volatile写操作上面的storestore屏障和下面的storeload屏障这一段。(happens-before先行发生原则的对应。)

两个屏障保证了写的安全性,不会被影响。

屏障保证volatile写不会被其他写重排序。

volatile读:

在每一个volatile读操作后面插入一个loadload屏障,之后再插入一个loadstore屏障。

屏障保证volatile读不会被后面的普通读写重排序。

这样就防止出现脏读。

volatile变量的读写过程

Java内存模型中定义的8种工作内存与主内存之间的原子操作

read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)

上述不是加黑的操作只能够保证单条指令的原子性,如果出现了多条指令,没有进行大面积的加锁。

所以,JVM提供了另外两个原子指令。(lock和unlock),都作用于主内存。

i++的在线程中问题

在i++中,底层字节码逻辑实际上时有三步:

getfield拿到原始的i;

执行iadd加1操作。

执行putfield写操作把累加后的值写回。

而原子性是指一个操作是不可中断的,就算是在多线程,一旦开始执行也不会被其他线程影响到。

所以单i++不具备有原子性。

执行原理:(出问题的原因)如果第二个线程在第一个线程读取旧值和写回新值期间读取了i的值,那么第二个线程就会与第一个线程一起看到同一个值。然后呢,在执行相同的+1操作,就会导致线程安全问题。

**注意:**如果要使用add方法,就必须使用sync关键字修饰,可以保证线程安全。

为什么volatile无法保证原子性——————–volatile的操作有很多个步骤,并非原子。

**重排序:**数据具有依赖性,禁止重排序。

**数据依赖性:**两个操作访问同一变量,如果两个操作有一个为写操作,就存在数据依赖性。

DCL双端检锁的发布(单例模式)

public class SafeDoubleCheckSingleton {    
//通过volatile声明,实现线程安全的延迟初始化。    
private static SafeDoubleCheckSingleton singleton;    
// 私有化构造方法   
 private SafeDoubleCheckSingleton() {    }    
// 双重锁设计    
public static SafeDoubleCheckSingleton getInstance() {        
if (singleton == null) {            
// 1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象            
synchronized (SafeDoubleCheckSingleton.class) {                
if (singleton == null) {                    
// 隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取                    
// 原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序                    singleton = new SafeDoubleCheckSingleton();                }            }        }        // 2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象        return singleton;    }}

singleton没有加volatile在多线程并发下,会导致指定重排序,导致初始化对象和设置内存地址的值对调,可能出现为null等问题。

(new的过程会先申请一个内存,然后将实例存放到内存中,将内存地址指向对象)

如果不加volatile也可以实现以上。可以使用静态内部类

静态内部类的实现
public class SingletonDemo {    
public SingletonDemo() {    }        
private static class SingletonDemoHandler{        
private static SingletonDemo singletonDemo = new SingletonDemo();    
}        public static SingletonDemo getInstance(){       
 return SingletonDemoHandler.singletonDemo;    
}}

java写了一个volatile关键字系统底层加入内存屏障?

当使用了volatile时,底层字节码就会产生一个flags:ACC_volatile标识,就会告诉底层需要调屏障来禁止重拍和可见。(理解来说:JMM把字节码生成机器码时,如果操作时volatile变量,就会根据JMM的要求,在相应的位置插入内存屏障指令