java基础—Volatile关键字详解

java基础—Volatile关键字详解

并发编程的三大特性:

原子性、可见性和有序性。只要有一条原则没有被保证,就有可能会导致程序运行不正确。volatile关键字 被用来保证可见性,即保证共享变量的内存可见性以解决缓存一致性问题。一旦一个共享变量被 volatile关键字 修饰,那么就具备了两层语义:内存可见性和禁止进行指令重排序。

  • 原子性:就是一个操作或多个操作中,要么全部执行,要么全部不执行。

    例如:账户A向账户B转账1000元,这个么过程涉及到两个操作,(1)A账户减去1000元 (2)B账户增加1000元。这么两个操作必须具备原子性。否则A账户钱少了,B账户没增加。

  • 有序性: 程序执行顺序按照代码先后顺序执行。

    处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致(指令重排),但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。(此处的结果一致指的是在单线程情况下)

    指令重排的理解:单线程侠,如果两个操作更换位置后,对后续操作结果没有影响,可以对这两个操作可以互换顺序。

  • 可见性: 可见性是指多线程共享一个变量,其中一个线程改变了变量值,其他线程能够立即看到修改的值。

    //线程1执行的代码
    int i = 0;
    i = 10;
    //线程2执行的代码
    j = i;
    
    

    CPU1执行线程1代码,CPU执行线程2代码。CPU读取i=0到CPU缓存中,修改i=10到自己缓存,还没更新到主存,此时CPU2读取的i还是主存中i=0,此时j会被赋值为0;

volatile的作用是什么

volatile是一个类型修饰符,JDK1.5之后,对其语义进行了增强。

  • 保证了不同线程之间对共享变量操作的可见性
  • 通过禁止编译器、CPU指令重排序和部分hapens-before规则,解决有序性

volatile如何保证有可见性


volatile保证可见性在JMM层面原理

volatile修饰的共享变量在执行写操作后,会立即刷回到主存,以供其它线程读取到最新的记录。

volatile保证可见性在CPU层面原理

volatile关键字底层通过lock前缀指令,进行缓存一致性的缓存锁定方案,通过总线嗅探和MESI协议来保证多核缓存的一致性问题,保证多个线程读取到最新内容。 lock前缀指令除了具有缓存锁定这样的原子操作,它还具有类似内存屏障的功能,能够保证指令重排的问题。

  • 被volatile修饰的变量在写操作生成汇编指令时,会多出Lock前缀指令,这个指令会引起CPU缓存刷回主存。
  • 刷回主存后,导致其他核心缓存了该内存地址的数据无效,通过缓存一致性协议(MESI)保证每个线程的数据是最新的。
  • 缓存一致性协议保证每个CPU核心通过嗅探在总线上传播的数据来检查自己的缓存是不是被修改,· 当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存
可见性问题的例子

启动线程1和线程2,线程2设置stop=true。查看线程1是否会停止

public class TestVisibility {

    //是否停止 变量
    private static boolean stop = false;
    public static void main(String[] args) throws InterruptedException {


        new Thread(() -> {
            System.out.println("线程 1 正在运行...");
            while (!stop) ;
            System.out.println("线程 1 终止");
        }).start();

        //休眠 10 毫秒
        Thread.sleep(10);
        //启动线程 2, 设置 stop = true
        new Thread(() -> {
            System.out.println("线程 2 正在运行...");
            stop = true;
            System.out.println("设置 stop 变量为 true.");
        }).start();
    }
    
}

可见,线程1并不会停止,而是一直循环下去。这就是CPU缓存导致的一致性问题。

给stop加上volatile关键字,并运行,会发现线程1终止了

volatile如何保证有序性

  1. 内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令)禁止重排序

    Volatile关键字(JMM内存屏障),内存屏障也成为内存栏杆,是一个CPU指令,volatile修饰的变量,在读写操作前后都会进行屏障的插入来保证执行的顺序不被编译器等优化器锁重排序。

    内存屏障的功能有两个:(1)阻止屏障两边的指令重排、(2)刷新处理器缓存(保证内存可见性)image-20230204181902182

  2. 3 个 happens-before 规则实现:

    Happens-Before
    SR-133 提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的 happens-before 规则如下:

    • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
    • 监视器锁规则: 对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
    • volatile 变量规则: 对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。
    • 传递性: 如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

    注意,两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。happens- before 的定义很微妙

    img
单例模式使用volatile保证有序性的例子

为什么变量singleton之前需要加volatile

public class Singleton {
    public static volatile Singleton singleton;

    /**
     * 构造函数私有,禁止外部实例化
     */
    private Singleton() {};

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (singleton) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:

  • 分配内存空间。
  • 初始化对象。
  • 将内存空间的地址赋值给对应的引用。

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

  • 分配内存空间。
  • 将内存空间的地址赋值给对应的引用。
  • 初始化对象

如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量

volatile能保证线程安全吗?

单纯使用 volatile 关键字是不能保证线程安全的

  • volatile 只提供了一种弱的同步机制,用来确保将变量的更新操作通知到其他线程
  • volatile 语义是禁用 CPU 缓存,直接从主内存读、写变量。表现为:更新 volatile 变量时,JMM 会把线程对应的本地内存中的共享变量值刷新到主内存中;读 volatile 变量时,JMM 会把线程对应的本地内存设置为无效,直接从主内存中读取共享变量
  • 当把变量声明为 volatile 类型后,JVM 增加内存屏障,禁止 CPU 进行指令重排

volatile能保原子性吗?

不能volatile 关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是 volatile 没办法保证对变量的操作的原子性

public class atomiciVolitile {
volatile  int  i = 0;
public void addI(){
    i++;
}

public static void main(String[] args) throws InterruptedException {
    atomiciVolitile a=new atomiciVolitile();
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            try {
                Thread.sleep(10);//执行速度太快,没有起到并发作用,等待10毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a.addI();
        }).start();
    }

    Thread.sleep(5000);

    System.out.println(a.i);

}

img

原因

原因:i++其实是一个复合操作,包括三步骤:

  • 读取i的值。
  • 对i加1。
  • 将i的值写回内存。

volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。


volatile不能保证原子性详细解析
  • 变量count=10,线程1对变量自增操作,线程1读取主存中count=10后,被阻塞
  • 线程1对count自增操作,读取count时,由于线程1还没修改(不会导致线程2的count地址所在的缓存行失效),线程2去主存读取的count=10;接着进行+1操作,此时线程2进行+1操作后,还没写回主存,就被阻塞
  • 线程1进行+1操作,此时在线程一的缓存中,count是10
  • 然后线程2刷回主存,主存中count=11,虽然此时 线程1 能感受到 线程2 对count的修改(MESI缓存一致性协议),但由于线程1只剩下对count的写操作了,而不必对count进行读操作了,所以此时 线程2 对count的修改并不能影响到 线程1。于是,线程1 也将 11 写入工作内存并刷到主内存。也就是说,两个线程分别进行了一次自增操作后,count 只增加了 1。下图演示了这种情形:

image-20230213003321016

演示内存可见性的5个例子


public class TestVolatile {

    public static boolean flag = false;

    public static void main(String[] args) {

        new Thread(()->{

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            flag = true;
            System.out.println("线程 " + Thread.currentThread().getName() + " 执行完毕: "
                    + "置  flag= " + flag + " ...");

        }).start();


        while (true){
            // 加上下面三句代码的任意一句,程序都会正常结束:
            // System.out.println("!!");                              //...语句1
            // synchronized (TestVolatile.class) {}                     //...语句2
            //TestVolatile.test2();                                    //...语句3
             // 若只加上下面一句代码,程序都会死循环:
            //  TestVolatile.test1();                                  //...语句4
            
            if (flag) {
                System.out.println("线程 " + Thread.currentThread().getName()
                        + " 即将跳出while循环体... ");
                break;
            }

        }
    }
    public static void test1() {}
    public synchronized static void test2() {}
}

执行结果 :线程并没有停止,可见,线程1将flag置为true对主线程并不可见。

image-20230212232012118

案例1:flag加上volatile关键字
 public volatile  static boolean flag = false;

执行结果:加上voaltile关键字后,falg对于主线程可见,执行break,结束循环。

image-20230212232352756

案例2:while循环中加上System.out.println(“!!”);

这里只在while循环中加上System.out.println(“!!”); ,没有flag加volatile


public class TestVolatile {

    public  static boolean flag = false;

    public static void main(String[] args) {

        new Thread(()->{

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            flag = true;
            System.out.println("线程 " + Thread.currentThread().getName() + " 执行完毕: "
                    + "置  flag= " + flag + " ...");

        }).start();


        while (true){
            // 加上下面三句代码的任意一句,程序都会正常结束:
            System.out.println("!!");                              //...语句1
            // synchronized (TestVolatile.class) {}                     //...语句2
            //TestVolatile.test2();                                    //...语句3  
             // 若只加上下面一句代码,程序都会死循环:
            //  TestVolatile.test1();                                      //...语句4
            if (flag) {
                System.out.println("线程 " + Thread.currentThread().getName()
                        + " 即将跳出while循环体... ");
                break;
            }

        }
    }
    public static void test1() {}
    public synchronized static void test2() {}
}

执行结果 :在输出无数的"!!"后,程序最终结束。说明main线程也能看到对flag的修改,并跳出while循环。

image-20230212233128541

案例3:while循环中加上synchronized (TestVolite.class)

代码不再写贴上来,每个案例只针对讲述的地方更改。

image-20230212234830055

执行结果 :线程结束。使用synchronized关键字,加上了synchronized(类锁)后,mian线程看见了flag的修改。结束while循环。

image-20230212233730365

案例4:while中加上test2()(被synchronized修饰)

image-20230212234845908

执行结果:线程结束

image-20230212234601757

案例5:while循环中加上test1方法

image-20230212234721620

执行结果 :进入死循环,flag为true对主线程不可见

image-20230212234918485

案例总结

案例1和案例5很容易理解,就是可见性问题。案例2和案例3是因为加上了synchronized。synchronized 也可以保证可见性,因为每次运行synchronized块 或者 synchronized方法都会导致线程工作内存与主存的同步,使得其他线程可以取得共享变量的最新值。也就是说,synchronized 语义范围不但包括 volatile 具有的可见性,也包括原子性,但不能禁止指令重排序,这是二者一个功能上的差异。,那为什么案例2也会可见的效果呢?看一下源码,源码中加了类锁(synchronized)

public void println(String x) {  
    synchronized (this) {         // synchronized 块
        print(x);  
        newLine();  
    }  
}  

happens-before (先行发生)原则—《深入理解Java虚拟机》

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 锁定原则:一个unlock操作先发生于后面对同一个锁的lock操作。
  • volatile变量规则:对一个变量的写操作先发生于后面对同一个变量的读操作。
  • 传递规则:操作A先发生于操作B,操作B先发生于操作C,由此可得出A先发生于操作C。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  • 线程中断规则:线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 对象终结规则:个对象的初始化完成先行发生于他的finalize()方法的开始。

比较重要的是前4条,讲解一下前四条

  • 程序次序规则 :中指的是单个线程,在单线程情况下,程序看起来顺序执行(其中可能发生了重排序)且结果一致。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
  • 锁定原则 :无论在单线程还是多线程中,同一个锁出现于被锁定的状态,那么必须先对锁进行释放操作,后面才能继续进行lock操作
  • volatile变量规则 :如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
  • 传递规则 :happens-before具有传递性。

想要了解更详细,请看这篇

java基础—java内存模型(JMM)CPU架构、缓存一致性、重排序、JMM的实现、JMM保证可见性、有序性问题的详解

  • 10
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: volatileJava中的一个关键字,用于修饰变量。它的作用是告诉编译器,该变量可能会被多个线程同时访问,因此需要特殊处理,以保证线程安全。 具体来说,volatile关键字有以下几个特点: 1. 可见性:当一个线程修改了volatile变量的值,其他线程能够立即看到这个修改。 2. 有序性:volatile变量的读写操作会按照程序的顺序执行,不会被重排序。 3. 不保证原子性:虽然volatile变量能够保证可见性和有序性,但是它并不能保证多个线程同时修改变量时的原子性。 因此,如果需要保证原子性,需要使用synchronized关键字或者Lock接口来进行同步。 总之,volatile关键字Java中用于保证多线程访问变量的安全性的一种机制,它能够保证可见性和有序性,但是不能保证原子性。 ### 回答2: Java中的volatile关键字是一种轻量级的同步机制,用于确保多个线程之间的可见性和有序性。它可以用于修饰变量、类和方法。 1. 修饰变量:当一个变量被volatile修饰时,它会被立即写入到主内存中,并且每次读取变量时都会从主内存中重新获取最新的值。这样可以保证多个线程操作同一个变量时的可见性和一致性。 2. 修饰类:当一个类被volatile修饰时,它的实例变量就会被同步,而且每个线程都会获取最新的变量值。这样可以保证多线程操作同一对象时的可见性和一致性。 3. 修饰方法:当一个方法被volatile修饰时,它的调用会插入内存栅栏(memory barrier)指令,这可以保证方法调用前的修改操作都已经被写入主内存中,而方法调用后的读取操作也会重新从主内存中读取最新值。这样可以确保多线程之间的调用顺序和结果可见性。 需要注意的是,volatile并不能完全取代synchronized关键字,它只适用于并发度不高的场景,适用于只写入不读取的场景,不能保证复合操作的原子性。 总之,volatile关键字Java中具有广泛的应用,可以保证多线程之间的数据同步和可见性,但也需要谨慎使用,以免造成数据不一致和性能问题。 ### 回答3: Java中的volatile关键字意味着该变量在多个线程之间共享,并且每次访问该变量时都是最新的值。简单来说,volatile保证了线程之间的可见性和有序性。下面我们详细解释一下volatile的用法和作用。 1. 线程之间的可见性 volatile关键字保证了对该变量的读写操作对所有线程都是可见的。在没有用volatile关键字修饰变量的情况下,如果多个线程并发访问该变量,每个线程都会从自己的线程缓存中读取该变量的值,而不是直接从主存中读取。如果一个线程修改了该变量的值,但是其他线程不知道,那么可能导致其他线程获取到的数据不是最新的,从而引发一系列问题。而用了volatile关键字修饰该变量后,每次修改操作都会立即刷新到主存中,其他线程的缓存中的变量值也会被更新,从而保证了线程之间的可见性。 2. 线程之间的有序性 volatile关键字也保证了线程之间的有序性。多个线程并发访问同一个volatile变量时,JVM会保证每个线程按照程序指定的顺序执行操作。例如,在一个变量被volatile修饰的情况下,多个线程同时对该变量进行读写操作,JVM会保证先执行写操作的线程能够在后续的读操作中获取到最新的变量值。这么做的好处是,可以避免出现线程间操作顺序的乱序问题,从而保证了程序的正确性。 需要注意的是,并不是所有的变量都需要用volatile关键字修饰。只有在多个线程之间共享变量并且对变量的读写操作之间存在依赖关系的情况下,才需要使用volatile关键字。此外,volatile关键字不能保证原子性,如果需要保证操作的原子性,需要使用synchronized或者Lock等其他并发工具。 总之,volatile关键字Java中非常重要的关键字之一,它可以在多个线程之间保证可见性和有序性,从而保证了程序的正确性。在开发过程中,我们应该根据具体情况来选择是否使用volatile关键字,以及如何使用它。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值