Java并发编程(二)—volatile关键字的作用及使用场景

在这篇文章Java并发编程(一)—Java内存模型以及线程安全中多次提及volatile关键字,这是一个非常重要的概念,主要用于多线程编程中,它确保了变量的可见性和禁止指令重排序,但不保证原子性,下面详细解释volatile关键字的作用和特性:

1、volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证可见性

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。


上篇文中有讲一个例子:🌰线程A和线程B从主内存读取和修改x=1的过程

419f7d591bdf461fa9f107e3838ff3e4.png

由于线程A对变量的修改x=2未立即对线程B可见,造成了线程安全问题,为了确保线程间的即时通信和数据一致性,使用 volatile 关键字是必要的

如果在用volatile修饰变量x后,都会从主内存中读取最新的最新的值,而不是从线程自己的工作内存中读取可能过期的版本

线程A和线程B要进行通信的话,必须要进行以下几个步骤:

  1. 初始化:x = 1,存储在主内存。

  2. 线程A读取:A从主内存读取x,复制x=1到A的工作内存。

  3. 线程B读取:B从主内存读取x,复制x=1到B的工作内存。

  4. 线程A修改:A在工作内存中修改x=2

  5. 线程A写回:A将工作内存中的x=2写回主内存。

  6. 线程B重新读取:B从主内存读取最新的x=2,保证了数据的可见性。

这个过程展示了JMM如何确保多线程环境下的数据一致性

看起来这个流程没什么问题,但是线程A的修改和写回的操作不是原子性的,可能在线程A还未写回,线程B已经重新读取了,这个问题先按下不表


因此:volatile写-读的内存语义

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

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

2)禁止进行指令重排序

volatile关键字禁止指令重排序有两层意思:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行


🌰例子:以下面的代码来讲述使用和未使用volatile关键字会出现什么杨的情况来理解指令重排

 🔴未使用 volatile 关键字

class Example {
    int a = 0;
    boolean ready = false;

    public void writerThread() {
        a = 5;           // 语句1
        ready = true;    // 语句2
    }

    public void readerThread() {
        while (!ready) { // 语句3
            Thread.yield();
        }
        System.out.println(a); // 语句4
    }
}

编译器和处理器可能会为了优化性能而对指令进行重排序。在这种情况下,语句1和语句2之间没有数据依赖关系,所以它们可能被重排序。

例如:处理器可能会先执行语句2再执行语句1

可能的执行顺序

        写线程:执行语句2,然后执行语句1。

        读线程:执行语句3,检查 ready 是否为 true。如果 ready 已经被设置为 true(即使 a 还没有被设置为5),读线程将进入语句4并打印 a 的值,此时 a 可能还是默认的0。

结果

由于指令重排序,读线程可能在 a 被写线程修改之前就读取了 a 的值,导致输出结果为0,而不是预期的5。这就是指令重排序可能导致的线程安全问题

 🔴使用 volatile 关键字

class Example {
    volatile int a = 0;
    volatile boolean ready = false;

    public void writerThread() {
        a = 5;           // 语句1
        ready = true;    // 语句2
    }

    public void readerThread() {
        while (!ready) { // 语句3
            Thread.yield();
        }
        System.out.println(a); // 语句4
    }
}

指令重排序限制

volatile 关键字会阻止编译器和处理器对与 volatile 变量相关的指令进行重排序。这意味着语句1和语句2之间的执行顺序将被保留,确保了写线程先修改 a 的值,然后再设置 ready 为 true

结果

由于 volatile 的内存屏障效果,读线程在检查 ready 是否为 true 并进入语句4之前,将看到写线程对 a 的最新修改,从而避免了指令重排序引起的线程安全问题


总结来说,volatile 关键字通过提供内存屏障来限制指令重排序,确保了变量的可见性和一定程度上的有序性,从而帮助解决多线程环境下的指令重排序问题

2、 volatile关键字的使用场景

  • 适用场景:volatile适用于那些被多个线程访问但并不涉及复合操作(例如递增操作)的变量。典型的使用场景包括状态标志、控制变量等。

  • 不适用场景:不要将volatile用于需要原子性操作的场景,因为volatile并不能保证原子性。对于需要原子性操作的场景,应该使用锁或者Atomic原子类

实际开发中,几乎看不到volatile的使用,因为volatile只能保证可见性,并不能保证原子性,就需要结合CAS(Compare and Swap)

其实在Java中,java.util.concurrent.atomic包提供了一组原子类,比如AtomicIntegerAtomicLongAtomicBoolean等,它们提供了一种无锁的线程安全机制,以确保对变量的操作是原子性的。

当谈到Atomic原子类的实现原理时,CAS操作是其中的关键。CAS是一种乐观锁技术,它涉及比较内存中的值和预期值,如果相等,则使用新值替换内存中的值。在Java中,CAS是通过Unsafe类实现的,它是一种硬件级别的原子性操作

但是,CAS操作本身无法解决线程可见性的问题,这就是volatile关键字的作用。volatile关键字可以确保变量的写操作立即可见于其他线程,从而解决了线程之间的可见性问题。因此,Atomic原子类是结合了CAS和volatile关键字来实现线程安全

一般情况下都是直接使用的Atomic原子类来保证线程安全的情况,并不会去直接使用volatile关键字

在上面的例子中使用了volatile关键字修改共享变量x的过程,线程A的修改和写回的操作不是原子性的,那么CAS就可以解决这个问题,至于如何解决,在下篇文章再讲吧……


下一篇:​​​​​​​Java并发编程(三)—CAS的原理及应用场景-CSDN博客

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值