Java关键字volatile篇小总结

本文详细解析volatile关键字的作用,包括防止指令重排序、保证变量可见性和单次读写原子性。通过示例说明其与原子性、long/double处理、内存屏障及应用场景的关联。

volatile

volatile的作用

  • 防止指令重排序
  • 实现变量的可见性
  • 保证单次的读/写具有原子性
    volatile保证可见性的demo:
public class VolatileDemo {
    int a = 1;
    int b = 2;

    public void change() {
        a = 3;
        b = a;
    }

    public void print() {
        System.out.println("a : " + a + ", b : " + b);
    }

    public static void main(String[] args) {
        while (true) {
            final VolatileDemo volatileDemo = new VolatileDemo();
            new Thread(() -> {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                volatileDemo.change();
            }).start();
            new Thread(() -> {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                volatileDemo.print();
            }).start();
        }
    }
}

正常来说只会有两种结果:
a : 1, b : 2 表示执行完print()还没执行change()
a : 3, b : 3 表示执行完change()马上执行print
第三种:
a : 1, b : 3 表示change()执行到一半的时候,print()开始执行,因此出现第一个线程对变量a的修改,第二线程还无法感知到。a :3,b : 2 同理。

volatile与原子性

volatile能保证原子性吗?i++为什么不能保证原子性?
保证单词的读/写操作具有原子性。
i++实际有三步操作,先读取i的值,+1,再设置回对应的对象值。如果多个线程在这三步之间同时操作,就有可能出问题。
volatile不能保证原子性demo:

public class VolatileVisibilityDemo {
    volatile int i;

    public void addI(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        final  VolatileVisibilityDemo volatileVisibilityDemo = new VolatileVisibilityDemo();
        for (int n = 0; n < 1000; n++) {
            new Thread(() -> {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                volatileVisibilityDemo.addI();
            }).start();
        }
        Thread.sleep(10000); // 等待10秒,保证上面程序执行完成
        System.out.println(volatileVisibilityDemo.i);
    }
}

结果:

971

在这里插入图片描述
JMM的这个示意图为例,如果线程A、B同时拿到了主内存中的变量i,此时值为0,两个线程在各自的内存空间中对i的值+1以后,如果A线程先写回到主内存中i的空间,此时B线程已经读取到了0的值,也+1,也把1写回到主内存i中的地址,这时候最终的结果就成了1,所以上述demo的结果不为1000

volatilelongdouble

之前32位机器上共享的longdouble变量为什么要用volatile?现在也要吗?
32位机器上,会把longdouble设置为两个32位长度,高低32位的操作可能是不一致的,因此不能保证原子性。但是目前各种平台上的上用虚拟机选择把64位的读写操作作为原子操作独代,所以一般不用专门将longdouble声明为volatile

volatile与可见性

volatile是如何实现可见性的? 内存屏障。
内存屏障又称内存栅栏,是一个 CPU 指令。

在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止 + 特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。

指令重排的例子:

public void mySort() {
    int x = 1;  // 1
    int y = 1;  // 2
    x = x + 1;  // 3
    y = x * x;  // 4
}

上述4条指令中,可能的执行顺序为1 2 3 4,但是因为1 2之间没有数据依赖关系,所以可能的结果为2 1 3 4以及1 3 2 4,但是不可能会出现3 1 2 4,因为3 1之间存在这数据依赖的关系。

volatile禁止指令重排的demo:

public class ResortDemo {
    int a = 0;                                                     
    boolean flag = false;                                        

    public void method1() {
        a = 1;                                                      // 1
        flag = true;                                                // 2
    }

    public void method2() {
        if (flag) {                                                 // 3
            a = a + 5;                                              // 4
            System.out.println("a value is : " + a);                // 5
        }
    }

    public static void main(String[] args) {
        ResortDemo resortDemo = new ResortDemo();                  
        new Thread(resortDemo::method1).start();                    
        new Thread(resortDemo::method2).start();
    }
}

如果按正常的顺序执行,1 2 3(true) 4 5,即

a = 1;                                                      // 1
flag = true;                                                // 2
a = a + 5;                                                  // 4
System.out.println("a value is : " + a);                    // 5

最终得到的结果是a value is : 6
但是aflag之间没有数据依赖关系,就有可能出现的执行顺序是2 3(true) 4 5 1,即

flag = true;                                                // 2
a = a + 5;                                                  // 4
System.out.println("a value is : " + a);                    // 5
a = 1;                                                      // 1

这个时候结果会是a value is : 5

volatile与有序性

volatile是如何实现有序性的? happens-before原则
happens-before规则中有一条是volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个 volatile 域的读。上述demo添加了volatile以后,相当于在2中对volatile变量flag的写入、2中flag的读取添加了内存屏障,保证了指令不会被重排序:

public class ResortDemo {
    int a = 0;                                                     
    volatile boolean flag = false;                                        

    public void method1() {
        a = 1;                                                      // 1
        flag = true;                                                // 2
    }

    public void method2() {
        if (flag) {                                                 // 3
            a = a + 5;                                              // 4
            System.out.println("a value is : " + a);                // 5
        }
    }

    public static void main(String[] args) {
        ResortDemo resortDemo = new ResortDemo();                  
        new Thread(resortDemo::method1).start();                    
        new Thread(resortDemo::method2).start();
    }
}

根据happens-before原则,有三个happens-before关系:

  1. 根据程序次序关系,1 happens-before 2 且 3 happens-before 4
  2. 根据volatile规则,2 happens-before 3
  3. 根据happens-before传递性规则,1 happens-before 4

在这里插入图片描述
因为以上规则,当线程 A 将 volatile 变量 flag 更改为 true 后,线程 B 能够迅速感知。

volatile保证有序性的另一个demo:

public class Singleton {   
    private static Singleton instance = null;     // 1
    // 私有化构造器  
    private Singleton() {}   					  // 2

    public static Singleton getInstance() {       // 3
        if (instance == null) {                   // 4    第一次检查
            synchronized(Singleton.class) {       // 5    加锁
                if (instance == null) {           // 6    第二次检查
                    instance = new Singleton();   // 7    实例化
                }                                 // 8
            }                                     // 9
        }                                         // 10
        return instance;                          // 11
    }                                             // 12
}                                                 // 13

上述是单例模式的双重检查代码,其中,语句7可以进一步分解为以下步骤:

memery = allocate(); // 7.1 分配内存
instance(memory);    // 7.2 初始化内存
instance = memory;   // 7.3 将内存地址指向给对象

双重检查的代码的问题在于当存在两个线程同时访问该段代码时,假设线程A执行到了7,并进行了指令重排序,排序的结果如下:

memery = allocate(); // A 7.1 分配内存
instance = memory;   // A 7.3 将内存地址指向给对象
instance(memory);    // A 7.2 初始化内存

对单个线程而言,这样的指令重排不影响最终的结果,当A执行到上述的步骤2,即A 7.3时,instance该对象已经是非空,已经分配到了内存。此时线程B执行到了4,检查时发现instance == null不满足,直接退出返回这个分配到了内存,但是未初始化的对象,导致后续出错。

解决方案之一是直接在instance加上volatile,禁止指令重排,保证7.1->7.2->7.3的顺序执行。

public class Singleton {   
    private volatile static Singleton instance = null;  // 1
    // 私有化构造器  
    private Singleton() {}   					      // 2

    public static Singleton getInstance() {             // 3
        if (instance == null) {                         // 4    第一次检查
            synchronized(Singleton.class) {             // 5    加锁
                if (instance == null) {                 // 6    第二次检查
                    instance = new Singleton();         // 7    实例化
                }                                       // 8
            }                                           // 9
        }                                               // 10
        return instance;                                // 11
    }                                                   // 12
}                                                       // 13

JMM会针对编译器制定 volatile 重排序规则表。

在这里插入图片描述

NO表示禁止重排序。

为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM 采取了保守的策略。

  • 在每个 volatile 写操作的前面插入一个 StoreStore屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。

内存屏障说明
StoreStore屏障禁止上面的普通写和下面的 volatile 写重排序。
StoreLoad 屏障防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。
LoadLoad 屏障禁止下面所有的普通读操作和上面的 volatile 读重排序。
LoadStore 屏障禁止下面所有的普通写操作和上面的 volatile 读重排序。

在这里插入图片描述
在这里插入图片描述

说下volatile的应用场景?

使用 volatile 必须具备的条件

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。
  • 只有在状态真正独立于程序内其他内容时才能使用 volatile。

参考文章:
volatile-的实现原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值