volatile关键字详解

2年前 (2022) 程序员胖胖胖虎阿
314 0 0

volatile关键字

  • 1.volatile简介:
  • 2.三大特性
    • 1>.保证可见性:
      • 代码验证:
    • 2>.保证有序性(禁止指令重排序)
      • 有序性的实现原理:
    • 3>.不保证原子性:
      • 解决方式:

1.volatile简介:

volatile 是 JVM 提供的轻量级的同步机制。volatile 关键字可以保证并发编程三大特征(原子性、可见性、有序性)中的可见性和有序性,不能保证原子性

2.三大特性

1>.保证可见性:

加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值。当写线程写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内存。当读线程读一个volatile变量时,JMM会把该线程对应的本地工作内存置为无效,线程将到主内存中重新读取共享变量。

volatile语义实现原理:

先来看两个与CPU相关的专业术语:

  • 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。
  • 缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行。

volatile可见性的实现是借助了CPU的lock指令,lock指令在多核处理器下,可以将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:

  1. 写volatile时处理器会将缓存写回到主内存。
  2. 一个处理器的缓存写回到内存,会导致其他处理器的缓存失效。

代码验证:

例如:

int number = 0;此时number变量是没有可见性的。

volatile int number = 0;前面添加了volatile关键字之后,可以解决可见性问题。

没加volatile关键字之前:

/**
 * 普通类:
 * 为了验证volatile的可见性
 */
public class Test1 {
    int number = 0;

    public void add(){
        this.number = 10;
    }


    public static void main(String[] args) {
        Test1 test1 = new Test1();

        //创建第一个线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"开始执行时,number = "+test1.number);

            try{ Thread.sleep(3000);}catch (Exception e){e.printStackTrace();}
            test1.add();//暂停3秒后,修改number的值。
            System.out.println(Thread.currentThread().getName()+"执行add()方法之后,number = "+test1.number);

        },"Thread_One").start();


        //第二个是main线程
        while (test1.number == 0){
            //如果第二个main线程 可以监测到number值的改变,就会跳出当前循环,执行后续程序。
        }

        System.out.println(Thread.currentThread().getName()+"程序结束!");

    }
}

运行结果:(程序卡死在 循环while (test1.number == 0)里,跳不出来)
volatile关键字详解

加上volatile关键字之后:

/**
 * 变量上加了volatile关键字:
 * 为了验证volatile的可见性。
 */
public class Test2 {
    volatile int number = 0;

    public void add(){
        this.number = 10;
    }


    public static void main(String[] args) {
        Test2 test2 = new Test2();

        //创建第一个线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"开始执行时,number = "+test2.number);

            try{ Thread.sleep(3000);}catch (Exception e){e.printStackTrace();}
            test2.add();//暂停3秒后,修改number的值。
            System.out.println(Thread.currentThread().getName()+"执行add()方法之后,number = "+test2.number);

        },"Thread_One").start();


        //第二个是main线程
        while (test2.number == 0){
            //由于变量number上加了volatile关键字,
            // 使得第二个main线程可以监测到number值的改变,从而跳出了循环。
        }

        System.out.println(Thread.currentThread().getName()+"程序运行结束!");

    }
}

运行结果:( 循环while (test1.number == 0)可以正常结束)
volatile关键字详解

2>.保证有序性(禁止指令重排序)

简单说明:

计算机在执行程序时,为了提高计算性能,编译器和处理器常常会对指令进行重排序,一般分为如下3种:

源代码 ——> 编译器优化的重排 ——> 指令并行的重排 ——>内存系统的重排 ——> 最终执行的指令
volatile关键字详解

解释说明:

·单线程环境下,可以确保程序最终执行结果和代码顺序执行结果的一致性(单线程环境下不用关注指令重排,因为是否重排都不会出错)。处理器在进行重排序时,必须要考虑指令之间的数据依赖性。

·多线程环境中,线程交替执行。由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果也就无法预测。而用volatile关键字修饰的变量,可以禁止指令重排序,从而避免多线程环境下,程序出现乱序执行的现象。

有序性的实现原理:

volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。

volatile通过加内存屏障来实现禁止指令重排序。JMM为volatile加内存屏障有以下4种情况:

  • 在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。

  • 在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。

  • 在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。

  • 在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。

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

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

3>.不保证原子性:

​ 原子性指的是,当某个线程正在执行某件事情的过程中,是不允许被外来线程打断的。也就是说,原子性的特点是要么不执行,一旦执行就必须全部执行完毕。而volatile是不能保证原子性的,即执行过程中是可以被其他线程打断甚至是加塞的。

​ 所以,volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作的。而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。并且在多线程环境中,volatile并不能保证原子性。

代码验证:

加上volatile关键字 ,不能保证原子性:

/**
 * 变量上加了volatile关键字:
 * 为了验证volatile的 不保证原子性。
 */
public class Test1 {

    volatile int number = 0;

    public void add(){
        number++;
    }


    public static void main(String[] args) {
        Test1 test1 = new Test1();


        //创建10个线程
        for (int i = 0;i < 10;i++){
            new Thread(() -> {
                //每个线程执行1001次+1操作
                for (int j = 0;j<100;j++){
                    test1.add();
                }
            },"Thread_"+(i+1)).start();
        }

        //如果正在运行的线程数>2个(除了main线程和GC线程以外,还有其他线程正在运行)
        while(Thread.activeCount() >2){
            Thread.yield();//礼让其他线程,暂不执行后续程序
        }

        System.out.println("执行 1000次 +1操作后,number = "+test1.number);

    }
}

运行结果:(部分数据可能会丢失)
volatile关键字详解

解决方式:

方式一:方法上加 synchronized 关键字。

方式二:利用AtomicInteger类实现原子性。代码如下:

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 变量上加了volatile关键字 ,
 * 但 不能保证原子性 的 解决方式。
 */
public class Test2 {

    volatile int number = 0;

    //解决方式一:方法上加 synchronized 关键字
    public void add(){
        number++;
    }

    //解决方式二:如下
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAtomic(){
        //每调用一次此方法,加个一。
        atomicInteger.getAndIncrement();
    }



    public static void main(String[] args) {
        Test2 test2 = new Test2();


        //创建10个线程
        for (int i = 0;i < 10;i++){
            new Thread(() -> {
                //每个线程执行1001次+1操作
                for (int j = 0;j<100;j++){
                    test2.add();//调用不能保证原子性的方法
                    test2.addMyAtomic();//调用可以保证原子性的方法。
                }
            },"Thread_"+(i+1)).start();
        }

        //如果正在运行的线程数>2个(除了main线程和GC线程以外,还有其他线程正在运行)
        while(Thread.activeCount() >2){
            Thread.yield();//礼让其他线程,暂不执行后续程序
        }

        System.out.println("执行 1000次 +1操作后,number = "+test2.number);
        System.out.println("执行 1000次 +1操作后,atomicInteger = "+test2.atomicInteger);

    }
}

运行结果对比:
volatile关键字详解

版权声明:程序员胖胖胖虎阿 发表于 2022年10月30日 下午12:40。
转载请注明:volatile关键字详解 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...